diff options
Diffstat (limited to 'extension/src')
-rw-r--r-- | extension/src/activation/activate.ts | 58 | ||||
-rw-r--r-- | extension/src/activation/environmentSetup.ts | 147 | ||||
-rw-r--r-- | extension/src/commands.ts | 32 | ||||
-rw-r--r-- | extension/src/continueIdeClient.ts | 157 | ||||
-rw-r--r-- | extension/src/debugPanel.ts | 5 | ||||
-rw-r--r-- | extension/src/diffs.ts | 137 | ||||
-rw-r--r-- | extension/src/extension.ts | 12 | ||||
-rw-r--r-- | extension/src/lang-server/codeActions.ts | 58 | ||||
-rw-r--r-- | extension/src/lang-server/codeLens.ts | 15 | ||||
-rw-r--r-- | extension/src/suggestions.ts | 63 | ||||
-rw-r--r-- | extension/src/telemetry.ts | 53 | ||||
-rw-r--r-- | extension/src/util/messenger.ts | 2 | ||||
-rw-r--r-- | extension/src/util/util.ts | 29 |
13 files changed, 507 insertions, 261 deletions
diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index b03282e5..0c6e6009 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import { registerAllCommands } from "../commands"; import { registerAllCodeLensProviders } from "../lang-server/codeLens"; -import { sendTelemetryEvent, TelemetryEvent } from "../telemetry"; import IdeProtocolClient from "../continueIdeClient"; import { getContinueServerUrl } from "../bridge"; import { ContinueGUIWebviewViewProvider } from "../debugPanel"; @@ -10,6 +9,8 @@ import { startContinuePythonServer, } from "./environmentSetup"; import fetch from "node-fetch"; +import registerQuickFixProvider from "../lang-server/codeActions"; +import { get } from "http"; // import { CapturedTerminal } from "../terminal/terminalEmulator"; const PACKAGE_JSON_RAW_GITHUB_URL = @@ -35,21 +36,40 @@ export async function activateExtension(context: vscode.ExtensionContext) { }) .catch((e) => console.log("Error checking for extension updates: ", e)); + // Start the server and display loader if taking > 2 seconds const sessionIdPromise = (async () => { - // Start the Python server - await new Promise((resolve, reject) => { - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: - "Starting Continue Server... (it may take a minute to download Python packages)", - cancellable: false, - }, - async (progress, token) => { - await startContinuePythonServer(); - resolve(null); + await new Promise((resolve) => { + let serverStarted = false; + + // Start the server and set serverStarted to true when done + startContinuePythonServer().then(() => { + serverStarted = true; + resolve(null); + }); + + // Wait for 2 seconds + setTimeout(() => { + // If the server hasn't started after 2 seconds, show the notification + if (!serverStarted) { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: + "Starting Continue Server... (it may take a minute to download Python packages)", + cancellable: false, + }, + async (progress, token) => { + // Wait for the server to start + while (!serverStarted) { + await new Promise((innerResolve) => + setTimeout(innerResolve, 1000) + ); + } + return Promise.resolve(); + } + ); } - ); + }, 2000); }); // Initialize IDE Protocol Client @@ -58,11 +78,10 @@ export async function activateExtension(context: vscode.ExtensionContext) { `${serverUrl.replace("http", "ws")}/ide/ws`, context ); - - return ideProtocolClient.getSessionId(); + return await ideProtocolClient.getSessionId(); })(); - // Register the webview + // Register Continue GUI as sidebar webview, and beging a new session const provider = new ContinueGUIWebviewViewProvider(sessionIdPromise); context.subscriptions.push( @@ -74,9 +93,4 @@ export async function activateExtension(context: vscode.ExtensionContext) { } ) ); - - // Register commands and providers - sendTelemetryEvent(TelemetryEvent.ExtensionActivated); - registerAllCodeLensProviders(context); - registerAllCommands(context); } diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts index 7bd08929..5a9345a6 100644 --- a/extension/src/activation/environmentSetup.ts +++ b/extension/src/activation/environmentSetup.ts @@ -9,7 +9,9 @@ import fetch from "node-fetch"; import * as vscode from "vscode"; import * as os from "os"; import fkill from "fkill"; -import { sendTelemetryEvent, TelemetryEvent } from "../telemetry"; + +const WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR = + "A Python virtual enviroment cannot be activated because running scripts is disabled for this user. In order to use Continue, please enable signed scripts to run with this command in PowerShell: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, reload VS Code, and then try again."; const MAX_RETRIES = 3; async function retryThenFail( @@ -17,17 +19,43 @@ async function retryThenFail( retries: number = MAX_RETRIES ): Promise<any> { try { + if (retries < MAX_RETRIES && process.platform === "win32") { + let [stdout, stderr] = await runCommand("Get-ExecutionPolicy"); + if (!stdout.includes("RemoteSigned")) { + [stdout, stderr] = await runCommand( + "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" + ); + console.log("Execution policy stdout: ", stdout); + console.log("Execution policy stderr: ", stderr); + } + } + return await fn(); } catch (e: any) { if (retries > 0) { return await retryThenFail(fn, retries - 1); } - vscode.window.showInformationMessage( - "Failed to set up Continue extension. Please email nate@continue.dev and we'll get this fixed ASAP!" - ); - sendTelemetryEvent(TelemetryEvent.ExtensionSetupError, { - error: e.message, - }); + + // Show corresponding error message depending on the platform + let msg = + "Failed to set up Continue extension. Please email hi@continue.dev and we'll get this fixed ASAP!"; + try { + switch (process.platform) { + case "win32": + msg = WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR; + break; + case "darwin": + break; + case "linux": + const [pythonCmd] = await getPythonPipCommands(); + msg = await getLinuxAptInstallError(pythonCmd); + break; + } + } finally { + console.log("After retries, failed to set up Continue extension", msg); + vscode.window.showErrorMessage(msg); + } + throw e; } } @@ -51,12 +79,6 @@ async function runCommand(cmd: string): Promise<[string, string | undefined]> { stdout = ""; } - if (stderr) { - sendTelemetryEvent(TelemetryEvent.ExtensionSetupError, { - error: stderr, - }); - } - return [stdout, stderr]; } @@ -107,7 +129,7 @@ export async function getPythonPipCommands() { if (!versionExists) { vscode.window.showErrorMessage( - "Continue requires Python3 version 3.8 or greater. Please update your Python3 installation, reload VS Code, and try again." + "Continue requires Python version 3.8 or greater. Please update your Python installation, reload VS Code, and try again." ); throw new Error("Python3.8 or greater is not installed."); } @@ -186,16 +208,22 @@ async function checkRequirementsInstalled() { return fs.existsSync(continuePath); } -async function setupPythonEnv() { - console.log("Setting up python env for Continue extension..."); - - const [pythonCmd, pipCmd] = await getPythonPipCommands(); - const [activateCmd, pipUpgradeCmd] = getActivateUpgradeCommands( - pythonCmd, - pipCmd - ); +async function getLinuxAptInstallError(pythonCmd: string) { + // First, try to run the command to install python3-venv + let [stdout, stderr] = await runCommand(`${pythonCmd} --version`); + if (stderr) { + await vscode.window.showErrorMessage( + "Python3 is not installed. Please install from https://www.python.org/downloads, reload VS Code, and try again." + ); + throw new Error(stderr); + } + const version = stdout.split(" ")[1].split(".")[1]; + const installVenvCommand = `apt-get install python3.${version}-venv`; + await runCommand("apt-get update"); + return `[Important] Continue needs to create a Python virtual environment, but python3.${version}-venv is not installed. Please run this command in your terminal: \`${installVenvCommand}\`, reload VS Code, and then try again.`; +} - // First, create the virtual environment +async function createPythonVenv(pythonCmd: string) { if (checkEnvExists()) { console.log("Python env already exists, skipping..."); } else { @@ -210,32 +238,38 @@ async function setupPythonEnv() { stderr && stderr.includes("running scripts is disabled on this system") ) { - await vscode.window.showErrorMessage( - "A Python virtual enviroment cannot be activated because running scripts is disabled for this user. Please enable signed scripts to run with this command in PowerShell: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, reload VS Code, and then try again." - ); + console.log("Scripts disabled error when trying to create env"); + await vscode.window.showErrorMessage(WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR); throw new Error(stderr); } else if ( stderr?.includes("On Debian/Ubuntu systems") || stdout?.includes("On Debian/Ubuntu systems") ) { - // First, try to run the command to install python3-venv - let [stdout, stderr] = await runCommand(`${pythonCmd} --version`); - if (stderr) { - await vscode.window.showErrorMessage( - "Python3 is not installed. Please install from https://www.python.org/downloads, reload VS Code, and try again." - ); - throw new Error(stderr); - } - const version = stdout.split(" ")[1].split(".")[1]; - const installVenvCommand = `apt-get install python3.${version}-venv`; - await runCommand("apt-get update"); - // Ask the user to run the command to install python3-venv (requires sudo, so we can't) - // First, get the python version - const msg = `[Important] Continue needs to create a Python virtual environment, but python3.${version}-venv is not installed. Please run this command in your terminal: \`${installVenvCommand}\`, reload VS Code, and then try again.`; + const msg = await getLinuxAptInstallError(pythonCmd); console.log(msg); await vscode.window.showErrorMessage(msg); } else if (checkEnvExists()) { console.log("Successfully set up python env at ", `${serverPath()}/env`); + } else if ( + stderr?.includes("Permission denied") && + stderr?.includes("python.exe") + ) { + // This might mean that another window is currently using the python.exe file to install requirements + // So we want to wait and try again + let i = 0; + await new Promise((resolve, reject) => + setInterval(() => { + if (i > 5) { + reject("Timed out waiting for other window to create env..."); + } + if (checkEnvExists()) { + resolve(null); + } else { + console.log("Waiting for other window to create env..."); + } + i++; + }, 5000) + ); } else { const msg = [ "Python environment not successfully created. Trying again. Here was the stdout + stderr: ", @@ -246,9 +280,22 @@ async function setupPythonEnv() { throw new Error(msg); } } +} + +async function setupPythonEnv() { + console.log("Setting up python env for Continue extension..."); + + const [pythonCmd, pipCmd] = await getPythonPipCommands(); + const [activateCmd, pipUpgradeCmd] = getActivateUpgradeCommands( + pythonCmd, + pipCmd + ); - // Install the requirements await retryThenFail(async () => { + // First, create the virtual environment + await createPythonVenv(pythonCmd); + + // Install the requirements if (await checkRequirementsInstalled()) { console.log("Python requirements already installed, skipping..."); } else { @@ -343,6 +390,14 @@ function serverPath(): string { return sPath; } +export function devDataPath(): string { + const sPath = path.join(getContinueGlobalPath(), "dev_data"); + if (!fs.existsSync(sPath)) { + fs.mkdirSync(sPath); + } + return sPath; +} + function serverVersionPath(): string { return path.join(serverPath(), "server_version.txt"); } @@ -409,21 +464,17 @@ export async function startContinuePythonServer() { console.log(`stdout: ${data}`); if ( data.includes("Uvicorn running on") || // Successfully started the server - data.includes("address already in use") // The server is already running (probably a simultaneously opened VS Code window) + data.includes("only one usage of each socket address") || // [windows] The server is already running (probably a simultaneously opened VS Code window) + data.includes("address already in use") // [mac/linux] The server is already running (probably a simultaneously opened VS Code window) ) { console.log("Successfully started Continue python server"); resolve(null); } else if (data.includes("ERROR") || data.includes("Traceback")) { - sendTelemetryEvent(TelemetryEvent.ExtensionSetupError, { - error: data, - }); + console.log("Error starting Continue python server: ", data); } }); child.on("error", (error: any) => { console.log(`error: ${error.message}`); - sendTelemetryEvent(TelemetryEvent.ExtensionSetupError, { - error: error.message, - }); }); // Write the current version of vscode to a file called server_version.txt diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 0b002549..1da2f04e 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -16,9 +16,14 @@ import { import { acceptDiffCommand, rejectDiffCommand } from "./diffs"; import * as bridge from "./bridge"; import { debugPanelWebview } from "./debugPanel"; -import { sendTelemetryEvent, TelemetryEvent } from "./telemetry"; import { ideProtocolClient } from "./activation/activate"; +let focusedOnContinueInput = false; + +export const setFocusedOnContinueInput = (value: boolean) => { + focusedOnContinueInput = value; +}; + // Copy everything over from extension.ts const commandsMap: { [command: string]: (...args: any) => any } = { "continue.suggestionDown": suggestionDownCommand, @@ -29,11 +34,26 @@ const commandsMap: { [command: string]: (...args: any) => any } = { "continue.rejectDiff": rejectDiffCommand, "continue.acceptAllSuggestions": acceptAllSuggestionsCommand, "continue.rejectAllSuggestions": rejectAllSuggestionsCommand, + "continue.quickFix": async (message: string, code: string, edit: boolean) => { + ideProtocolClient.sendMainUserInput( + `${ + edit ? "/edit " : "" + }${code}\n\nHow do I fix this problem in the above code?: ${message}` + ); + if (!edit) { + vscode.commands.executeCommand("continue.continueGUIView.focus"); + } + }, "continue.focusContinueInput": async () => { - vscode.commands.executeCommand("continue.continueGUIView.focus"); - debugPanelWebview?.postMessage({ - type: "focusContinueInput", - }); + if (focusedOnContinueInput) { + vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup"); + } else { + vscode.commands.executeCommand("continue.continueGUIView.focus"); + debugPanelWebview?.postMessage({ + type: "focusContinueInput", + }); + } + focusedOnContinueInput = !focusedOnContinueInput; }, "continue.quickTextEntry": async () => { const text = await vscode.window.showInputBox({ @@ -53,4 +73,4 @@ export function registerAllCommands(context: vscode.ExtensionContext) { vscode.commands.registerCommand(command, callback) ); } -}
\ No newline at end of file +} diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index 4c1fdf1e..802afc1d 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -15,6 +15,12 @@ import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; import fs = require("fs"); import { WebsocketMessenger } from "./util/messenger"; import { diffManager } from "./diffs"; +import path = require("path"); +import { registerAllCodeLensProviders } from "./lang-server/codeLens"; +import { registerAllCommands } from "./commands"; +import registerQuickFixProvider from "./lang-server/codeActions"; + +const continueVirtualDocumentScheme = "continue"; class IdeProtocolClient { private messenger: WebsocketMessenger | null = null; @@ -73,6 +79,11 @@ class IdeProtocolClient { this._serverUrl = serverUrl; this._newWebsocketMessenger(); + // Register commands and providers + registerAllCodeLensProviders(context); + registerAllCommands(context); + registerQuickFixProvider(); + // Setup listeners for any file changes in open editors // vscode.workspace.onDidChangeTextDocument((event) => { // if (this._makingEdit === 0) { @@ -103,8 +114,11 @@ class IdeProtocolClient { // } // }); - // Setup listeners for any file changes in open editors + // Setup listeners for any selection changes in open editors vscode.window.onDidChangeTextEditorSelection((event) => { + if (this.editorIsTerminal(event.textEditor)) { + return; + } if (this._highlightDebounce) { clearTimeout(this._highlightDebounce); } @@ -132,6 +146,41 @@ class IdeProtocolClient { this.sendHighlightedCode(highlightedCode); }, 100); }); + + // Register a content provider for the readonly virtual documents + const documentContentProvider = new (class + implements vscode.TextDocumentContentProvider + { + // emitter and its event + onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>(); + onDidChange = this.onDidChangeEmitter.event; + + provideTextDocumentContent(uri: vscode.Uri): string { + return uri.query; + } + })(); + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + continueVirtualDocumentScheme, + documentContentProvider + ) + ); + + // Listen for changes to settings.json + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("continue")) { + vscode.window + .showInformationMessage( + "Please reload VS Code for changes to Continue settings to take effect.", + "Reload" + ) + .then((selection) => { + if (selection === "Reload") { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + }); + } + }); } async handleMessage( @@ -196,6 +245,9 @@ class IdeProtocolClient { this.openFile(data.filepath); // TODO: Close file if False break; + case "showVirtualFile": + this.showVirtualFile(data.name, data.contents); + break; case "setSuggestionsLocked": this.setSuggestionsLocked(data.filepath, data.locked); break; @@ -291,6 +343,20 @@ class IdeProtocolClient { openEditorAndRevealRange(filepath, undefined, vscode.ViewColumn.One); } + showVirtualFile(name: string, contents: string) { + vscode.workspace + .openTextDocument( + vscode.Uri.parse( + `${continueVirtualDocumentScheme}:${name}?${encodeURIComponent( + contents + )}` + ) + ) + .then((doc) => { + vscode.window.showTextDocument(doc, { preview: false }); + }); + } + setSuggestionsLocked(filepath: string, locked: boolean) { editorSuggestionsLocked.set(filepath, locked); // TODO: Rerender? @@ -350,44 +416,55 @@ class IdeProtocolClient { // ------------------------------------ // // Respond to request + private editorIsTerminal(editor: vscode.TextEditor) { + return ( + !!path.basename(editor.document.uri.fsPath).match(/\d/) || + (editor.document.languageId === "plaintext" && + editor.document.getText() === "accessible-buffer-accessible-buffer-") + ); + } + getOpenFiles(): string[] { return vscode.window.visibleTextEditors - .filter((editor) => { - return !( - editor.document.uri.fsPath.endsWith("/1") || - (editor.document.languageId === "plaintext" && - editor.document.getText() === - "accessible-buffer-accessible-buffer-") - ); - }) + .filter((editor) => !this.editorIsTerminal(editor)) .map((editor) => { return editor.document.uri.fsPath; }); } getVisibleFiles(): string[] { - return vscode.window.visibleTextEditors.map((editor) => { - return editor.document.uri.fsPath; - }); + return vscode.window.visibleTextEditors + .filter((editor) => !this.editorIsTerminal(editor)) + .map((editor) => { + return editor.document.uri.fsPath; + }); } saveFile(filepath: string) { - vscode.window.visibleTextEditors.forEach((editor) => { - if (editor.document.uri.fsPath === filepath) { - editor.document.save(); - } - }); + vscode.window.visibleTextEditors + .filter((editor) => !this.editorIsTerminal(editor)) + .forEach((editor) => { + if (editor.document.uri.fsPath === filepath) { + editor.document.save(); + } + }); } readFile(filepath: string): string { let contents: string | undefined; - vscode.window.visibleTextEditors.forEach((editor) => { - if (editor.document.uri.fsPath === filepath) { - contents = editor.document.getText(); + vscode.window.visibleTextEditors + .filter((editor) => !this.editorIsTerminal(editor)) + .forEach((editor) => { + if (editor.document.uri.fsPath === filepath) { + contents = editor.document.getText(); + } + }); + if (typeof contents === "undefined") { + if (fs.existsSync(filepath)) { + contents = fs.readFileSync(filepath, "utf-8"); + } else { + contents = ""; } - }); - if (!contents) { - contents = fs.readFileSync(filepath, "utf-8"); } return contents; } @@ -421,25 +498,27 @@ class IdeProtocolClient { getHighlightedCode(): RangeInFile[] { // TODO let rangeInFiles: RangeInFile[] = []; - vscode.window.visibleTextEditors.forEach((editor) => { - editor.selections.forEach((selection) => { - // if (!selection.isEmpty) { - rangeInFiles.push({ - filepath: editor.document.uri.fsPath, - range: { - start: { - line: selection.start.line, - character: selection.start.character, - }, - end: { - line: selection.end.line, - character: selection.end.character, + vscode.window.visibleTextEditors + .filter((editor) => !this.editorIsTerminal(editor)) + .forEach((editor) => { + editor.selections.forEach((selection) => { + // if (!selection.isEmpty) { + rangeInFiles.push({ + filepath: editor.document.uri.fsPath, + range: { + start: { + line: selection.start.line, + character: selection.start.character, + }, + end: { + line: selection.end.line, + character: selection.end.character, + }, }, - }, + }); + // } }); - // } }); - }); return rangeInFiles; } diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts index 5e1689d1..dd24a8d8 100644 --- a/extension/src/debugPanel.ts +++ b/extension/src/debugPanel.ts @@ -6,6 +6,7 @@ import { openEditorAndRevealRange, } from "./util/vscode"; import { RangeInFile } from "./client"; +import { setFocusedOnContinueInput } from "./commands"; const WebSocket = require("ws"); let websocketConnections: { [url: string]: WebsocketConnection | undefined } = @@ -226,6 +227,10 @@ export function setupDebugPanel( openEditorAndRevealRange(data.path, undefined, vscode.ViewColumn.One); break; } + case "blurContinueInput": { + setFocusedOnContinueInput(false); + break; + } case "withProgress": { // This message allows withProgress to be used in the webview if (data.done) { diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts index 28089fc6..1130a06a 100644 --- a/extension/src/diffs.ts +++ b/extension/src/diffs.ts @@ -3,21 +3,30 @@ import * as path from "path"; import * as fs from "fs"; import * as vscode from "vscode"; import { extensionContext, ideProtocolClient } from "./activation/activate"; +import { getMetaKeyLabel } from "./util/util"; +import { devDataPath } from "./activation/environmentSetup"; interface DiffInfo { originalFilepath: string; newFilepath: string; editor?: vscode.TextEditor; step_index: number; + range: vscode.Range; } -export const DIFF_DIRECTORY = path.join(os.homedir(), ".continue", "diffs"); +export const DIFF_DIRECTORY = path + .join(os.homedir(), ".continue", "diffs") + .replace(/^C:/, "c:"); 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(); + diffAtNewFilepath(newFilepath: string): DiffInfo | undefined { + return this.diffs.get(newFilepath); + } + private setupDirectory() { // Make sure the diff directory exists if (!fs.existsSync(DIFF_DIRECTORY)) { @@ -29,12 +38,25 @@ class DiffManager { constructor() { this.setupDirectory(); + + // Listen for file closes, and if it's a diff file, clean up + vscode.workspace.onDidCloseTextDocument((document) => { + const newFilepath = document.uri.fsPath; + const diffInfo = this.diffs.get(newFilepath); + if (diffInfo) { + this.cleanUpDiff(diffInfo, false); + } + }); } private escapeFilepath(filepath: string): string { return filepath.replace(/\\/g, "_").replace(/\//g, "_"); } + private getNewFilepath(originalFilepath: string): string { + return path.join(DIFF_DIRECTORY, this.escapeFilepath(originalFilepath)); + } + private openDiffEditor( originalFilepath: string, newFilepath: string @@ -47,7 +69,7 @@ class DiffManager { return undefined; } - const rightUri = vscode.Uri.parse(newFilepath); + const rightUri = vscode.Uri.file(newFilepath); const leftUri = vscode.Uri.file(originalFilepath); const title = "Continue Diff"; console.log( @@ -77,7 +99,7 @@ class DiffManager { ) { vscode.window .showInformationMessage( - "Accept (⌘⇧↩) or reject (⌘⇧⌫) at the top of the file.", + `Accept (${getMetaKeyLabel()}⇧↩) or reject (${getMetaKeyLabel()}⇧⌫) at the top of the file.`, "Got it", "Don't show again" ) @@ -95,6 +117,17 @@ class DiffManager { return editor; } + private _findFirstDifferentLine(contentA: string, contentB: string): number { + const linesA = contentA.split("\n"); + const linesB = contentB.split("\n"); + for (let i = 0; i < linesA.length && i < linesB.length; i++) { + if (linesA[i] !== linesB[i]) { + return i; + } + } + return 0; + } + writeDiff( originalFilepath: string, newContent: string, @@ -103,18 +136,20 @@ class DiffManager { this.setupDirectory(); // Create or update existing diff - const newFilepath = path.join( - DIFF_DIRECTORY, - this.escapeFilepath(originalFilepath) - ); + const newFilepath = this.getNewFilepath(originalFilepath); fs.writeFileSync(newFilepath, newContent); // Open the diff editor if this is a new diff if (!this.diffs.has(newFilepath)) { + // Figure out the first line that is different + const oldContent = ideProtocolClient.readFile(originalFilepath); + const line = this._findFirstDifferentLine(oldContent, newContent); + const diffInfo: DiffInfo = { originalFilepath, newFilepath, step_index, + range: new vscode.Range(line, 0, line + 1, 0), }; this.diffs.set(newFilepath, diffInfo); } @@ -126,12 +161,17 @@ class DiffManager { this.diffs.set(newFilepath, diffInfo); } + vscode.commands.executeCommand( + "workbench.action.files.revert", + vscode.Uri.file(newFilepath) + ); + return newFilepath; } - cleanUpDiff(diffInfo: DiffInfo) { + cleanUpDiff(diffInfo: DiffInfo, hideEditor: boolean = true) { // Close the editor, remove the record, delete the file - if (diffInfo.editor) { + if (hideEditor && diffInfo.editor) { vscode.window.showTextDocument(diffInfo.editor.document); vscode.commands.executeCommand("workbench.action.closeActiveEditor"); } @@ -139,10 +179,38 @@ class DiffManager { fs.unlinkSync(diffInfo.newFilepath); } + private inferNewFilepath() { + const activeEditorPath = + vscode.window.activeTextEditor?.document.uri.fsPath; + if (activeEditorPath && path.dirname(activeEditorPath) === DIFF_DIRECTORY) { + return activeEditorPath; + } + const visibleEditors = vscode.window.visibleTextEditors.map( + (editor) => editor.document.uri.fsPath + ); + for (const editorPath of visibleEditors) { + if (path.dirname(editorPath) === DIFF_DIRECTORY) { + for (const otherEditorPath of visibleEditors) { + if ( + path.dirname(otherEditorPath) !== DIFF_DIRECTORY && + this.getNewFilepath(otherEditorPath) === editorPath + ) { + return editorPath; + } + } + } + } + + if (this.diffs.size === 1) { + return Array.from(this.diffs.keys())[0]; + } + return undefined; + } + 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]; + // When coming from a keyboard shortcut, we have to infer the newFilepath from visible text editors + if (!newFilepath) { + newFilepath = this.inferNewFilepath(); } if (!newFilepath) { console.log("No newFilepath provided to accept the diff"); @@ -166,12 +234,14 @@ class DiffManager { ); this.cleanUpDiff(diffInfo); }); + + recordAcceptReject(true, 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) { + newFilepath = this.inferNewFilepath(); } if (!newFilepath) { console.log( @@ -195,11 +265,50 @@ class DiffManager { .then(() => { this.cleanUpDiff(diffInfo); }); + + recordAcceptReject(false, diffInfo); } } export const diffManager = new DiffManager(); +function recordAcceptReject(accepted: boolean, diffInfo: DiffInfo) { + const collectOn = vscode.workspace + .getConfiguration("continue") + .get<boolean>("dataSwitch"); + + if (collectOn) { + const devDataDir = devDataPath(); + const suggestionsPath = path.join(devDataDir, "suggestions.json"); + + // Initialize suggestions list + let suggestions = []; + + // Check if suggestions.json exists + if (fs.existsSync(suggestionsPath)) { + const rawData = fs.readFileSync(suggestionsPath, "utf-8"); + suggestions = JSON.parse(rawData); + } + + // Add the new suggestion to the list + suggestions.push({ + accepted, + timestamp: Date.now(), + suggestion: diffInfo.originalFilepath, + }); + + // Send the suggestion to the server + // ideProtocolClient.sendAcceptRejectSuggestion(accepted); + + // Write the updated suggestions back to the file + fs.writeFileSync( + suggestionsPath, + JSON.stringify(suggestions, null, 4), + "utf-8" + ); + } +} + export async function acceptDiffCommand(newFilepath?: string) { diffManager.acceptDiff(newFilepath); ideProtocolClient.sendAcceptRejectDiff(true); diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 6959ec05..f2e580a1 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -17,15 +17,5 @@ async function dynamicImportAndActivate(context: vscode.ExtensionContext) { } export function activate(context: vscode.ExtensionContext) { - // Only show progress if we have to setup - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Setting up Continue extension...", - cancellable: false, - }, - async () => { - dynamicImportAndActivate(context); - } - ); + dynamicImportAndActivate(context); } diff --git a/extension/src/lang-server/codeActions.ts b/extension/src/lang-server/codeActions.ts new file mode 100644 index 00000000..892c69be --- /dev/null +++ b/extension/src/lang-server/codeActions.ts @@ -0,0 +1,58 @@ +import * as vscode from "vscode"; + +class ContinueQuickFixProvider implements vscode.CodeActionProvider { + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix, + ]; + + provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): vscode.ProviderResult<(vscode.Command | vscode.CodeAction)[]> { + if (context.diagnostics.length === 0) { + return []; + } + + const createQuickFix = (edit: boolean) => { + const diagnostic = context.diagnostics[0]; + const quickFix = new vscode.CodeAction( + edit ? "Fix with Continue" : "Ask Continue", + vscode.CodeActionKind.QuickFix + ); + quickFix.isPreferred = false; + const surroundingRange = new vscode.Range( + Math.max(0, range.start.line - 3), + 0, + Math.min(document.lineCount, range.end.line + 3), + 0 + ); + quickFix.command = { + command: "continue.quickFix", + title: "Continue Quick Fix", + arguments: [ + diagnostic.message, + document.getText(surroundingRange), + edit, + ], + }; + return quickFix; + }; + return [ + // createQuickFix(true), + createQuickFix(false), + ]; + } +} + +export default function registerQuickFixProvider() { + // In your extension's activate function: + vscode.languages.registerCodeActionsProvider( + { language: "*" }, + new ContinueQuickFixProvider(), + { + providedCodeActionKinds: ContinueQuickFixProvider.providedCodeActionKinds, + } + ); +} diff --git a/extension/src/lang-server/codeLens.ts b/extension/src/lang-server/codeLens.ts index 778b98dc..ba80e557 100644 --- a/extension/src/lang-server/codeLens.ts +++ b/extension/src/lang-server/codeLens.ts @@ -2,7 +2,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"; +import { DIFF_DIRECTORY, diffManager } from "../diffs"; +import { getMetaKeyLabel } from "../util/util"; class SuggestionsCodeLensProvider implements vscode.CodeLensProvider { public provideCodeLenses( document: vscode.TextDocument, @@ -35,7 +36,7 @@ class SuggestionsCodeLensProvider implements vscode.CodeLensProvider { if (codeLenses.length === 2) { codeLenses.push( new vscode.CodeLens(range, { - title: "(⌘⇧↩/⌘⇧⌫ to accept/reject all)", + title: `(${getMetaKeyLabel()}⇧↩/${getMetaKeyLabel()}⇧⌫ to accept/reject all)`, command: "", }) ); @@ -53,15 +54,19 @@ class DiffViewerCodeLensProvider implements vscode.CodeLensProvider { ): 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); + let range = new vscode.Range(0, 0, 1, 0); + const diffInfo = diffManager.diffAtNewFilepath(document.uri.fsPath); + if (diffInfo) { + range = diffInfo.range; + } codeLenses.push( new vscode.CodeLens(range, { - title: "Accept ✅ (⌘⇧↩)", + title: `Accept All ✅ (${getMetaKeyLabel()}⇧↩)`, command: "continue.acceptDiff", arguments: [document.uri.fsPath], }), new vscode.CodeLens(range, { - title: "Reject ❌ (⌘⇧⌫)", + title: `Reject All ❌ (${getMetaKeyLabel()}⇧⌫)`, command: "continue.rejectDiff", arguments: [document.uri.fsPath], }) diff --git a/extension/src/suggestions.ts b/extension/src/suggestions.ts index 6e5a444f..5c2b8860 100644 --- a/extension/src/suggestions.ts +++ b/extension/src/suggestions.ts @@ -1,9 +1,6 @@ import * as vscode from "vscode"; -import { sendTelemetryEvent, TelemetryEvent } from "./telemetry"; import { openEditorAndRevealRange } from "./util/vscode"; -import { translate, readFileAtRange } from "./util/vscode"; -import * as fs from "fs"; -import * as path from "path"; +import { translate } from "./util/vscode"; import { registerAllCodeLensProviders } from "./lang-server/codeLens"; import { extensionContext, ideProtocolClient } from "./activation/activate"; @@ -214,62 +211,6 @@ function selectSuggestion( : suggestion.newRange; } - let workspaceDir = vscode.workspace.workspaceFolders - ? vscode.workspace.workspaceFolders[0]?.uri.fsPath - : undefined; - - let collectOn = vscode.workspace - .getConfiguration("continue") - .get<boolean>("dataSwitch"); - - if (workspaceDir && collectOn) { - let continueDir = path.join(workspaceDir, ".continue"); - - // Check if .continue directory doesn't exists - if (!fs.existsSync(continueDir)) { - fs.mkdirSync(continueDir); - } - - let suggestionsPath = path.join(continueDir, "suggestions.json"); - - // Initialize suggestions list - let suggestions = []; - - // Check if suggestions.json exists - if (fs.existsSync(suggestionsPath)) { - let rawData = fs.readFileSync(suggestionsPath, "utf-8"); - suggestions = JSON.parse(rawData); - } - - const accepted = - accept === "new" || (accept === "selected" && suggestion.newSelected); - suggestions.push({ - accepted, - timestamp: Date.now(), - suggestion: suggestion.newContent, - }); - ideProtocolClient.sendAcceptRejectSuggestion(accepted); - - // Write the updated suggestions back to the file - fs.writeFileSync( - suggestionsPath, - JSON.stringify(suggestions, null, 4), - "utf-8" - ); - - // If it's not already there, add .continue to .gitignore - const gitignorePath = path.join(workspaceDir, ".gitignore"); - if (fs.existsSync(gitignorePath)) { - const gitignoreData = fs.readFileSync(gitignorePath, "utf-8"); - const gitIgnoreLines = gitignoreData.split("\n"); - if (!gitIgnoreLines.includes(".continue")) { - fs.appendFileSync(gitignorePath, "\n.continue\n"); - } - } else { - fs.writeFileSync(gitignorePath, ".continue\n"); - } - } - rangeToDelete = new vscode.Range( rangeToDelete.start, new vscode.Position(rangeToDelete.end.line, 0) @@ -302,7 +243,6 @@ function selectSuggestion( } export function acceptSuggestionCommand(key: SuggestionRanges | null = null) { - sendTelemetryEvent(TelemetryEvent.SuggestionAccepted); selectSuggestion("selected", key); } @@ -329,7 +269,6 @@ export function rejectAllSuggestionsCommand() { export async function rejectSuggestionCommand( key: SuggestionRanges | null = null ) { - sendTelemetryEvent(TelemetryEvent.SuggestionRejected); selectSuggestion("old", key); } diff --git a/extension/src/telemetry.ts b/extension/src/telemetry.ts deleted file mode 100644 index db5cb8ca..00000000 --- a/extension/src/telemetry.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as Segment from "@segment/analytics-node"; -import * as vscode from "vscode"; - -// Setup Segment -const SEGMENT_WRITE_KEY = "57yy2uYXH2bwMuy7djm9PorfFlYqbJL1"; -const analytics = new Segment.Analytics({ writeKey: SEGMENT_WRITE_KEY }); -analytics.identify({ - userId: vscode.env.machineId, - // traits: { - // name: "Michael Bolton", - // email: "mbolton@example.com", - // createdAt: new Date("2014-06-14T02:00:19.467Z"), - // }, -}); - -// Enum of telemetry events -export enum TelemetryEvent { - // Extension has been activated - ExtensionActivated = "ExtensionActivated", - // Suggestion has been accepted - SuggestionAccepted = "SuggestionAccepted", - // Suggestion has been rejected - SuggestionRejected = "SuggestionRejected", - // Queried universal prompt - UniversalPromptQuery = "UniversalPromptQuery", - // `Explain Code` button clicked - ExplainCode = "ExplainCode", - // `Generate Ideas` button clicked - GenerateIdeas = "GenerateIdeas", - // `Suggest Fix` button clicked - SuggestFix = "SuggestFix", - // `Create Test` button clicked - CreateTest = "CreateTest", - // `AutoDebug This Test` button clicked - AutoDebugThisTest = "AutoDebugThisTest", - // Command run to generate docstring - GenerateDocstring = "GenerateDocstring", - // Error setting up the extension - ExtensionSetupError = "ExtensionSetupError", -} - -export function sendTelemetryEvent( - event: TelemetryEvent, - properties?: Record<string, any> -) { - if (!vscode.env.isTelemetryEnabled) return; - - analytics.track({ - event, - userId: vscode.env.machineId, - properties, - }); -} diff --git a/extension/src/util/messenger.ts b/extension/src/util/messenger.ts index 7fd71ddd..3044898e 100644 --- a/extension/src/util/messenger.ts +++ b/extension/src/util/messenger.ts @@ -15,7 +15,7 @@ export abstract class Messenger { abstract onOpen(callback: () => void): void; abstract onClose(callback: () => void): void; - + abstract onError(callback: () => void): void; abstract sendAndReceive(messageType: string, data: any): Promise<any>; diff --git a/extension/src/util/util.ts b/extension/src/util/util.ts index d33593e1..dfc10c90 100644 --- a/extension/src/util/util.ts +++ b/extension/src/util/util.ts @@ -1,5 +1,6 @@ import { RangeInFile, SerializedDebugContext } from "../client"; import * as fs from "fs"; +const os = require("os"); function charIsEscapedAtIndex(index: number, str: string): boolean { if (index === 0) return false; @@ -113,3 +114,31 @@ export function debounced(delay: number, fn: Function) { }, delay); }; } + +type Platform = "mac" | "linux" | "windows" | "unknown"; + +function getPlatform(): Platform { + const platform = os.platform(); + if (platform === "darwin") { + return "mac"; + } else if (platform === "linux") { + return "linux"; + } else if (platform === "win32") { + return "windows"; + } else { + return "unknown"; + } +} + +export function getMetaKeyLabel() { + const platform = getPlatform(); + switch (platform) { + case "mac": + return "⌘"; + case "linux": + case "windows": + return "^"; + default: + return "⌘"; + } +} |