diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 2a3d15220c..0809a6d146 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -149,6 +149,11 @@ "command": "sqlDatabaseProjects.openContainingFolder", "title": "%sqlDatabaseProjects.openContainingFolder%", "category": "%sqlDatabaseProjects.displayName%" + }, + { + "command": "sqlDatabaseProjects.editProjectFile", + "title": "%sqlDatabaseProjects.editProjectFile%", + "category": "%sqlDatabaseProjects.displayName%" } ], "menus": { @@ -226,6 +231,10 @@ "command": "sqlDatabaseProjects.openContainingFolder", "when": "false" }, + { + "command": "sqlDatabaseProjects.editProjectFile", + "when": "false" + }, { "command": "sqlDatabaseProjects.exclude", "when": "false" @@ -302,6 +311,11 @@ "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", "group": "9_dbProjectsLast@2" }, + { + "command": "sqlDatabaseProjects.editProjectFile", + "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", + "group": "9_dbProjectsLast@7" + }, { "command": "sqlDatabaseProjects.openContainingFolder", "when": "view == sqlDatabaseProjectsView && viewItem == databaseProject.itemType.project", diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 6e7c9092a0..52a343d96f 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -26,6 +26,7 @@ "sqlDatabaseProjects.addDatabaseReference": "Add Database Reference", "sqlDatabaseProjects.openContainingFolder": "Open Containing Folder", + "sqlDatabaseProjects.editProjectFile": "Edit .sqlproj File", "sqlDatabaseProjects.Settings": "Database Projects", "sqlDatabaseProjects.netCoreInstallLocation": "Full path to .Net Core SDK on the machine.", diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 9e11c2585d..0b34573e97 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -61,6 +61,7 @@ export const newDefaultProjectSaveLocation = localize('newDefaultProjectSaveLoca export const invalidDefaultProjectSaveLocation = localize('invalidDefaultProjectSaveLocation', "Default location to save new database projects is invalid. Would you like to update it?"); export const openWorkspaceSettings = localize('openWorkspaceSettings', "Yes, open Settings"); export const doNotPromptAgain = localize('doNotPromptAgain', "Don't ask again"); +export const reloadProject = localize('reloadProject', "Would you like to reload your database project?"); export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); } export function deleteConfirmation(toDelete: string) { return localize('deleteConfirmation', "Are you sure you want to delete {0}?", toDelete); } export function deleteConfirmationContents(toDelete: string) { return localize('deleteConfirmationContents', "Are you sure you want to delete {0} and all of its contents?", toDelete); } @@ -139,6 +140,7 @@ export const databaseReferenceAlreadyExists = localize('databaseReferenceAlready export const ousiderFolderPath = localize('outsideFolderPath', "Items with absolute path outside project folder are not supported. Please make sure the paths in the project file are relative to project folder."); export const parentTreeItemUnknown = localize('parentTreeItemUnknown', "Cannot access parent of provided tree item"); export const prePostDeployCount = localize('prePostDeployCount', "To successfully build, update the project to have one pre-deployment script and/or one post-deployment script"); +export const invalidProjectReload = localize('invalidProjectReload', "Cannot access provided database project. Only valid, open database projects can be reloaded."); export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); } export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); } export function noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); } diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 7dabed1e1d..8a6391c7da 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -73,6 +73,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: BaseProjectTreeItem) => { await this.projectsController.addDatabaseReference(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.openContainingFolder(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: BaseProjectTreeItem) => { await this.projectsController.editProjectFile(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 5d0a23cc50..01d921f293 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -431,6 +431,45 @@ export class ProjectsController { await vscode.commands.executeCommand(constants.revealFileInOsCommand, vscode.Uri.file(project.projectFilePath)); } + /** + * Opens the .sqlproj file for the given project. Upon update of file, prompts user to + * reload their project. + * @param context a treeItem in a project's hierarchy, to be used to obtain a Project + */ + public async editProjectFile(context: BaseProjectTreeItem): Promise { + const project = this.getProjectFromContext(context); + + try { + await vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(project.projectFilePath)); + const projFileWatcher: vscode.FileSystemWatcher = vscode.workspace.createFileSystemWatcher(project.projectFilePath); + + projFileWatcher.onDidChange(async (projectFileUri: vscode.Uri) => { + const result = await vscode.window.showInformationMessage(constants.reloadProject, constants.yesString, constants.noString); + + if (result === constants.yesString) { + this.reloadProject(projectFileUri); + } + }); + } catch (err) { + vscode.window.showErrorMessage(utils.getErrorMessage(err)); + } + } + + /** + * Reloads the given project. Throws an error if given project is not a valid open project. + * @param projectFileUri the uri of the project to be reloaded + */ + public async reloadProject(projectFileUri: vscode.Uri) { + const project = this.projects.find((e) => e.projectFilePath === projectFileUri.fsPath); + if (project) { + // won't open any newly referenced projects, but otherwise matches the behavior of reopening the project + await project.readProjFile(); + this.refreshProjectsTree(); + } else { + throw new Error(constants.invalidProjectReload); + } + } + /** * Adds a database reference to the project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index b77180748f..24d8b7358a 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -56,6 +56,8 @@ export class Project { * Reads the project setting and contents from the file */ public async readProjFile() { + this.resetProject(); + const projFileText = await fs.readFile(this.projectFilePath); this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); @@ -147,6 +149,17 @@ export class Project { } } + private resetProject() { + this.files = []; + this.importedTargets = []; + this.databaseReferences = []; + this.sqlCmdVariables = {}; + this.preDeployScripts = []; + this.postDeployScripts = []; + this.noneDeployScripts = []; + this.projFileXmlDoc = undefined; + } + public async updateProjectForRoundTrip() { await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup'); await this.updateImportToSupportRoundTrip(); diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts index 5630234200..77c73c24c4 100644 --- a/extensions/sql-database-projects/src/test/baselines/baselines.ts +++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts @@ -8,6 +8,7 @@ import { promises as fs } from 'fs'; // Project baselines export let newProjectFileBaseline: string; +export let newProjectFileWithScriptBaseline: string; export let openProjectFileBaseline: string; export let openDataSourcesBaseline: string; export let SSDTProjectFileBaseline: string; @@ -26,6 +27,7 @@ const baselineFolderPath = __dirname; export async function loadBaselines() { newProjectFileBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectBaseline.xml'); + newProjectFileWithScriptBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectWithScriptBaseline.xml'); openProjectFileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectBaseline.xml'); openDataSourcesBaseline = await loadBaseline(baselineFolderPath, 'openDataSourcesBaseline.json'); SSDTProjectFileBaseline = await loadBaseline(baselineFolderPath, 'SSDTProjectBaseline.xml'); diff --git a/extensions/sql-database-projects/src/test/baselines/newSqlProjectWithScriptBaseline.xml b/extensions/sql-database-projects/src/test/baselines/newSqlProjectWithScriptBaseline.xml new file mode 100644 index 0000000000..5055f37003 --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/newSqlProjectWithScriptBaseline.xml @@ -0,0 +1,70 @@ + + + + Debug + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider + Database + + + TestProjectName + TestProjectName + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index a04dff9d89..db998bf364 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -246,6 +246,26 @@ describe('ProjectsController', function (): void { should(await exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk'); }); + it('Should reload correctly after changing sqlproj file', async function (): Promise { + // create project + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, folderPath); + const treeProvider = new SqlDatabaseProjectTreeViewProvider(); + const projController = new ProjectsController(treeProvider); + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); + + // change the sql project file + await fs.writeFile(sqlProjPath, baselines.newProjectFileWithScriptBaseline); + should(project.files.length).equal(0); + + // call reload project + await projController.reloadProject(vscode.Uri.file(project.projectFilePath)); + should(project.files.length).equal(1); + + // check that the new project is in the tree + should(treeProvider.getChildren()[0].children.find(c => c.friendlyName === 'Script1.sql')).not.equal(undefined); + }); + it('Should be able to add pre deploy and post deploy script', async function (): Promise { const preDeployScriptName = 'PreDeployScript1.sql'; const postDeployScriptName = 'PostDeployScript1.sql';