Fix how data workspace handles untitled workspaces (#14505)

* add more workspace apis

* update dialog and check workspace scheme

* cleanup

* add comment

* update create project from db dialog

* cleanup

* update names

* add test
This commit is contained in:
Kim Santiago
2021-03-03 15:31:21 -08:00
committed by GitHub
parent 6ecacd6faa
commit 42fba14d88
12 changed files with 100 additions and 32 deletions

View File

@@ -15,7 +15,7 @@ export const AllProjectTypes = localize('AllProjectTypes', "All Project Types");
export const ProviderNotFoundForProjectTypeError = (projectType: string): string => { return localize('UnknownProjectTypeError', "No provider was found for project type with id: '{0}'", projectType); };
export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequiredMessage', "A workspace is required in order to use the project feature.");
export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…");
export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A new workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed.");
export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed.");
export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, the Extension Host will restart and if there is a workspace or folder currently open, it will be closed.");
export const WorkspaceContainsNotAddedProjects = localize('dataworkspace.workspaceContainsNotAddedProjects', "The current workspace contains one or more projects that have not been added to the workspace. Use the 'Open existing' dialog to add projects to the projects pane.");
export const LaunchOpenExisitingDialog = localize('dataworkspace.launchOpenExistingDialog', "Launch Open existing dialog");
@@ -41,7 +41,7 @@ export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceho
export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location");
export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Select location to create project");
export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace.");
export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A new workspace will be created for this project.");
export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A workspace will be created for this project.");
export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location");
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); };
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as vscode from 'vscode';
export async function directoryExist(directoryPath: string): Promise<boolean> {
const stats = await getFileStatus(directoryPath);
@@ -30,6 +31,13 @@ async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
}
}
/**
* if the current workspace is untitled, the returned URI of vscode.workspace.workspaceFile will use the `untitled` scheme
*/
export function isCurrentWorkspaceUntitled(): boolean {
return !!vscode.workspace.workspaceFile && vscode.workspace.workspaceFile.scheme.toLowerCase() === 'untitled';
}
export interface IPackageInfo {
name: string;
version: string;

View File

@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import * as path from 'path';
import * as constants from '../common/constants';
import { IconPathHelper } from '../common/iconHelper';
import { directoryExist, fileExist } from '../common/utils';
import { directoryExist, fileExist, isCurrentWorkspaceUntitled } from '../common/utils';
interface Deferred<T> {
resolve: (result: T | Promise<T>) => void;
@@ -94,12 +94,14 @@ export abstract class DialogBase {
CSSStyles: { 'margin-top': '3px', 'margin-bottom': '0px' }
}).component();
const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : '';
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.WorkspaceLocationTitle,
width: constants.DefaultInputWidth,
enabled: !vscode.workspace.workspaceFile, // want it editable if no workspace is open
value: vscode.workspace.workspaceFile?.fsPath ?? '',
title: vscode.workspace.workspaceFile?.fsPath ?? '' // hovertext for if file path is too long to be seen in textbox
enabled: !vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled(), // want it editable if no saved workspace is open
value: initialWorkspaceInputBoxValue,
title: initialWorkspaceInputBoxValue // hovertext for if file path is too long to be seen in textbox
}).component();
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
@@ -129,7 +131,7 @@ export abstract class DialogBase {
this.workspaceInputBox!.title = selectedFile;
}));
if (vscode.workspace.workspaceFile) {
if (vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled()) {
this.workspaceInputFormComponent = {
component: this.workspaceInputBox
};
@@ -154,7 +156,7 @@ export abstract class DialogBase {
* @param name
*/
protected updateWorkspaceInputbox(location: string, name: string): void {
if (!vscode.workspace.workspaceFile) {
if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) {
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
this.workspaceInputBox!.value = fileLocation;
this.workspaceInputBox!.title = fileLocation;

View File

@@ -13,6 +13,7 @@ import { IWorkspaceService } from '../common/interfaces';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import Logger from '../common/logger';
import { TelemetryReporter, TelemetryViews, calculateRelativity, TelemetryActions } from '../common/telemetry';
import { isCurrentWorkspaceUntitled } from '../common/utils';
const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects';
@@ -51,9 +52,15 @@ export class WorkspaceService implements IWorkspaceService {
// save temp project
await this._context.globalState.update(TempProject, [projectFileFsPath]);
// create a new workspace
// create workspace
const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath));
await azdata.workspace.createWorkspace(projectFolder, workspaceFile);
if (isCurrentWorkspaceUntitled()) {
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, { uri: projectFolder });
await azdata.workspace.saveAndEnterWorkspace(workspaceFile!);
} else {
await azdata.workspace.createAndEnterWorkspace(projectFolder, workspaceFile);
}
}
get isProjectProviderAvailable(): boolean {
@@ -67,10 +74,10 @@ export class WorkspaceService implements IWorkspaceService {
}
/**
* Verify that a workspace is open or that if one isn't, it's ok to create a workspace
* Verify that a workspace is open or that if one isn't, it's ok to create a workspace and restart the extension host
*/
async validateWorkspace(): Promise<boolean> {
if (!vscode.workspace.workspaceFile) {
if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) {
const result = await vscode.window.showWarningMessage(constants.CreateWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
if (result === constants.OkButtonText) {
return true;
@@ -102,7 +109,7 @@ export class WorkspaceService implements IWorkspaceService {
}
// a workspace needs to be open to add projects
if (!vscode.workspace.workspaceFile) {
if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) {
await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath, workspaceFilePath);
// this won't get hit since the extension host will get restarted, but helps with testing

View File

@@ -11,6 +11,7 @@ import * as should from 'should';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import { WorkspaceService } from '../services/workspaceService';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import { createProjectProvider } from './projectProviderRegistry.test';
@@ -246,13 +247,32 @@ suite('WorkspaceService Tests', function (): void {
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
onWorkspaceProjectsChangedStub();
});
const createWorkspaceStub = sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
const createWorkspaceStub = sinon.stub(azdata.workspace, 'createAndEnterWorkspace').resolves(undefined);
await service.addProjectsToWorkspace([
vscode.Uri.file('/test/folder/proj1.sqlproj')
]);
should.strictEqual(createWorkspaceStub.calledOnce, true, 'createWorkspace should have been called once');
should.strictEqual(createWorkspaceStub.calledOnce, true, 'createAndEnterWorkspace should have been called once');
should.strictEqual(onWorkspaceProjectsChangedStub.notCalled, true, 'the onDidWorkspaceProjectsChange event should not have been fired');
onWorkspaceProjectsChangedDisposable.dispose();
});
test('test addProjectsToWorkspace when untitled workspace is open', async () => {
stubWorkspaceFile(undefined);
const onWorkspaceProjectsChangedStub = sinon.stub();
const onWorkspaceProjectsChangedDisposable = service.onDidWorkspaceProjectsChange(() => {
onWorkspaceProjectsChangedStub();
});
const saveWorkspaceStub = sinon.stub(azdata.workspace, 'saveAndEnterWorkspace').resolves(undefined);
sinon.stub(utils, 'isCurrentWorkspaceUntitled').returns(true);
sinon.stub(vscode.workspace, 'workspaceFolders').value(['folder1']);
await service.addProjectsToWorkspace([
vscode.Uri.file('/test/folder/proj1.sqlproj')
]);
should.strictEqual(saveWorkspaceStub.calledOnce, true, 'saveAndEnterWorkspace should have been called once');
should.strictEqual(onWorkspaceProjectsChangedStub.notCalled, true, 'the onDidWorkspaceProjectsChange event should not have been fired');
onWorkspaceProjectsChangedDisposable.dispose();
});
@@ -269,7 +289,7 @@ suite('WorkspaceService Tests', function (): void {
onWorkspaceProjectsChangedStub();
});
stubGetConfigurationValue(getConfigurationStub, updateConfigurationStub);
sinon.stub(azdata.workspace, 'createWorkspace').resolves(undefined);
sinon.stub(azdata.workspace, 'createAndEnterWorkspace').resolves(undefined);
sinon.stub(vscode.workspace, 'workspaceFolders').value(['folder1']);
mockGlobalState.setup(x => x.get(TypeMoq.It.isAny())).returns(() => [processPath('folder1/proj2.sqlproj')]);

View File

@@ -244,6 +244,13 @@ export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
return extension.exports;
}
/**
* if the current workspace is untitled, the returned URI of vscode.workspace.workspaceFile will use the `untitled` scheme
*/
export function isCurrentWorkspaceUntitled(): boolean {
return !!vscode.workspace.workspaceFile && vscode.workspace.workspaceFile.scheme.toLowerCase() === 'untitled';
}
/*
* Returns the default deployment options from DacFx
*/

View File

@@ -15,7 +15,7 @@ import { cssStyles } from '../common/uiConstants';
import { ImportDataModel } from '../models/api/import';
import { Deferred } from '../common/promise';
import { getConnectionName } from './utils';
import { exists } from '../common/utils';
import { exists, isCurrentWorkspaceUntitled } from '../common/utils';
export class CreateProjectFromDatabaseDialog {
public dialog: azdata.window.Dialog;
@@ -358,11 +358,13 @@ export class CreateProjectFromDatabaseDialog {
* @param view
*/
private createWorkspaceContainerRow(view: azdata.ModelView): azdata.FlexContainer {
const initialWorkspaceInputBoxValue = !!vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled() ? vscode.workspace.workspaceFile.fsPath : '';
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties({
ariaLabel: constants.workspaceLocationTitle,
enabled: !vscode.workspace.workspaceFile, // want it editable if no workspace is open
value: vscode.workspace.workspaceFile?.fsPath ?? '',
title: vscode.workspace.workspaceFile?.fsPath ?? '', // hovertext for if file path is too long to be seen in textbox
enabled: !vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled(), // want it editable if no saved workspace is open
value: initialWorkspaceInputBoxValue,
title: initialWorkspaceInputBoxValue, // hovertext for if file path is too long to be seen in textbox
width: '100%'
}).component();
@@ -399,9 +401,8 @@ export class CreateProjectFromDatabaseDialog {
}).component();
let workspaceContainerRow;
if (vscode.workspace.workspaceFile) {
if (vscode.workspace.workspaceFile && !isCurrentWorkspaceUntitled()) {
workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, this.workspaceInputBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component();
} else {
// have browse button to help select where the workspace file should be created
const workspaceInput = view.modelBuilder.flexContainer().withItems([this.workspaceInputBox], { CSSStyles: { 'margin-right': '10px', 'margin-bottom': '10px', 'width': '100%' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
@@ -418,7 +419,7 @@ export class CreateProjectFromDatabaseDialog {
* @param name
*/
public updateWorkspaceInputbox(location: string, name: string): void {
if (!vscode.workspace.workspaceFile) {
if (!vscode.workspace.workspaceFile || isCurrentWorkspaceUntitled()) {
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
this.workspaceInputBox!.value = fileLocation;
this.workspaceInputBox!.title = fileLocation;

View File

@@ -944,13 +944,19 @@ declare module 'azdata' {
/**
* Creates and enters a workspace at the specified location
*/
export function createWorkspace(location: vscode.Uri, workspaceFile?: vscode.Uri): Promise<void>;
export function createAndEnterWorkspace(location: vscode.Uri, workspaceFile?: vscode.Uri): Promise<void>;
/**
* Enters the workspace with the provided path
* @param workspacefile
*/
export function enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
/**
* Saves and enters the workspace with the provided path
* @param workspacefile
*/
export function saveAndEnterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
}
export interface TableComponentProperties {

View File

@@ -9,18 +9,20 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@extHostNamedCustomer(SqlMainContext.MainThreadWorkspace)
export class MainThreadWorkspace extends Disposable implements MainThreadWorkspaceShape {
constructor(
extHostContext: IExtHostContext,
@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
) {
super();
}
$createWorkspace(folder: URI, workspaceFile?: URI): Promise<void> {
$createAndEnterWorkspace(folder: URI, workspaceFile?: URI): Promise<void> {
folder = URI.revive(folder);
workspaceFile = URI.revive(workspaceFile);
return this.workspaceEditingService.createAndEnterWorkspace([{ uri: folder }], workspaceFile);
@@ -30,4 +32,10 @@ export class MainThreadWorkspace extends Disposable implements MainThreadWorkspa
workspaceFile = URI.revive(workspaceFile);
return this.workspaceEditingService.enterWorkspace(workspaceFile);
}
$saveAndEnterWorkspace(workspaceFile: URI): Promise<void> {
workspaceFile = URI.revive(workspaceFile);
return this.workspaceEditingService.saveAndEnterWorkspace(workspaceFile);
}
}

View File

@@ -17,11 +17,15 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadWorkspace);
}
$createWorkspace(folder: URI, workspaceFile: URI): Promise<void> {
return this._proxy.$createWorkspace(folder, workspaceFile);
$createAndEnterWorkspace(folder: URI, workspaceFile: URI): Promise<void> {
return this._proxy.$createAndEnterWorkspace(folder, workspaceFile);
}
$enterWorkspace(workspaceFile: URI): Promise<void> {
return this._proxy.$enterWorkspace(workspaceFile);
}
$saveAndEnterWorkspace(workspaceFile: URI): Promise<void> {
return this._proxy.$saveAndEnterWorkspace(workspaceFile);
}
}

View File

@@ -467,11 +467,14 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
createModelViewEditor(title: string, options?: azdata.ModelViewEditorOptions, name?: string): azdata.workspace.ModelViewEditor {
return extHostModelViewDialog.createModelViewEditor(title, extension, name, options);
},
createWorkspace(location: vscode.Uri, workspaceFile: vscode.Uri): Promise<void> {
return extHostWorkspace.$createWorkspace(location, workspaceFile);
createAndEnterWorkspace(location: vscode.Uri, workspaceFile: vscode.Uri): Promise<void> {
return extHostWorkspace.$createAndEnterWorkspace(location, workspaceFile);
},
enterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
return extHostWorkspace.$enterWorkspace(workspaceFile);
},
saveAndEnterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
return extHostWorkspace.$saveAndEnterWorkspace(workspaceFile);
}
};

View File

@@ -759,13 +759,15 @@ export interface ExtHostBackgroundTaskManagementShape {
}
export interface ExtHostWorkspaceShape {
$createWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
$createAndEnterWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
$enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
$saveAndEnterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
}
export interface MainThreadWorkspaceShape {
$createWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
$createAndEnterWorkspace(folder: vscode.Uri, workspaceFile: vscode.Uri): Promise<void>;
$enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
$saveAndEnterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
}
export interface MainThreadBackgroundTaskManagementShape extends IDisposable {