mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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); };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')]);
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
src/sql/azdata.proposed.d.ts
vendored
8
src/sql/azdata.proposed.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user