summaryrefslogtreecommitdiff
path: root/extension/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src')
-rw-r--r--extension/src/activation/activate.ts58
-rw-r--r--extension/src/activation/environmentSetup.ts147
-rw-r--r--extension/src/commands.ts32
-rw-r--r--extension/src/continueIdeClient.ts157
-rw-r--r--extension/src/debugPanel.ts5
-rw-r--r--extension/src/diffs.ts137
-rw-r--r--extension/src/extension.ts12
-rw-r--r--extension/src/lang-server/codeActions.ts58
-rw-r--r--extension/src/lang-server/codeLens.ts15
-rw-r--r--extension/src/suggestions.ts63
-rw-r--r--extension/src/telemetry.ts53
-rw-r--r--extension/src/util/messenger.ts2
-rw-r--r--extension/src/util/util.ts29
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 "⌘";
+ }
+}