save participants - provide access to the from resource in "Save As" scenarios (#203516)

This commit is contained in:
Benjamin Pasero 2024-01-26 11:53:26 +01:00 committed by GitHub
parent b26b05031e
commit 0e7e31b4be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 172 additions and 60 deletions

View file

@ -58,7 +58,7 @@ else {
// Running out of sources
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.82.0-dev',
version: '1.87.0-dev',
nameShort: 'Code - OSS Dev',
nameLong: 'Code - OSS Dev',
applicationName: 'code-oss',

View file

@ -8,11 +8,10 @@ import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { SaveReason } from 'vs/workbench/common/editor';
import { ExtHostContext, ExtHostNotebookDocumentSaveParticipantShape } from '../common/extHost.protocol';
import { IDisposable } from 'vs/base/common/lifecycle';
import { raceCancellationError } from 'vs/base/common/async';
import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopySaveParticipant, IStoredFileWorkingCopySaveParticipantContext, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
@ -24,7 +23,7 @@ class ExtHostNotebookDocumentSaveParticipant implements IStoredFileWorkingCopySa
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookDocumentSaveParticipant);
}
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, env: { reason: SaveReason }, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) {
return undefined;
@ -38,7 +37,7 @@ class ExtHostNotebookDocumentSaveParticipant implements IStoredFileWorkingCopySa
() => reject(new Error(localize('timeout.onWillSave', "Aborted onWillSaveNotebookDocument-event after 1750ms"))),
1750
);
this._proxy.$participateInSave(workingCopy.resource, env.reason, token).then(_ => {
this._proxy.$participateInSave(workingCopy.resource, context.reason, token).then(_ => {
clearTimeout(_warningTimeout);
return undefined;
}).then(resolve, reject);

View file

@ -9,8 +9,7 @@ import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ITextFileSaveParticipant, ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { SaveReason } from 'vs/workbench/common/editor';
import { ITextFileSaveParticipant, ITextFileService, ITextFileEditorModel, ITextFileSaveParticipantContext } from 'vs/workbench/services/textfile/common/textfiles';
import { ExtHostContext, ExtHostDocumentSaveParticipantShape } from '../common/extHost.protocol';
import { IDisposable } from 'vs/base/common/lifecycle';
import { raceCancellationError } from 'vs/base/common/async';
@ -23,7 +22,7 @@ class ExtHostSaveParticipant implements ITextFileSaveParticipant {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant);
}
async participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(editorModel: ITextFileEditorModel, context: ITextFileSaveParticipantContext, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (!editorModel.textEditorModel || !shouldSynchronizeModel(editorModel.textEditorModel)) {
// the model never made it to the extension
@ -37,7 +36,7 @@ class ExtHostSaveParticipant implements ITextFileSaveParticipant {
() => reject(new Error(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms"))),
1750
);
this._proxy.$participateInSave(editorModel.resource, env.reason).then(values => {
this._proxy.$participateInSave(editorModel.resource, context.reason).then(values => {
if (!values.every(success => success)) {
return Promise.reject(new Error('listener failed'));
}

View file

@ -30,7 +30,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as
import { SaveReason } from 'vs/workbench/common/editor';
import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ITextFileEditorModel, ITextFileSaveParticipant, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileEditorModel, ITextFileSaveParticipant, ITextFileSaveParticipantContext, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export class TrimWhitespaceParticipant implements ITextFileSaveParticipant {
@ -41,13 +41,13 @@ export class TrimWhitespaceParticipant implements ITextFileSaveParticipant {
// Nothing
}
async participate(model: ITextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext): Promise<void> {
if (!model.textEditorModel) {
return;
}
if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageId(), resource: model.resource })) {
this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO);
this.doTrimTrailingWhitespace(model.textEditorModel, context.reason === SaveReason.AUTO);
}
}
@ -107,7 +107,7 @@ export class FinalNewLineParticipant implements ITextFileSaveParticipant {
// Nothing
}
async participate(model: ITextFileEditorModel, _env: { reason: SaveReason }): Promise<void> {
async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext): Promise<void> {
if (!model.textEditorModel) {
return;
}
@ -145,13 +145,13 @@ export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant {
// Nothing
}
async participate(model: ITextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext): Promise<void> {
if (!model.textEditorModel) {
return;
}
if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageId(), resource: model.resource })) {
this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO);
this.doTrimFinalNewLines(model.textEditorModel, context.reason === SaveReason.AUTO);
}
}
@ -217,11 +217,11 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant {
// Nothing
}
async participate(model: ITextFileEditorModel, env: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (!model.textEditorModel) {
return;
}
if (env.reason === SaveReason.AUTO) {
if (context.reason === SaveReason.AUTO) {
return undefined;
}
@ -272,7 +272,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant {
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
) { }
async participate(model: ITextFileEditorModel, env: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (!model.textEditorModel) {
return;
}
@ -286,11 +286,11 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant {
return undefined;
}
if (env.reason === SaveReason.AUTO) {
if (context.reason === SaveReason.AUTO) {
return undefined;
}
if (env.reason !== SaveReason.EXPLICIT && Array.isArray(setting)) {
if (context.reason !== SaveReason.EXPLICIT && Array.isArray(setting)) {
return undefined;
}
@ -326,7 +326,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant {
progress.report({ message: localize('codeaction', "Quick Fixes") });
const filteredSaveList = Array.isArray(setting) ? codeActionsOnSave : codeActionsOnSave.filter(x => setting[x.value] === 'always' || ((setting[x.value] === 'explicit' || setting[x.value] === true) && env.reason === SaveReason.EXPLICIT));
const filteredSaveList = Array.isArray(setting) ? codeActionsOnSave : codeActionsOnSave.filter(x => setting[x.value] === 'always' || ((setting[x.value] === 'explicit' || setting[x.value] === true) && context.reason === SaveReason.EXPLICIT));
await this.applyOnSaveActions(textEditorModel, filteredSaveList, excludedActions, progress, token);
}

View file

@ -36,7 +36,7 @@ import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/comm
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopySaveParticipant, IStoredFileWorkingCopySaveParticipantContext, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
constructor(
@ -47,7 +47,7 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
@IConfigurationService private readonly configurationService: IConfigurationService,
) { }
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) {
return;
}
@ -108,7 +108,7 @@ class TrimWhitespaceParticipant implements IStoredFileWorkingCopySaveParticipant
@IBulkEditService private readonly bulkEditService: IBulkEditService,
) { }
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
if (this.configurationService.getValue<boolean>('files.trimTrailingWhitespace')) {
await this.doTrimTrailingWhitespace(workingCopy, context.reason === SaveReason.AUTO, progress);
}
@ -175,7 +175,7 @@ class TrimFinalNewLinesParticipant implements IStoredFileWorkingCopySaveParticip
@IBulkEditService private readonly bulkEditService: IBulkEditService,
) { }
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
if (this.configurationService.getValue<boolean>('files.trimFinalNewlines')) {
await this.doTrimFinalNewLines(workingCopy, context.reason === SaveReason.AUTO, progress);
}
@ -252,7 +252,7 @@ class FinalNewLineParticipant implements IStoredFileWorkingCopySaveParticipant {
@IEditorService private readonly editorService: IEditorService,
) { }
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
// waiting on notebook-specific override before this feature can sync with 'files.insertFinalNewline'
// if (this.configurationService.getValue('files.insertFinalNewline')) {
@ -317,7 +317,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
) {
}
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
const nbDisposable = new DisposableStore();
const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted();
if (!isTrusted) {

View file

@ -560,7 +560,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
// save model
return targetModel.save(options);
return targetModel.save({
...options,
from: source
});
}
private async confirmOverwrite(resource: URI): Promise<boolean> {

View file

@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { mark } from 'vs/base/common/performance';
import { assertIsDefined } from 'vs/base/common/types';
import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, TextFileResolveReason, ITextFileEditorModelSaveEvent, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { IRevertOptions, SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup';
@ -723,7 +723,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
//#region Save
async save(options: ITextFileSaveOptions = Object.create(null)): Promise<boolean> {
async save(options: ITextFileSaveAsOptions = Object.create(null)): Promise<boolean> {
if (!this.isResolved()) {
return false;
}
@ -751,7 +751,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.hasState(TextFileEditorModelState.SAVED);
}
private async doSave(options: ITextFileSaveOptions): Promise<void> {
private async doSave(options: ITextFileSaveAsOptions): Promise<void> {
if (typeof options.reason !== 'number') {
options.reason = SaveReason.EXPLICIT;
}
@ -852,7 +852,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (!saveCancellation.token.isCancellationRequested) {
this.ignoreSaveFromSaveParticipants = true;
try {
await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token);
await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, saveCancellation.token);
} finally {
this.ignoreSaveFromSaveParticipants = false;
}
@ -919,7 +919,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
})(), () => saveCancellation.cancel());
}
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void {
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveAsOptions): void {
// Updated resolved stat with updated stat
this.updateLastResolvedFileStat(stat);
@ -939,7 +939,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this._onDidSave.fire({ reason: options.reason, stat, source: options.source });
}
private handleSaveError(error: Error, versionId: number, options: ITextFileSaveOptions): void {
private handleSaveError(error: Error, versionId: number, options: ITextFileSaveAsOptions): void {
(options.ignoreErrorHandler ? this.logService.trace : this.logService.error).apply(this.logService, [`[text file model] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString()]);
// Return early if the save() call was made asking to

View file

@ -16,10 +16,9 @@ import { IFileService, FileChangesEvent, FileOperation, FileChangeType, IFileSys
import { Promises, ResourceQueue } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { TextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textFileSaveParticipant';
import { SaveReason } from 'vs/workbench/common/editor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopySaveParticipantContext, IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { ITextSnapshot } from 'vs/editor/common/model';
import { extname, joinPath } from 'vs/base/common/resources';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
@ -536,7 +535,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return this.saveParticipants.addSaveParticipant(participant);
}
runSaveParticipants(model: ITextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise<void> {
runSaveParticipants(model: ITextFileEditorModel, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise<void> {
return this.saveParticipants.participate(model, context, token);
}

View file

@ -8,8 +8,7 @@ import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { ITextFileSaveParticipant, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { SaveReason } from 'vs/workbench/common/editor';
import { ITextFileSaveParticipant, ITextFileEditorModel, ITextFileSaveParticipantContext } from 'vs/workbench/services/textfile/common/textfiles';
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { insert } from 'vs/base/common/arrays';
@ -30,7 +29,7 @@ export class TextFileSaveParticipant extends Disposable {
return toDisposable(() => remove());
}
participate(model: ITextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise<void> {
participate(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, token: CancellationToken): Promise<void> {
const cts = new CancellationTokenSource(token);
return this.progressService.withProgress({

View file

@ -313,6 +313,22 @@ export interface ITextFileResolveEvent {
readonly reason: TextFileResolveReason;
}
export interface ITextFileSaveParticipantContext {
/**
* The reason why the save was triggered.
*/
readonly reason: SaveReason;
/**
* Only applies to when a text file was saved as, for
* example when starting with untitled and saving. This
* provides access to the initial resource the text
* file had before.
*/
readonly savedFrom?: URI;
}
export interface ITextFileSaveParticipant {
/**
@ -321,7 +337,7 @@ export interface ITextFileSaveParticipant {
*/
participate(
model: ITextFileEditorModel,
context: { reason: SaveReason },
context: ITextFileSaveParticipantContext,
progress: IProgress<IProgressStep>,
token: CancellationToken
): Promise<void>;
@ -369,7 +385,7 @@ export interface ITextFileEditorModelManager {
/**
* Runs the registered save participants on the provided model.
*/
runSaveParticipants(model: ITextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise<void>;
runSaveParticipants(model: ITextFileEditorModel, context: ITextFileSaveParticipantContext, token: CancellationToken): Promise<void>;
/**
* Waits for the model to be ready to be disposed. There may be conditions
@ -406,6 +422,11 @@ export interface ITextFileSaveOptions extends ISaveOptions {
export interface ITextFileSaveAsOptions extends ITextFileSaveOptions {
/**
* Optional URI of the resource the text file is saved from if known.
*/
readonly from?: URI;
/**
* Optional URI to use as suggested file path to save as.
*/
@ -498,7 +519,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
updatePreferredEncoding(encoding: string | undefined): void;
save(options?: ITextFileSaveOptions): Promise<boolean>;
save(options?: ITextFileSaveAsOptions): Promise<boolean>;
revert(options?: IRevertOptions): Promise<void>;
resolve(options?: ITextFileResolveOptions): Promise<void>;

View file

@ -19,6 +19,7 @@ import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor';
import { isEqual } from 'vs/base/common/resources';
import { UTF16be } from 'vs/workbench/services/textfile/common/encoding';
import { isWeb } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
suite('Files - TextFileEditorModel', () => {
@ -849,5 +850,31 @@ suite('Files - TextFileEditorModel', () => {
await savePromise;
}
test('Save Participant carries context', async function () {
const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined));
const from = URI.file('testFrom');
let e: Error | undefined = undefined;
disposables.add(accessor.textFileService.files.addSaveParticipant({
participate: async (wc, context) => {
try {
assert.strictEqual(context.reason, SaveReason.EXPLICIT);
assert.strictEqual(context.savedFrom?.toString(), from.toString());
} catch (error) {
e = error;
}
}
}));
await model.resolve();
model.updateTextEditorModel(createTextBufferFactory('foo'));
await model.save({ force: true, from });
if (e) {
throw e;
}
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View file

@ -429,7 +429,11 @@ export class FileWorkingCopyManager<S extends IStoredFileWorkingCopyModel, U ext
}
// Save target
const success = await targetStoredFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ });
const success = await targetStoredFileWorkingCopy.save({
...options,
from: source,
force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */
});
if (!success) {
return undefined;
}

View file

@ -156,6 +156,15 @@ export interface IStoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> e
* Whether the stored file working copy is readonly or not.
*/
isReadonly(): boolean | IMarkdownString;
/**
* Asks the stored file working copy to save. If the stored file
* working copy was dirty, it is expected to be non-dirty after
* this operation has finished.
*
* @returns `true` if the operation was successful and `false` otherwise.
*/
save(options?: IStoredFileWorkingCopySaveAsOptions): Promise<boolean>;
}
export interface IResolvedStoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extends IStoredFileWorkingCopy<M> {
@ -236,6 +245,14 @@ export interface IStoredFileWorkingCopySaveOptions extends ISaveOptions {
readonly ignoreErrorHandler?: boolean;
}
export interface IStoredFileWorkingCopySaveAsOptions extends IStoredFileWorkingCopySaveOptions {
/**
* Optional URI of the resource the text file is saved from if known.
*/
readonly from?: URI;
}
export interface IStoredFileWorkingCopyResolver {
/**
@ -815,7 +832,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
private ignoreSaveFromSaveParticipants = false;
async save(options: IStoredFileWorkingCopySaveOptions = Object.create(null)): Promise<boolean> {
async save(options: IStoredFileWorkingCopySaveAsOptions = Object.create(null)): Promise<boolean> {
if (!this.isResolved()) {
return false;
}
@ -843,7 +860,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
return this.hasState(StoredFileWorkingCopyState.SAVED);
}
private async doSave(options: IStoredFileWorkingCopySaveOptions): Promise<void> {
private async doSave(options: IStoredFileWorkingCopySaveAsOptions): Promise<void> {
if (typeof options.reason !== 'number') {
options.reason = SaveReason.EXPLICIT;
}
@ -947,7 +964,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
if (!saveCancellation.token.isCancellationRequested) {
this.ignoreSaveFromSaveParticipants = true;
try {
await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token);
await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT, savedFrom: options.from }, saveCancellation.token);
} finally {
this.ignoreSaveFromSaveParticipants = false;
}
@ -1039,7 +1056,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
})(), () => saveCancellation.cancel());
}
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IStoredFileWorkingCopySaveOptions): void {
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IStoredFileWorkingCopySaveAsOptions): void {
// Updated resolved stat with updated stat
this.updateLastResolvedFileStat(stat);
@ -1059,7 +1076,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
this._onDidSave.fire({ reason: options.reason, stat, source: options.source });
}
private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void {
private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveAsOptions): void {
(options.ignoreErrorHandler ? this.logService.trace : this.logService.error).apply(this.logService, [`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId]);
// Return early if the save() call was made asking to

View file

@ -8,10 +8,9 @@ import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { SaveReason } from 'vs/workbench/common/editor';
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { insert } from 'vs/base/common/arrays';
import { IStoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopySaveParticipant, IStoredFileWorkingCopySaveParticipantContext } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
export class StoredFileWorkingCopySaveParticipant extends Disposable {
@ -33,7 +32,7 @@ export class StoredFileWorkingCopySaveParticipant extends Disposable {
return toDisposable(() => remove());
}
participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, token: CancellationToken): Promise<void> {
participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise<void> {
const cts = new CancellationTokenSource(token);
return this.progressService.withProgress({

View file

@ -84,6 +84,21 @@ export interface IWorkingCopyFileOperationParticipant {
): Promise<void>;
}
export interface IStoredFileWorkingCopySaveParticipantContext {
/**
* The reason why the save was triggered.
*/
readonly reason: SaveReason;
/**
* Only applies to when a text file was saved as, for
* example when starting with untitled and saving. This
* provides access to the initial resource the text
* file had before.
*/
readonly savedFrom?: URI;
}
export interface IStoredFileWorkingCopySaveParticipant {
/**
@ -92,7 +107,7 @@ export interface IStoredFileWorkingCopySaveParticipant {
*/
participate(
workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>,
context: { reason: SaveReason },
context: IStoredFileWorkingCopySaveParticipantContext,
progress: IProgress<IProgressStep>,
token: CancellationToken
): Promise<void>;
@ -191,7 +206,7 @@ export interface IWorkingCopyFileService {
/**
* Runs all available save participants for stored file working copies.
*/
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, token: CancellationToken): Promise<void>;
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise<void>;
//#endregion
@ -492,7 +507,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
return this.saveParticipants.addSaveParticipant(participant);
}
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, token: CancellationToken): Promise<void> {
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise<void> {
return this.saveParticipants.participate(workingCopy, context, token);
}

View file

@ -879,11 +879,12 @@ suite('StoredFileWorkingCopy', function () {
});
async function testSaveFromSaveParticipant(workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>, async: boolean): Promise<void> {
const from = URI.file('testFrom');
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, false);
const disposable = accessor.workingCopyFileService.addSaveParticipant({
participate: async () => {
participate: async (wc, context) => {
if (async) {
await timeout(10);
}
@ -894,11 +895,40 @@ suite('StoredFileWorkingCopy', function () {
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, true);
await workingCopy.save({ force: true });
await workingCopy.save({ force: true, from });
disposable.dispose();
}
test('Save Participant carries context', async function () {
await workingCopy.resolve();
const from = URI.file('testFrom');
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, false);
let e: Error | undefined = undefined;
const disposable = accessor.workingCopyFileService.addSaveParticipant({
participate: async (wc, context) => {
try {
assert.strictEqual(context.reason, SaveReason.EXPLICIT);
assert.strictEqual(context.savedFrom?.toString(), from.toString());
} catch (error) {
e = error;
}
}
});
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, true);
await workingCopy.save({ force: true, from });
if (e) {
throw e;
}
disposable.dispose();
});
test('revert', async () => {
await workingCopy.resolve();
workingCopy.model?.updateContents('hello revert');

View file

@ -15,7 +15,7 @@ import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { InMemoryStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy';
import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation, IStoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation, IStoredFileWorkingCopySaveParticipant, IStoredFileWorkingCopySaveParticipantContext } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IBaseFileStat, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { ISaveOptions, IRevertOptions, SaveReason, GroupIdentifier } from 'vs/workbench/common/editor';
@ -253,7 +253,7 @@ export class TestWorkingCopyFileService implements IWorkingCopyFileService {
readonly hasSaveParticipants = false;
addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable { return Disposable.None; }
async runSaveParticipants(workingCopy: IWorkingCopy, context: { reason: SaveReason }, token: CancellationToken): Promise<void> { }
async runSaveParticipants(workingCopy: IWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, token: CancellationToken): Promise<void> { }
async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<void> { }