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

Initial implementation of Continue Edit Session #151528

Merged
merged 21 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build/lib/i18n.resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@
"name": "vs/workbench/contrib/userDataSync",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/sessionSync",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/views",
"project": "vscode-workbench"
Expand Down Expand Up @@ -426,6 +430,10 @@
"name": "vs/workbench/services/userDataSync",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/services/sessionSync",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/services/views",
"project": "vscode-workbench"
Expand Down
2 changes: 2 additions & 0 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export interface IProductConfiguration {

readonly 'configurationSync.store'?: ConfigurationSyncStore;

readonly 'sessionSync.store'?: Omit<ConfigurationSyncStore, 'insidersUrl' | 'stableUrl'>;

readonly darwinUniversalAssetId?: string;
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/userDataSync/common/userDataSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export interface IResourceRefHandle {
created: number;
}

export type ServerResource = SyncResource | 'machines';
export type ServerResource = SyncResource | 'machines' | 'editSessions';
export type UserDataSyncStoreType = 'insiders' | 'stable';

export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { ISessionSyncWorkbenchService, Change, ChangeType, Folder, EditSession, FileType } from 'vs/workbench/services/sessionSync/common/sessionSync';
import { ISCMService } from 'vs/workbench/contrib/scm/common/scm';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { joinPath, relativePath } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { SessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';

registerSingleton(ISessionSyncWorkbenchService, SessionSyncWorkbenchService);

const SYNC_TITLE = localize('session sync', 'Edit Sessions');
const applyLatestCommand = {
id: 'workbench.sessionSync.actions.applyLatest',
title: localize('apply latest', "{0}: Apply Latest Edit Session", SYNC_TITLE),
};
const storeLatestCommand = {
id: 'workbench.sessionSync.actions.storeLatest',
title: localize('store latest', "{0}: Store Latest Edit Session", SYNC_TITLE),
};

export class SessionSyncContribution extends Disposable implements IWorkbenchContribution {

private registered = false;

constructor(
@ISessionSyncWorkbenchService private readonly sessionSyncWorkbenchService: ISessionSyncWorkbenchService,
@IFileService private readonly fileService: IFileService,
@IProgressService private readonly progressService: IProgressService,
@ISCMService private readonly scmService: ISCMService,
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
) {
super();

this.configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('workbench.experimental.sessionSync.enabled')) {
this.registerActions();
}
});

this.registerActions();
}

private registerActions() {
if (this.registered || this.configurationService.getValue('workbench.experimental.sessionSync.enabled') === false) {
return;
}

this.registerApplyEditSessionAction();
this.registerStoreEditSessionAction();

this.registered = true;
}

private registerApplyEditSessionAction(): void {
const that = this;
this._register(registerAction2(class ApplyEditSessionAction extends Action2 {
constructor() {
super({
id: applyLatestCommand.id,
title: applyLatestCommand.title,
menu: {
id: MenuId.CommandPalette,
}
});
}

async run(accessor: ServicesAccessor): Promise<void> {
await that.progressService.withProgress({
location: ProgressLocation.Notification,
title: localize('applying edit session', 'Applying edit session...')
}, async () => await that.applyEditSession());
}
}));
}

private registerStoreEditSessionAction(): void {
const that = this;
this._register(registerAction2(class StoreEditSessionAction extends Action2 {
constructor() {
super({
id: storeLatestCommand.id,
title: storeLatestCommand.title,
menu: {
id: MenuId.CommandPalette,
}
});
}

async run(accessor: ServicesAccessor): Promise<void> {
await that.progressService.withProgress({
location: ProgressLocation.Notification,
title: localize('storing edit session', 'Storing edit session...')
}, async () => await that.storeEditSession());
}
}));
}

async applyEditSession() {
const editSession = await this.sessionSyncWorkbenchService.read();
if (!editSession) {
return;
}

for (const folder of editSession.folders) {
const folderRoot = this.contextService.getWorkspace().folders.find((f) => f.name === folder.name);
if (!folderRoot) {
return;
}

for (const { relativeFilePath, contents, type } of folder.workingChanges) {
const uri = joinPath(folderRoot.uri, relativeFilePath);
if (type === ChangeType.Addition) {
await this.fileService.writeFile(uri, VSBuffer.fromString(contents));
} else if (type === ChangeType.Deletion && await this.fileService.exists(uri)) {
await this.fileService.del(uri);
}
}
}
}

async storeEditSession() {
const folders: Folder[] = [];

for (const repository of this.scmService.repositories) {
// Look through all resource groups and compute which files were added/modified/deleted
const trackedUris = repository.provider.groups.elements.reduce((resources, resourceGroups) => {
resourceGroups.elements.map((resource) => resources.add(resource.sourceUri));
return resources;
}, new Set<URI>()); // A URI might appear in more than one resource group

const workingChanges: Change[] = [];
let name = repository.provider.rootUri ? this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name : undefined;

for (const uri of trackedUris) {
const workspaceFolder = this.contextService.getWorkspaceFolder(uri);
if (!workspaceFolder) {
continue;
}

name = name ?? workspaceFolder.name;
const relativeFilePath = relativePath(workspaceFolder.uri, uri) ?? uri.path;

// Only deal with file contents for now
try {
if (!(await this.fileService.stat(uri)).isFile) {
continue;
}
} catch { }

if (await this.fileService.exists(uri)) {
workingChanges.push({ type: ChangeType.Addition, fileType: FileType.File, contents: (await this.fileService.readFile(uri)).value.toString(), relativeFilePath: relativeFilePath });
} else {
// Assume it's a deletion
workingChanges.push({ type: ChangeType.Deletion, fileType: FileType.File, contents: undefined, relativeFilePath: relativeFilePath });
}
}

folders.push({ workingChanges, name: name ?? '' });
}

const data: EditSession = { folders, version: 1 };

await this.sessionSyncWorkbenchService.write(data);
}
}

const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(SessionSyncContribution, LifecyclePhase.Restored);
110 changes: 110 additions & 0 deletions src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
import { SessionSyncContribution } from 'vs/workbench/contrib/sessionSync/browser/sessionSync.contribution';
import { ProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ISCMService } from 'vs/workbench/contrib/scm/common/scm';
import { SCMService } from 'vs/workbench/contrib/scm/common/scmService';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { mock } from 'vs/base/test/common/mock';
import * as sinon from 'sinon';
import * as assert from 'assert';
import { ChangeType, FileType, ISessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/common/sessionSync';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';

const folderName = 'test-folder';
const folderUri = URI.file(`/${folderName}`);

suite('Edit session sync', () => {
let instantiationService: TestInstantiationService;
let sessionSyncContribution: SessionSyncContribution;
let fileService: FileService;

const disposables = new DisposableStore();

setup(() => {
instantiationService = new TestInstantiationService();

// Set up filesystem
const logService = new NullLogService();
fileService = disposables.add(new FileService(logService));
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
fileService.registerProvider(Schemas.file, fileSystemProvider);

// Stub out all services
instantiationService.stub(ILogService, logService);
instantiationService.stub(IFileService, fileService);
instantiationService.stub(ISessionSyncWorkbenchService, new class extends mock<ISessionSyncWorkbenchService>() { });
instantiationService.stub(IProgressService, ProgressService);
instantiationService.stub(ISCMService, SCMService);
instantiationService.stub(IConfigurationService, new TestConfigurationService({ workbench: { experimental: { sessionSync: { enabled: true } } } }));
instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
override getWorkspace() {
return {
id: 'workspace-id',
folders: [{
uri: folderUri,
name: folderName,
index: 0,
toResource: (relativePath: string) => joinPath(folderUri, relativePath)
}]
};
}
});

sessionSyncContribution = instantiationService.createInstance(SessionSyncContribution);
});

teardown(() => {
sinon.restore();
disposables.clear();
});

test('Can apply edit session', async function () {
const fileUri = joinPath(folderUri, 'dir1', 'README.md');
const fileContents = '# readme';
const editSession = {
version: 1,
folders: [
{
name: folderName,
workingChanges: [
{
relativeFilePath: 'dir1/README.md',
fileType: FileType.File,
contents: fileContents,
type: ChangeType.Addition
}
]
}
]
};

// Stub sync service to return edit session data
const sandbox = sinon.createSandbox();
const readStub = sandbox.stub().returns(editSession);
instantiationService.stub(ISessionSyncWorkbenchService, 'read', readStub);

// Create root folder
await fileService.createFolder(folderUri);

// Apply edit session
await sessionSyncContribution.applyEditSession();

// Verify edit session was correctly applied
assert.equal((await fileService.readFile(fileUri)).value.toString(), fileContents);
});
});
Loading