Skip to content

Commit 9abec0b

Browse files
committed
Rearrange lambda quick editing to avoid download on activation
1 parent 0ecc958 commit 9abec0b

File tree

7 files changed

+221
-124
lines changed

7 files changed

+221
-124
lines changed

packages/core/src/lambda/activation.ts

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ExtContext } from '../shared/extensions'
1717
import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda'
1818
import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend'
1919
import { Commands } from '../shared/vscode/commands2'
20-
import { DefaultLambdaClient, getFunctionWithCredentials } from '../shared/clients/lambdaClient'
20+
import { DefaultLambdaClient } from '../shared/clients/lambdaClient'
2121
import { copyLambdaUrl } from './commands/copyLambdaUrl'
2222
import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode'
2323
import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider'
@@ -29,47 +29,125 @@ import { ToolkitError, isError } from '../shared/errors'
2929
import { LogStreamFilterResponse } from '../awsService/cloudWatchLogs/wizard/liveTailLogStreamSubmenu'
3030
import { tempDirPath } from '../shared/filesystemUtilities'
3131
import fs from '../shared/fs/fs'
32-
import { deployFromTemp, editLambda, getReadme, openLambdaFolderForEdit } from './commands/editLambda'
33-
import { getTempLocation } from './utils'
32+
import {
33+
confirmOutdatedChanges,
34+
deleteFilesInFolder,
35+
deployFromTemp,
36+
getReadme,
37+
openLambdaFolderForEdit,
38+
watchForUpdates,
39+
} from './commands/editLambda'
40+
import { compareCodeSha, getFunctionInfo, getTempLocation, setFunctionInfo } from './utils'
3441
import { registerLambdaUriHandler } from './uriHandlers'
42+
import globals from '../shared/extensionGlobals'
3543

3644
const localize = nls.loadMessageBundle()
3745

38-
/**
39-
* Activates Lambda components.
40-
*/
41-
export async function activate(context: ExtContext): Promise<void> {
42-
try {
43-
if (vscode.workspace.workspaceFolders) {
44-
for (const workspaceFolder of vscode.workspace.workspaceFolders) {
45-
// Making the comparison case insensitive because Windows can have `C\` or `c\`
46-
const workspacePath = workspaceFolder.uri.fsPath.toLowerCase()
47-
const tempPath = path.join(tempDirPath, 'lambda').toLowerCase()
48-
if (workspacePath.startsWith(tempPath)) {
49-
const name = path.basename(workspaceFolder.uri.fsPath)
50-
const region = path.basename(path.dirname(workspaceFolder.uri.fsPath))
51-
const getFunctionOutput = await getFunctionWithCredentials(region, name)
52-
const configuration = getFunctionOutput.Configuration
53-
await editLambda(
54-
{
55-
name,
56-
region,
57-
// Configuration as any due to the difference in types between sdkV2 and sdkV3
58-
configuration: configuration as any,
59-
},
60-
true
46+
async function openReadme() {
47+
const readmeUri = vscode.Uri.file(await getReadme())
48+
// We only want to do it if there's not a readme already
49+
const isPreviewOpen = vscode.window.tabGroups.all.some((group) =>
50+
group.tabs.some((tab) => tab.label.includes('README'))
51+
)
52+
if (!isPreviewOpen) {
53+
await vscode.commands.executeCommand('markdown.showPreviewToSide', readmeUri)
54+
}
55+
}
56+
57+
async function quickEditActivation() {
58+
if (vscode.workspace.workspaceFolders) {
59+
for (const workspaceFolder of vscode.workspace.workspaceFolders) {
60+
// Making the comparison case insensitive because Windows can have `C\` or `c\`
61+
const workspacePath = workspaceFolder.uri.fsPath.toLowerCase()
62+
const tempPath = path.join(tempDirPath, 'lambda').toLowerCase()
63+
if (workspacePath.includes(tempPath)) {
64+
const name = path.basename(workspaceFolder.uri.fsPath)
65+
const region = path.basename(path.dirname(workspaceFolder.uri.fsPath))
66+
67+
const lambda = { name, region, configuration: undefined }
68+
69+
watchForUpdates(lambda, vscode.Uri.file(workspacePath))
70+
71+
await openReadme()
72+
73+
// Open handler function
74+
try {
75+
const handler = await getFunctionInfo(lambda, 'handlerFile')
76+
const lambdaLocation = path.join(workspacePath, handler)
77+
await openLambdaFile(lambdaLocation, vscode.ViewColumn.One)
78+
} catch (e) {
79+
void vscode.window.showWarningMessage(
80+
localize('AWS.lambda.openFile.failure', `Failed to determine handler location: ${e}`)
6181
)
82+
}
6283

63-
const readmeUri = vscode.Uri.file(await getReadme())
64-
await vscode.commands.executeCommand('markdown.showPreviewToSide', readmeUri)
84+
// Check if there are changes that need overwritten
85+
try {
86+
// Checking if there are changes that need to be overwritten
87+
const prompt = localize(
88+
'AWS.lambda.download.confirmOutdatedSync',
89+
'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?'
90+
)
91+
92+
// Adding delay to give the authentication time to catch up
93+
await new Promise((resolve) => globals.clock.setTimeout(resolve, 1000))
94+
95+
const overwriteChanges = !(await compareCodeSha(lambda))
96+
? await confirmOutdatedChanges(prompt)
97+
: false
98+
if (overwriteChanges) {
99+
// Close all open tabs from this workspace
100+
const workspaceUri = vscode.Uri.file(workspacePath)
101+
for (const tabGroup of vscode.window.tabGroups.all) {
102+
const tabsToClose = tabGroup.tabs.filter(
103+
(tab) =>
104+
tab.input instanceof vscode.TabInputText &&
105+
tab.input.uri.fsPath.startsWith(workspaceUri.fsPath)
106+
)
107+
if (tabsToClose.length > 0) {
108+
await vscode.window.tabGroups.close(tabsToClose)
109+
}
110+
}
111+
112+
// Delete all files in the directory
113+
await deleteFilesInFolder(workspacePath)
114+
115+
// Remove workspace folder
116+
const workspaceIndex = vscode.workspace.workspaceFolders?.findIndex(
117+
(folder) => folder.uri.fsPath.toLowerCase() === workspacePath
118+
)
119+
if (workspaceIndex !== undefined && workspaceIndex >= 0) {
120+
vscode.workspace.updateWorkspaceFolders(workspaceIndex, 1)
121+
}
122+
123+
await setFunctionInfo(lambda, { undeployed: false })
124+
125+
// Show message to user about next steps
126+
void vscode.window.showInformationMessage(
127+
localize(
128+
'AWS.lambda.refresh.complete',
129+
'Local workspace cleared. Navigate to the Toolkit explorer to get fresh code from the cloud.'
130+
)
131+
)
132+
}
133+
} catch (e) {
134+
void vscode.window.showWarningMessage(
135+
localize(
136+
'AWS.lambda.pull.failure',
137+
`Failed to pull latest changes from the cloud, you can still edit locally: ${e}`
138+
)
139+
)
65140
}
66141
}
67142
}
68-
} catch (e) {
69-
void vscode.window.showWarningMessage(
70-
localize('AWS.lambda.open.failure', `Unable to edit Lambda Function locally: ${e}`)
71-
)
72143
}
144+
}
145+
146+
/**
147+
* Activates Lambda components.
148+
*/
149+
export async function activate(context: ExtContext): Promise<void> {
150+
await quickEditActivation()
73151

74152
context.extensionContext.subscriptions.push(
75153
Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode | TreeNode) => {

packages/core/src/lambda/commands/downloadLambda.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ async function downloadAndUnzipLambda(
194194
}
195195
}
196196

197-
export async function openLambdaFile(lambdaLocation: string): Promise<void> {
197+
export async function openLambdaFile(lambdaLocation: string, viewColumn?: vscode.ViewColumn): Promise<void> {
198198
if (!(await fs.exists(lambdaLocation))) {
199199
const warning = localize(
200200
'AWS.lambda.download.fileNotFound',
@@ -206,7 +206,7 @@ export async function openLambdaFile(lambdaLocation: string): Promise<void> {
206206
throw new Error()
207207
}
208208
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(lambdaLocation))
209-
await vscode.window.showTextDocument(doc)
209+
await vscode.window.showTextDocument(doc, viewColumn)
210210
}
211211

212212
async function addLaunchConfigEntry(

packages/core/src/lambda/commands/editLambda.ts

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { LambdaFunctionNodeDecorationProvider } from '../explorer/lambdaFunction
2222
import path from 'path'
2323
import { telemetry } from '../../shared/telemetry/telemetry'
2424
import { ToolkitError } from '../../shared/errors'
25+
import { getFunctionWithCredentials } from '../../shared/clients/lambdaClient'
2526

2627
const localize = nls.loadMessageBundle()
2728

@@ -84,7 +85,7 @@ export async function promptForSync(lambda: LambdaFunction, projectUri: vscode.U
8485
}
8586
}
8687

87-
async function confirmOutdatedChanges(prompt: string): Promise<boolean> {
88+
export async function confirmOutdatedChanges(prompt: string): Promise<boolean> {
8889
return await showConfirmationMessage({
8990
prompt,
9091
confirm: localize('AWS.lambda.upload.overwrite', 'Overwrite'),
@@ -128,28 +129,62 @@ export async function deployFromTemp(lambda: LambdaFunction, projectUri: vscode.
128129
})
129130
}
130131

132+
export async function deleteFilesInFolder(location: string) {
133+
const entries = await fs.readdir(location)
134+
await Promise.all(
135+
entries.map((entry) => fs.delete(path.join(location, entry[0]), { recursive: true, force: true }))
136+
)
137+
}
138+
131139
export async function editLambdaCommand(functionNode: LambdaFunctionNode) {
132140
const region = functionNode.regionCode
133141
const functionName = functionNode.configuration.FunctionName!
134-
return await editLambda({ name: functionName, region, configuration: functionNode.configuration })
142+
return await editLambda({ name: functionName, region, configuration: functionNode.configuration }, 'explorer')
135143
}
136144

137-
export async function editLambda(lambda: LambdaFunction, onActivation?: boolean) {
145+
export async function overwriteChangesForEdit(lambda: LambdaFunction, downloadLocation: string) {
146+
try {
147+
// Clear directory contents instead of deleting to avoid Windows EBUSY errors
148+
if (await fs.existsDir(downloadLocation)) {
149+
await deleteFilesInFolder(downloadLocation)
150+
} else {
151+
await fs.mkdir(downloadLocation)
152+
}
153+
154+
await downloadLambdaInLocation(lambda, 'local', downloadLocation)
155+
156+
// Watching for updates, then setting info, then removing the badges must be done in this order
157+
// This is because the files creating can throw the watcher, which sometimes leads to changes being marked as undeployed
158+
watchForUpdates(lambda, vscode.Uri.file(downloadLocation))
159+
160+
await setFunctionInfo(lambda, {
161+
lastDeployed: globals.clock.Date.now(),
162+
undeployed: false,
163+
sha: lambda.configuration!.CodeSha256,
164+
handlerFile: getLambdaDetails(lambda.configuration!).fileName,
165+
})
166+
await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge(
167+
vscode.Uri.file(downloadLocation),
168+
vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` })
169+
)
170+
} catch {
171+
throw new ToolkitError('Failed to download Lambda function', { code: 'failedDownload' })
172+
}
173+
}
174+
175+
export async function editLambda(lambda: LambdaFunction, source?: 'workspace' | 'explorer') {
138176
return await telemetry.lambda_quickEditFunction.run(async () => {
139-
telemetry.record({ source: onActivation ? 'workspace' : 'explorer' })
177+
telemetry.record({ source })
140178
const downloadLocation = getTempLocation(lambda.name, lambda.region)
141-
const downloadLocationName = vscode.workspace.asRelativePath(downloadLocation, true)
142179

143180
// We don't want to do anything if the folder already exists as a workspace folder, it means it's already being edited
144-
if (
145-
vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === downloadLocation && !onActivation)
146-
) {
181+
if (vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === downloadLocation)) {
147182
return downloadLocation
148183
}
149184

150185
const prompt = localize(
151186
'AWS.lambda.download.confirmOutdatedSync',
152-
'There are changes to your Function in the cloud since you last edited locally, do you want to overwrite your local changes?'
187+
'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?'
153188
)
154189

155190
// We want to overwrite changes in the following cases:
@@ -169,37 +204,9 @@ export async function editLambda(lambda: LambdaFunction, onActivation?: boolean)
169204
(!(await compareCodeSha(lambda)) ? await confirmOutdatedChanges(prompt) : false)
170205

171206
if (overwriteChanges) {
172-
try {
173-
// Clear directory contents instead of deleting to avoid Windows EBUSY errors
174-
if (await fs.existsDir(downloadLocation)) {
175-
const entries = await fs.readdir(downloadLocation)
176-
await Promise.all(
177-
entries.map((entry) =>
178-
fs.delete(path.join(downloadLocation, entry[0]), { recursive: true, force: true })
179-
)
180-
)
181-
} else {
182-
await fs.mkdir(downloadLocation)
183-
}
184-
185-
await downloadLambdaInLocation(lambda, downloadLocationName, downloadLocation)
186-
187-
// Watching for updates, then setting info, then removing the badges must be done in this order
188-
// This is because the files creating can throw the watcher, which sometimes leads to changes being marked as undeployed
189-
watchForUpdates(lambda, vscode.Uri.file(downloadLocation))
190-
await setFunctionInfo(lambda, {
191-
lastDeployed: globals.clock.Date.now(),
192-
undeployed: false,
193-
sha: lambda.configuration!.CodeSha256,
194-
})
195-
await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge(
196-
vscode.Uri.file(downloadLocation),
197-
vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` })
198-
)
199-
} catch {
200-
throw new ToolkitError('Failed to download Lambda function', { code: 'failedDownload' })
201-
}
202-
} else {
207+
await overwriteChangesForEdit(lambda, downloadLocation)
208+
} else if (source === 'explorer') {
209+
// If the source is the explorer, we want to open, otherwise we just wait to open in the workspace
203210
const lambdaLocation = path.join(downloadLocation, getLambdaDetails(lambda.configuration!).fileName)
204211
await openLambdaFile(lambdaLocation)
205212
watchForUpdates(lambda, vscode.Uri.file(downloadLocation))
@@ -212,22 +219,24 @@ export async function editLambda(lambda: LambdaFunction, onActivation?: boolean)
212219
export async function openLambdaFolderForEdit(name: string, region: string) {
213220
const downloadLocation = getTempLocation(name, region)
214221

215-
if (
216-
vscode.workspace.workspaceFolders?.some((workspaceFolder) =>
217-
workspaceFolder.uri.fsPath.toLowerCase().startsWith(downloadLocation.toLowerCase())
218-
)
219-
) {
220-
// If the folder already exists in the workspace, show that folder
221-
await vscode.commands.executeCommand('workbench.action.focusSideBar')
222-
await vscode.commands.executeCommand('workbench.view.explorer')
223-
} else {
224-
await fs.mkdir(downloadLocation)
225-
226-
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(downloadLocation), {
227-
newWindow: true,
228-
noRecentEntry: true,
229-
})
230-
}
222+
// Do all authentication work before opening workspace to avoid race condition
223+
const getFunctionOutput = await getFunctionWithCredentials(region, name)
224+
const configuration = getFunctionOutput.Configuration
225+
226+
// Download and set up Lambda code before opening workspace
227+
await editLambda(
228+
{
229+
name,
230+
region,
231+
configuration: configuration as any,
232+
},
233+
'workspace'
234+
)
235+
236+
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(downloadLocation), {
237+
newWindow: true,
238+
noRecentEntry: true,
239+
})
231240
}
232241

233242
export async function getReadme(): Promise<string> {

packages/core/src/lambda/uriHandlers.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { SearchParams } from '../shared/vscode/uriHandler'
1010
import { openLambdaFolderForEdit } from './commands/editLambda'
1111
import { showConfirmationMessage } from '../shared/utilities/messages'
1212
import globals from '../shared/extensionGlobals'
13-
import { getFunctionWithCredentials } from '../shared/clients/lambdaClient'
1413
import { telemetry } from '../shared/telemetry/telemetry'
1514
import { ToolkitError } from '../shared/errors'
1615

@@ -20,9 +19,6 @@ export function registerLambdaUriHandler() {
2019
async function openFunctionHandler(params: ReturnType<typeof parseOpenParams>) {
2120
await telemetry.lambda_uriHandler.run(async () => {
2221
try {
23-
// We just want to be able to get the function - if it fails we abort and throw the error
24-
await getFunctionWithCredentials(params.region, params.functionName)
25-
2622
if (params.isCfn === 'true') {
2723
const response = await showConfirmationMessage({
2824
prompt: localize(

0 commit comments

Comments
 (0)