Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appcomposer): load and save from VS Code buffer #4482

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Application Composer: Change file management to work with unsaved files"
}
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://ide-toolkits.app-composer.aws.dev'
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 '../util'
import { bufferTimeMs } from '../constants'

let timeSinceLastChange = 0

function bufferTextDocumentChange(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
}
bufferTextDocumentChange(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 '../util'

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, Number.MAX_SAFE_INTEGER, 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
21 changes: 21 additions & 0 deletions packages/toolkit/src/applicationcomposer/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { loadByContents } from '../shared/cloudformation/cloudformation'

/**
* 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 JSON.stringify(oldParsedTemplate) !== JSON.stringify(newParsedTemplate)
} catch (e) {
return true
}
}
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
50 changes: 50 additions & 0 deletions packages/toolkit/src/test/applicationcomposer/util.test.ts
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/util'
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)
})
})