Skip to content

Commit

Permalink
feat(appcomposer): load and save from VS Code buffer
Browse files Browse the repository at this point in the history
## Problem
- Application Composer works directly off of the workspace files.
Changes in VS Code will only be detected by App Composer when the user
saves, and changes in Composer will immediately be saved to disk.
This does not follow the standard VS Code pattern.

## Solution
- This updates Application Composer to read from and write to VS Code
text documents when possible, ensuring that Composer is synced with
the latest user changes and giving the user more control over what
changes are saved.
  • Loading branch information
Jacob Largent committed Feb 28, 2024
1 parent 82b6a9b commit 0841896
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 38 deletions.
1 change: 1 addition & 0 deletions packages/toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4352,6 +4352,7 @@
"bytes": "^3.1.2",
"cross-fetch": "^4.0.0",
"cross-spawn": "^7.0.3",
"fast-deep-equal": "^3.1.3",
"fast-json-patch": "^3.1.1",
"fs-extra": "^10.0.1",
"glob": "^10.3.10",
Expand Down
9 changes: 9 additions & 0 deletions packages/toolkit/src/applicationcomposer/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

export const bufferTimeMs = 1000

export const localhost = 'http://127.0.0.1:3000'
export const cdn = 'https://d29yvv2j7swt8n.cloudfront.net'
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,47 @@ import {
} from '../types'
import vscode from 'vscode'

import { templateShouldBeUpdated } from '../utils/templateShouldBeUpdated'
import { bufferTimeMs } from '../constants'

let timeSinceLastChange = 0

function bufferFileChange(event: vscode.TextDocumentChangeEvent, context: WebviewContext) {
timeSinceLastChange = Date.now()
setTimeout(async () => {
if (Date.now() - timeSinceLastChange < bufferTimeMs) {
return
}

const fileContents = event.document.getText()
const filePath = context.defaultTemplatePath
const fileName = context.defaultTemplateName

if (await templateShouldBeUpdated(context.fileWatches[filePath].fileContents, fileContents)) {
context.fileWatches[filePath] = { fileContents: fileContents }
const fileChangedMessage: FileChangedMessage = {
messageType: MessageType.BROADCAST,
command: Command.FILE_CHANGED,
fileName: fileName,
fileContents: fileContents,
}
await context.panel.webview.postMessage(fileChangedMessage)
}
}, bufferTimeMs)
}

export async function addFileWatchMessageHandler(request: AddFileWatchRequestMessage, context: WebviewContext) {
let addFileWatchResponseMessage: AddFileWatchResponseMessage
try {
// we only file watch on default template file now
if (context.defaultTemplateName !== request.fileName) {
throw new Error('file watching is only allowed on default template file')
}
const filePath = context.defaultTemplatePath
const fileName = context.defaultTemplateName
const fileWatch = vscode.workspace.createFileSystemWatcher(filePath)
context.disposables.push(fileWatch)

fileWatch.onDidChange(async () => {
const fileContents = (await vscode.workspace.fs.readFile(vscode.Uri.file(filePath))).toString()
if (fileContents !== context.fileWatches[filePath].fileContents) {
const fileChangedMessage: FileChangedMessage = {
messageType: MessageType.BROADCAST,
command: Command.FILE_CHANGED,
fileName: fileName,
fileContents: fileContents,
}

await context.panel.webview.postMessage(fileChangedMessage)
context.fileWatches[filePath] = { fileContents: fileContents }
vscode.workspace.onDidChangeTextDocument(async event => {
if (event.document.fileName !== context.textDocument.fileName || event.contentChanges.length === 0) {
return
}
bufferFileChange(event, context)
})

addFileWatchResponseMessage = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ export async function loadFileMessageHandler(request: LoadFileRequestMessage, co
switch (request.fileName) {
case '': {
// load default template file when 'fileName' is empty
const initFileContents = (
await vscode.workspace.fs.readFile(vscode.Uri.file(context.defaultTemplatePath))
).toString()
const initFileContents = context.textDocument.getText()
if (initFileContents === undefined) {
throw new Error(`Cannot read file contents from ${context.defaultTemplatePath}`)
}
Expand All @@ -32,7 +30,8 @@ export async function loadFileMessageHandler(request: LoadFileRequestMessage, co
}
default: {
const filePath = path.join(context.workSpacePath, request.fileName)
const fileContents = (await vscode.workspace.fs.readFile(vscode.Uri.file(filePath))).toString()
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath))
const fileContents = document.getText()
loadFileResponseMessage = {
messageType: MessageType.RESPONSE,
command: Command.LOAD_FILE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,41 @@
*/

import vscode from 'vscode'
import { SaveFileRequestMessage, SaveFileResponseMessage, WebviewContext, Command, MessageType } from '../types'
import { Command, MessageType, SaveFileRequestMessage, SaveFileResponseMessage, WebviewContext } from '../types'
import path from 'path'
import { fsCommon } from '../../srcShared/fs'
import { templateShouldBeUpdated } from '../utils/templateShouldBeUpdated'

export async function saveFileMessageHandler(request: SaveFileRequestMessage, context: WebviewContext) {
async function updateTextDocument(existingTemplate: string, fileUri: vscode.Uri) {
if (await templateShouldBeUpdated(existingTemplate, request.fileContents)) {
const edit = new vscode.WorkspaceEdit()
edit.replace(fileUri, new vscode.Range(0, 0, context.textDocument.lineCount, 0), request.fileContents)
await vscode.workspace.applyEdit(edit)
}
}

let saveFileResponseMessage: SaveFileResponseMessage
// If filePath is empty, save contents in default template file
const filePath =
request.filePath === '' ? context.defaultTemplatePath : path.join(context.workSpacePath, request.filePath)

try {
if (!context.textDocument.isDirty) {
const contents = Buffer.from(request.fileContents, 'utf8')
context.fileWatches[filePath] = { fileContents: request.fileContents }
const uri = vscode.Uri.file(filePath)
await vscode.workspace.fs.writeFile(uri, contents)
saveFileResponseMessage = {
messageType: MessageType.RESPONSE,
command: Command.SAVE_FILE,
eventId: request.eventId,
filePath: filePath,
isSuccess: true,
}
context.fileWatches[filePath] = { fileContents: request.fileContents }
const fileUri = vscode.Uri.file(filePath)
if (await fsCommon.existsFile(fileUri)) {
const textDoc = vscode.workspace.textDocuments.find(it => it.uri.path === fileUri.path)
const existingTemplate = textDoc?.getText() ?? (await fsCommon.readFileAsString(fileUri))
await updateTextDocument(existingTemplate, fileUri)
} else {
// TODO: If the template file is dirty, do we pop out a warning window?
throw new Error(`Cannot save latest contents in ${path.basename(request.filePath)}`)
await fsCommon.writeFile(fileUri, request.fileContents)
}
saveFileResponseMessage = {
messageType: MessageType.RESPONSE,
command: Command.SAVE_FILE,
eventId: request.eventId,
filePath: filePath,
isSuccess: true,
}
} catch (e) {
saveFileResponseMessage = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { loadByContents } from '../../shared/cloudformation/cloudformation'
import equal from 'fast-deep-equal/es6'

/**
* Checks whether a template needs to be updated. This is eiter when the template is out of sync, or
* when at least one of the templates cannot be parsed. Comments and whitespace should not result
* in a template update.
*/
export async function templateShouldBeUpdated(oldTemplate: string, newTemplate: string) {
try {
const oldParsedTemplate = await loadByContents(oldTemplate, false)
const newParsedTemplate = await loadByContents(newTemplate, false)
return !equal(oldParsedTemplate, newParsedTemplate)
} catch (e) {
return true
}
}
3 changes: 1 addition & 2 deletions packages/toolkit/src/applicationcomposer/webviewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import * as nls from 'vscode-nls'
import request from '../common/request'
import { ApplicationComposer } from './composerWebview'
import { getLogger } from '../shared/logger'
import { cdn, localhost } from './constants'

const localize = nls.loadMessageBundle()

// TODO turn this into a flag to make local dev easier
// Change this to true for local dev
const isLocalDev = false
const localhost = 'http://127.0.0.1:3000'
const cdn = 'https://ide-toolkits.app-composer.aws.dev'

const enabledFeatures = ['ide-only', 'anything-resource', 'sfnV2', 'starling']

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { templateShouldBeUpdated } from '../../../applicationcomposer/utils/templateShouldBeUpdated'
import assert from 'assert'

describe('templateShouldBeUpdated', async function () {
const before = `
Resources:
SomeResource:
Key: value`

it('template has meaningful changes, returns true', async function () {
const after = `
Resources:
SomeResource:
Key: value2`

const result = await templateShouldBeUpdated(before, after)
assert.strictEqual(result, true)
})

it('template cannot be parsed, returns true', async function () {
const unparseable = `
Resources:
SomeResource
Key: value`

const result = await templateShouldBeUpdated(unparseable, unparseable)
assert.strictEqual(result, true)
})

it('template is identical, returns false', async function () {
const result = await templateShouldBeUpdated(before, before)
assert.strictEqual(result, false)
})

it('template only has whitespace change, returns false', async function () {
const after = `
# Some comment
Resources:
SomeResource:
Key: value`
const result = await templateShouldBeUpdated(before, after)
assert.strictEqual(result, false)
})
})

0 comments on commit 0841896

Please sign in to comment.