Skip to content

Commit

Permalink
[vscode][7/n] Setup Env Variables: Connect missing API keys error to …
Browse files Browse the repository at this point in the history
…command

Now it's more intuitive for users to know what needs to be done when they receive this error. I also moved the logic for the command into a utils file so it can be accessed both by activation flow and the error checking flow in the editor

## Test Plan

Getting missing API key error

https://github.com/lastmile-ai/aiconfig/assets/151060367/cc6c51e5-d205-4afa-a3b4-dc04f68d4946

Walkthrough commands

https://github.com/lastmile-ai/aiconfig/assets/151060367/bda40b3c-d260-40a2-82da-563d363152b3
  • Loading branch information
Rossdan Craig rossdan@lastmileai.dev committed Feb 22, 2024
1 parent 90a27b9 commit e177917
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 121 deletions.
30 changes: 22 additions & 8 deletions vscode-extension/src/aiConfigEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getCurrentWorkingDirectory,
getDocumentFromServer,
getPythonPath,
setupEnvironmentVariables,
updateServerState,
updateWebviewEditorThemeMode,
waitUntilServerReady,
Expand Down Expand Up @@ -340,12 +341,23 @@ export class AIConfigEditorProvider implements vscode.CustomTextEditorProvider {

const message = notification.message;

// Notification supports 'details' for modal only. For now, just show title
// in notification toast and full message in output channel.
const notificationAction = await notificationFn(
notification.title,
message ? "Details" : undefined
);
let notificationAction;
// TODO: Create a constant value somewhere in lastmile-utils to
// centralize string error message for missing API key. This
// logic is defined in https://github.com/lastmile-ai/aiconfig/blob/33fb852854d0bd64b8ddb4e52320112782008b99/python/src/aiconfig/util/config_utils.py#L41
if (message.includes("Missing API key")) {
notificationAction = await notificationFn(
"Looks like you're missing an API key, please set it in your env variables",
"Setup Environment Variables"
);
} else {
// Notification supports 'details' for modal only. For now, just show title
// in notification toast and full message in output channel.
notificationAction = await notificationFn(
notification.title,
message ? "Details" : undefined
);
}

if (message) {
outputChannelFn(
Expand All @@ -354,8 +366,10 @@ export class AIConfigEditorProvider implements vscode.CustomTextEditorProvider {
document
)
);
// If user clicked "Details", show & focus the output channel
if (notificationAction === "Details") {
if (notificationAction === "Setup Environment Variables") {
await setupEnvironmentVariables(this.context);
} else if (notificationAction === "Details") {
// If user clicked "Details", show & focus the output channel
this.extensionOutputChannel.show(/*preserveFocus*/ true);
}
}
Expand Down
114 changes: 1 addition & 113 deletions vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SUPPORTED_FILE_EXTENSIONS,
isPythonVersionAtLeast310,
showGuideForPythonInstallation,
setupEnvironmentVariables,
} from "./util";
import { AIConfigEditorProvider } from "./aiConfigEditor";
import { AIConfigEditorManager } from "./aiConfigEditorManager";
Expand Down Expand Up @@ -720,119 +721,6 @@ async function checkPip() {
});
}

/**
* Creates an .env file (or opens it if it already exists) to define environment variables
* 1) If .env file exists:
* a) Add helper lines on how to add common API keys (if not currently present)
* 2) If .env file doesn't exist
* b) Add template file containing helper lines from 1a above
*/
async function setupEnvironmentVariables(context: vscode.ExtensionContext) {
// Use home dir because env variables should be global. I get the argument
// for having in the workspace dir. I personally feel this is more
// annoying to setup every time you create a new project when using the
// same API keys, but I can do whatever option you want, not hard to
// implement
const homedir = require("os").homedir(); // This is cross-platform: https://stackoverflow.com/a/9081436
const defaultEnvPath = path.join(homedir, ".env");

const workspacePath = vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: null;

const envPath = await vscode.window.showInputBox({
prompt: "Enter the path of your .env file",
value: defaultEnvPath,
validateInput: (input) => validateEnvPath(input, workspacePath),
});

if (!envPath) {
vscode.window.showInformationMessage(
"Environment variable setup cancelled"
);
return;
}

const envTemplatePath = vscode.Uri.joinPath(
context.extensionUri,
"static",
"env_template.env"
);

if (fs.existsSync(envPath)) {
const helperText = (
await vscode.workspace.fs.readFile(envTemplatePath)
).toString();

// TODO: Check if we already appended the template text to existing .env
// file before. If we did, don't do it again
fs.appendFile(envPath, "\n\n" + helperText, function (err) {
if (err) {
throw err;
}
console.log(
`Added .env template text from ${envTemplatePath.fsPath} to ${envPath}`
);
});
} else {
// Create the .env file from the sample
try {
await vscode.workspace.fs.copy(
envTemplatePath,
vscode.Uri.file(envPath),
{ overwrite: false }
);
} catch (err) {
vscode.window.showErrorMessage(
`Error creating new file ${envTemplatePath}: ${err}`
);
}
}

// Open the env file that was either was created or already existed
const doc = await vscode.workspace.openTextDocument(envPath);
if (doc) {
vscode.window.showTextDocument(doc, {
preview: false,
// Tried using vscode.ViewColumn.Active but that overrides existing
// walkthrough window
viewColumn: vscode.ViewColumn.Beside,
});
vscode.window.showInformationMessage(
"Please define your environment variables."
);
}
}

function validateEnvPath(
inputPath: string,
workspacePath: string | null
): string | null {
if (!inputPath) {
return "File path is required";
} else if (path.basename(inputPath) !== ".env") {
return 'Filename must be ".env"';
} else if (workspacePath !== null) {
// Loadenv from Python checks each folder from the file/program where it's
// invoked for the presence of an `.env` file. Therefore, the `.env` file
// must be saved either at the top-level directory of the workspace
// directory, or one of it's parent directories. This will ensure that if
// two AIConfig files are contained in separate paths within the workspace
// they'll still be able to access the same `.env` file.

// Note: If the `inputPath` directory is equal to the `workspacePath`,
// `relativePathFromEnvToWorkspace` will be an empty string
const relativePathFromEnvToWorkspace = path.relative(
path.dirname(inputPath),
workspacePath
);
if (relativePathFromEnvToWorkspace.startsWith("..")) {
return `File path must either be contained within the VS Code workspace directory ('${workspacePath}') or within a one of it's parent folders`;
}
}
return null;
}

async function shareAIConfig(
context: vscode.ExtensionContext,
aiconfigEditorManager: AIConfigEditorManager
Expand Down
112 changes: 112 additions & 0 deletions vscode-extension/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,115 @@ export function showGuideForPythonInstallation(message: string): void {
}
});
}

/**
* Creates an .env file (or opens it if it already exists) to define environment variables
* 1) If .env file exists:
* a) Add helper lines on how to add common API keys (if not currently present)
* 2) If .env file doesn't exist
* b) Add template file containing helper lines from 1a above
*/
export async function setupEnvironmentVariables(
context: vscode.ExtensionContext
) {
const homedir = require("os").homedir(); // This is cross-platform: https://stackoverflow.com/a/9081436
const defaultEnvPath = path.join(homedir, ".env");

// TODO: If there are multiple workspace folders, use common lowest
// ancestor as workspacePath: https://github.com/lastmile-ai/aiconfig/issues/1299
const workspacePath = vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: null;

const envPath = await vscode.window.showInputBox({
prompt: "Enter the path of your .env file",
value: defaultEnvPath,
validateInput: (input) => validateEnvPath(input, workspacePath),
});

if (!envPath) {
vscode.window.showInformationMessage(
"Environment variable setup cancelled"
);
return;
}

const envTemplatePath = vscode.Uri.joinPath(
context.extensionUri,
"static",
"env_template.env"
);

if (fs.existsSync(envPath)) {
const helperText = (
await vscode.workspace.fs.readFile(envTemplatePath)
).toString();

// TODO: Check if we already appended the template text to existing .env
// file before. If we did, don't do it again
fs.appendFile(envPath, "\n\n" + helperText, function (err) {
if (err) {
throw err;
}
console.log(
`Added .env template text from ${envTemplatePath.fsPath} to ${envPath}`
);
});
} else {
// Create the .env file from the sample
try {
await vscode.workspace.fs.copy(
envTemplatePath,
vscode.Uri.file(envPath),
{ overwrite: false }
);
} catch (err) {
vscode.window.showErrorMessage(
`Error creating new file ${envTemplatePath}: ${err}`
);
}
}

// Open the env file that was either was created or already existed
const doc = await vscode.workspace.openTextDocument(envPath);
if (doc) {
vscode.window.showTextDocument(doc, {
preview: false,
// Tried using vscode.ViewColumn.Active but that overrides existing
// walkthrough window
viewColumn: vscode.ViewColumn.Beside,
});
vscode.window.showInformationMessage(
"Please define your environment variables."
);
}
}

function validateEnvPath(
inputPath: string,
workspacePath: string | null
): string | null {
if (!inputPath) {
return "File path is required";
} else if (path.basename(inputPath) !== ".env") {
return 'Filename must be ".env"';
} else if (workspacePath !== null) {
// loadenv() from Python checks each folder from the file/program where
// it's invoked for the presence of an `.env` file. Therefore, the `.env
// file must be saved either at the top-level directory of the workspace
// directory, or one of it's parent directories. This will ensure that if
// two AIConfig files are contained in separate paths within the workspace
// they'll still be able to access the same `.env` file.

// Note: If the `inputPath` directory is equal to the `workspacePath`,
// `relativePathFromEnvToWorkspace` will be an empty string
const relativePathFromEnvToWorkspace = path.relative(
path.dirname(inputPath),
workspacePath
);
if (relativePathFromEnvToWorkspace.startsWith("..")) {
return `File path must either be contained within the VS Code workspace directory ('${workspacePath}') or within a one of it's parent folders`;
}
}
return null;
}

0 comments on commit e177917

Please sign in to comment.