Skip to content

Commit

Permalink
Return resulting URI from commands that save the active editor (fix #…
Browse files Browse the repository at this point in the history
…178713) (#179091)

* Return resulting `URI` from commands that save the active editor (fix #178713)

* 💄

* address feedback

* change to real proposed API

* cleanup
  • Loading branch information
bpasero committed Apr 20, 2023
1 parent 5ea57c3 commit 1ed110b
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 31 deletions.
1 change: 1 addition & 0 deletions extensions/vscode-api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"portsAttributes",
"quickPickSortByLabel",
"resolvers",
"saveEditor",
"scmActionButton",
"scmSelectedProvider",
"scmTextDocument",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,6 @@ suite('vscode API - workspace', () => {
assert.deepStrictEqual(edt.selections, [new vscode.Selection(0, 0, 0, 3)]);
});


test('Support creating binary files in a WorkspaceEdit', async function (): Promise<any> {

const fileUri = vscode.Uri.parse(`${testFs.scheme}:/${rndName()}`);
Expand All @@ -1187,4 +1186,46 @@ suite('vscode API - workspace', () => {

assert.deepStrictEqual(actual, data);
});

test('saveAll', async () => {
await testSave(true);
});

test('save', async () => {
await testSave(false);
});

async function testSave(saveAll: boolean) {
const file = await createRandomFile();
const disposables: vscode.Disposable[] = [];

await revertAllDirty(); // needed for a clean state for `onDidSaveTextDocument` (#102365)

const onDidSaveTextDocument = new Set<vscode.TextDocument>();

disposables.push(vscode.workspace.onDidSaveTextDocument(e => {
onDidSaveTextDocument.add(e);
}));

const doc = await vscode.workspace.openTextDocument(file);
await vscode.window.showTextDocument(doc);

if (saveAll) {
const edit = new vscode.WorkspaceEdit();
edit.insert(doc.uri, new vscode.Position(0, 0), 'Hello World');

await vscode.workspace.applyEdit(edit);
assert.ok(doc.isDirty);

await vscode.workspace.saveAll(false); // requires dirty documents
} else {
const res = await vscode.workspace.save(doc.uri); // enforces to save even when not dirty
assert.ok(res?.toString() === doc.uri.toString());
}

assert.ok(onDidSaveTextDocument);
assert.ok(Array.from(onDidSaveTextDocument).find(e => e.uri.toString() === file.toString()), 'did Save: ' + file.toString());
disposeAll(disposables);
return deleteFile(file);
}
});
32 changes: 30 additions & 2 deletions src/vs/workbench/api/browser/mainThreadWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspa
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { checkGlobFileExists } from 'vs/workbench/services/extensions/common/workspaceContains';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService';
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol';
import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions';
import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor';
import { coalesce, firstOrDefault } from 'vs/base/common/arrays';

@extHostNamedCustomer(MainContext.MainThreadWorkspace)
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
Expand Down Expand Up @@ -201,8 +203,34 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {

// --- save & edit resources ---

async $save(uriComponents: UriComponents): Promise<UriComponents | undefined> {
const uri = URI.revive(uriComponents);

const editors = [...this._editorService.findEditors(uri, { supportSideBySide: SideBySideEditor.PRIMARY })];
const result = await this._editorService.save(editors, { reason: SaveReason.EXPLICIT, force: true /* force save even when non-dirty */ });

return firstOrDefault(this.saveResultToUris(result));
}

async $saveAs(uriComponents: UriComponents): Promise<UriComponents | undefined> {
const uri = URI.revive(uriComponents);

const editors = [...this._editorService.findEditors(uri, { supportSideBySide: SideBySideEditor.PRIMARY })];
const result = await this._editorService.save(editors, { reason: SaveReason.EXPLICIT, saveAs: true });

return firstOrDefault(this.saveResultToUris(result));
}

private saveResultToUris(result: ISaveEditorsResult): URI[] {
if (!result.success) {
return [];
}

return coalesce(result.editors.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY })));
}

$saveAll(includeUntitled?: boolean): Promise<boolean> {
return this._editorService.saveAll({ includeUntitled });
return this._editorService.saveAll({ includeUntitled }).then(res => res.success);
}

$resolveProxy(url: string): Promise<string | undefined> {
Expand Down
8 changes: 8 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I

return extHostWorkspace.findTextInFiles(query, options || {}, callback, extension.identifier, token);
},
save: (uri) => {
checkProposedApiEnabled(extension, 'saveEditor');
return extHostWorkspace.save(uri);
},
saveAs: (uri) => {
checkProposedApiEnabled(extension, 'saveEditor');
return extHostWorkspace.saveAs(uri);
},
saveAll: (includeUntitled?) => {
return extHostWorkspace.saveAll(includeUntitled);
},
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,8 @@ export interface MainThreadWorkspaceShape extends IDisposable {
$startFileSearch(includePattern: string | null, includeFolder: UriComponents | null, excludePatternOrDisregardExcludes: string | false | null, maxResults: number | null, token: CancellationToken): Promise<UriComponents[] | null>;
$startTextSearch(query: search.IPatternInfo, folder: UriComponents | null, options: ITextQueryBuilderOptions, requestId: number, token: CancellationToken): Promise<ITextSearchComplete | null>;
$checkExists(folders: readonly UriComponents[], includes: string[], token: CancellationToken): Promise<boolean>;
$save(uri: UriComponents): Promise<UriComponents | undefined>;
$saveAs(uri: UriComponents): Promise<UriComponents | undefined>;
$saveAll(includeUntitled?: boolean): Promise<boolean>;
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise<void>;
$resolveProxy(url: string): Promise<string | undefined>;
Expand Down
12 changes: 12 additions & 0 deletions src/vs/workbench/api/common/extHostWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,18 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
this._activeSearchCallbacks[requestId]?.(result);
}

async save(uri: URI): Promise<URI | undefined> {
const result = await this._proxy.$save(uri);

return URI.revive(result);
}

async saveAs(uri: URI): Promise<URI | undefined> {
const result = await this._proxy.$saveAs(uri);

return URI.revive(result);
}

saveAll(includeUntitled?: boolean): Promise<boolean> {
return this._proxy.$saveAll(includeUntitled);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ suite('Edit session sync', () => {
override registerTextModelContentProvider = () => ({ dispose: () => { } });
});
instantiationService.stub(IEditorService, new class extends mock<IEditorService>() {
override saveAll = async (_options: ISaveAllEditorsOptions) => true;
override saveAll = async (_options: ISaveAllEditorsOptions) => { return { success: true, editors: [] }; };
});
instantiationService.stub(IEditSessionIdentityService, new class extends mock<IEditSessionIdentityService>() {
override async getEditSessionIdentifier() {
Expand Down
17 changes: 9 additions & 8 deletions src/vs/workbench/contrib/files/browser/fileCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
when: undefined,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyP),
id: 'workbench.action.files.copyPathOfActiveFile',
handler: async (accessor) => {
handler: async accessor => {
const editorService = accessor.get(IEditorService);
const activeInput = editorService.activeEditor;
const resource = EditorResourceAccessor.getOriginalUri(activeInput, { supportSideBySide: SideBySideEditor.PRIMARY });
Expand Down Expand Up @@ -491,7 +491,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyS },
win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyS) },
id: SAVE_ALL_COMMAND_ID,
handler: (accessor) => {
handler: accessor => {
return saveDirtyEditorsOfGroups(accessor, accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), { reason: SaveReason.EXPLICIT });
}
});
Expand All @@ -516,10 +516,11 @@ CommandsRegistry.registerCommand({

CommandsRegistry.registerCommand({
id: SAVE_FILES_COMMAND_ID,
handler: accessor => {
handler: async accessor => {
const editorService = accessor.get(IEditorService);

return editorService.saveAll({ includeUntitled: false, reason: SaveReason.EXPLICIT });
const res = await editorService.saveAll({ includeUntitled: false, reason: SaveReason.EXPLICIT });
return res.success;
}
});

Expand Down Expand Up @@ -580,7 +581,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()),
primary: KeyCode.LeftArrow,
id: PREVIOUS_COMPRESSED_FOLDER,
handler: (accessor) => {
handler: accessor => {
const paneCompositeService = accessor.get(IPaneCompositePartService);
const viewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar);

Expand All @@ -599,7 +600,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()),
primary: KeyCode.RightArrow,
id: NEXT_COMPRESSED_FOLDER,
handler: (accessor) => {
handler: accessor => {
const paneCompositeService = accessor.get(IPaneCompositePartService);
const viewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar);

Expand All @@ -618,7 +619,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()),
primary: KeyCode.Home,
id: FIRST_COMPRESSED_FOLDER,
handler: (accessor) => {
handler: accessor => {
const paneCompositeService = accessor.get(IPaneCompositePartService);
const viewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar);

Expand All @@ -637,7 +638,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()),
primary: KeyCode.End,
id: LAST_COMPRESSED_FOLDER,
handler: (accessor) => {
handler: accessor => {
const paneCompositeService = accessor.get(IPaneCompositePartService);
const viewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar);

Expand Down
11 changes: 7 additions & 4 deletions src/vs/workbench/services/editor/browser/editorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, isEditorReplacement, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IUntypedEditorReplacement, IEditorService, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions, PreferredGroup, isPreferredGroup, IEditorsChangeEvent } from 'vs/workbench/services/editor/common/editorService';
import { IUntypedEditorReplacement, IEditorService, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions, PreferredGroup, isPreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { coalesce, distinct } from 'vs/base/common/arrays';
Expand Down Expand Up @@ -894,7 +894,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {

//#region save/revert

async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> {
async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<ISaveEditorsResult> {

// Convert to array
if (!Array.isArray(editors)) {
Expand Down Expand Up @@ -973,10 +973,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
}

return saveResults.every(result => !!result);
return {
success: saveResults.every(result => !!result),
editors: coalesce(saveResults)
};
}

saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> {
saveAll(options?: ISaveAllEditorsOptions): Promise<ISaveEditorsResult> {
return this.save(this.getAllDirtyEditors(options), options);
}

Expand Down
21 changes: 15 additions & 6 deletions src/vs/workbench/services/editor/common/editorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export interface ISaveEditorsOptions extends ISaveOptions {
readonly saveAs?: boolean;
}

export interface ISaveEditorsResult {

/**
* Whether the save operation was successful.
*/
readonly success: boolean;

/**
* Resulting editors after the save operation.
*/
readonly editors: Array<EditorInput | IUntypedEditorInput>;
}

export interface IUntypedEditorReplacement {

/**
Expand Down Expand Up @@ -293,17 +306,13 @@ export interface IEditorService {

/**
* Save the provided list of editors.
*
* @returns `true` if all editors saved and `false` otherwise.
*/
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean>;
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<ISaveEditorsResult>;

/**
* Save all editors.
*
* @returns `true` if all editors saved and `false` otherwise.
*/
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean>;
saveAll(options?: ISaveAllEditorsOptions): Promise<ISaveEditorsResult>;

/**
* Reverts the provided list of editors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2068,7 +2068,9 @@ suite('EditorService', () => {
await service.openEditor(input2, { pinned: true });
await service.openEditor(sameInput1, { pinned: true }, SIDE_GROUP);

await service.save({ groupId: rootGroup.id, editor: input1 });
const res1 = await service.save({ groupId: rootGroup.id, editor: input1 });
assert.strictEqual(res1.success, true);
assert.strictEqual(res1.editors[0], input1);
assert.strictEqual(input1.gotSaved, true);

input1.gotSaved = false;
Expand All @@ -2079,7 +2081,9 @@ suite('EditorService', () => {
input2.dirty = true;
sameInput1.dirty = true;

await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true });
const res2 = await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true });
assert.strictEqual(res2.success, true);
assert.strictEqual(res2.editors[0], input1);
assert.strictEqual(input1.gotSavedAs, true);

input1.gotSaved = false;
Expand All @@ -2102,8 +2106,9 @@ suite('EditorService', () => {
input2.dirty = true;
sameInput1.dirty = true;

const saveRes = await service.saveAll();
assert.strictEqual(saveRes, true);
const res3 = await service.saveAll();
assert.strictEqual(res3.success, true);
assert.strictEqual(res3.editors.length, 2);
assert.strictEqual(input1.gotSaved, true);
assert.strictEqual(input2.gotSaved, true);

Expand Down Expand Up @@ -2161,7 +2166,8 @@ suite('EditorService', () => {
sameInput1.dirty = true;

const saveRes = await service.saveAll({ excludeSticky: true });
assert.strictEqual(saveRes, true);
assert.strictEqual(saveRes.success, true);
assert.strictEqual(saveRes.editors.length, 2);
assert.strictEqual(input1.gotSaved, false);
assert.strictEqual(input2.gotSaved, true);
assert.strictEqual(sameInput1.gotSaved, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const allApiProposals = Object.freeze({
quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts',
quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts',
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
saveEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.saveEditor.d.ts',
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',
scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts',
scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp

let result: boolean | undefined = undefined;
if (typeof arg1 === 'boolean' || dirtyWorkingCopies.length === this.workingCopyService.dirtyCount) {
result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions });
result = (await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions })).success;
}

// If we still have dirty working copies, save those directly
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor';
import { Dimension, IDimension } from 'vs/base/browser/dom';
Expand Down Expand Up @@ -997,8 +997,8 @@ export class TestEditorService implements EditorServiceImpl {
isOpened(_editor: IResourceEditorInputIdentifier): boolean { return false; }
isVisible(_editor: EditorInput): boolean { return false; }
replaceEditors(_editors: any, _group: any) { return Promise.resolve(undefined); }
save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
saveAll(options?: ISaveEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<ISaveEditorsResult> { throw new Error('Method not implemented.'); }
saveAll(options?: ISaveEditorsOptions): Promise<ISaveEditorsResult> { throw new Error('Method not implemented.'); }
revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> { throw new Error('Method not implemented.'); }
}
Expand Down

0 comments on commit 1ed110b

Please sign in to comment.