mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)
* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 * disable strict null check
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService';
|
||||
import { ITextFileService, IResourceEncodings, IResourceEncoding } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export class BrowserTextFileService extends TextFileService {
|
||||
|
||||
@@ -14,6 +16,42 @@ export class BrowserTextFileService extends TextFileService {
|
||||
return { encoding: 'utf8', hasBOM: false };
|
||||
}
|
||||
};
|
||||
|
||||
protected beforeShutdown(reason: ShutdownReason): boolean {
|
||||
// Web: we cannot perform long running in the shutdown phase
|
||||
// As such we need to check sync if there are any dirty files
|
||||
// that have not been backed up yet and then prevent the shutdown
|
||||
// if that is the case.
|
||||
return this.doBeforeShutdownSync(reason);
|
||||
}
|
||||
|
||||
private doBeforeShutdownSync(reason: ShutdownReason): boolean {
|
||||
const dirtyResources = this.getDirty();
|
||||
if (!dirtyResources.length) {
|
||||
return false; // no dirty: no veto
|
||||
}
|
||||
|
||||
if (!this.isHotExitEnabled) {
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
|
||||
for (const dirtyResource of dirtyResources) {
|
||||
let hasBackup = false;
|
||||
|
||||
if (this.fileService.canHandleResource(dirtyResource)) {
|
||||
const model = this.models.get(dirtyResource);
|
||||
hasBackup = !!(model && model.hasBackup());
|
||||
} else if (dirtyResource.scheme === Schemas.untitled) {
|
||||
hasBackup = this.untitledEditorService.hasBackup(dirtyResource);
|
||||
}
|
||||
|
||||
if (!hasBackup) {
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
}
|
||||
|
||||
return false; // dirty with backups: no veto
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ITextFileService, BrowserTextFileService);
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { guessMimeTypes } from 'vs/base/common/mime';
|
||||
@@ -25,10 +24,9 @@ import { RunOnceScheduler, timeout } from 'vs/base/common/async';
|
||||
import { ITextBufferFactory } from 'vs/editor/common/model';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources';
|
||||
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
@@ -39,6 +37,22 @@ export interface IBackupMetaData {
|
||||
orphaned: boolean;
|
||||
}
|
||||
|
||||
type FileTelemetryDataFragment = {
|
||||
mimeType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
path: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
reason?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
whitelistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
type TelemetryData = {
|
||||
mimeType: string;
|
||||
ext: string;
|
||||
path: number;
|
||||
reason?: number;
|
||||
whitelistedjson?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
|
||||
*/
|
||||
@@ -56,10 +70,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; }
|
||||
|
||||
private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
|
||||
get onDidContentChange(): Event<StateChange> { return this._onDidContentChange.event; }
|
||||
readonly onDidContentChange: Event<StateChange> = this._onDidContentChange.event;
|
||||
|
||||
private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
|
||||
get onDidStateChange(): Event<StateChange> { return this._onDidStateChange.event; }
|
||||
readonly onDidStateChange: Event<StateChange> = this._onDidStateChange.event;
|
||||
|
||||
private resource: URI;
|
||||
|
||||
@@ -76,7 +90,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
private autoSaveAfterMillies?: number;
|
||||
private autoSaveAfterMilliesEnabled: boolean;
|
||||
private autoSaveDisposable?: IDisposable;
|
||||
private readonly autoSaveDisposable = this._register(new MutableDisposable());
|
||||
|
||||
private saveSequentializer: SaveSequentializer;
|
||||
private lastSaveAttemptTime: number;
|
||||
@@ -144,7 +158,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
private async onFileChanges(e: FileChangesEvent): Promise<void> {
|
||||
let fileEventImpactsModel = false;
|
||||
let newInOrphanModeGuess: boolean | undefined;
|
||||
|
||||
@@ -167,28 +181,25 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) {
|
||||
let checkOrphanedPromise: Promise<boolean>;
|
||||
let newInOrphanModeValidated: boolean = false;
|
||||
if (newInOrphanModeGuess) {
|
||||
// We have received reports of users seeing delete events even though the file still
|
||||
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
|
||||
// Since we do not want to mark the model as orphaned, we have to check if the
|
||||
// file is really gone and not just a faulty file event.
|
||||
checkOrphanedPromise = timeout(100).then(() => {
|
||||
if (this.disposed) {
|
||||
return true;
|
||||
}
|
||||
await timeout(100);
|
||||
|
||||
return this.fileService.exists(this.resource).then(exists => !exists);
|
||||
});
|
||||
} else {
|
||||
checkOrphanedPromise = Promise.resolve(false);
|
||||
if (this.disposed) {
|
||||
newInOrphanModeValidated = true;
|
||||
} else {
|
||||
const exists = await this.fileService.exists(this.resource);
|
||||
newInOrphanModeValidated = !exists;
|
||||
}
|
||||
}
|
||||
|
||||
checkOrphanedPromise.then(newInOrphanModeValidated => {
|
||||
if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
|
||||
this.setOrphaned(newInOrphanModeValidated);
|
||||
}
|
||||
});
|
||||
if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
|
||||
this.setOrphaned(newInOrphanModeValidated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,40 +250,38 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
return this.backupFileService.backupResource<IBackupMetaData>(target, this.createSnapshot(), this.versionId, meta);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
hasBackup(): boolean {
|
||||
return this.backupFileService.hasBackupSync(this.resource, this.versionId);
|
||||
}
|
||||
|
||||
async revert(soft?: boolean): Promise<void> {
|
||||
if (!this.isResolved()) {
|
||||
return Promise.resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any running auto-save
|
||||
this.cancelPendingAutoSave();
|
||||
this.autoSaveDisposable.clear();
|
||||
|
||||
// Unset flags
|
||||
const undo = this.setDirty(false);
|
||||
|
||||
let loadPromise: Promise<unknown>;
|
||||
if (soft) {
|
||||
loadPromise = Promise.resolve();
|
||||
} else {
|
||||
loadPromise = this.load({ forceReadFromDisk: true });
|
||||
// Force read from disk unless reverting soft
|
||||
if (!soft) {
|
||||
try {
|
||||
await this.load({ forceReadFromDisk: true });
|
||||
} catch (error) {
|
||||
|
||||
// Set flags back to previous values, we are still dirty if revert failed
|
||||
undo();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await loadPromise;
|
||||
|
||||
// Emit file change event
|
||||
this._onDidStateChange.fire(StateChange.REVERTED);
|
||||
} catch (error) {
|
||||
|
||||
// Set flags back to previous values, we are still dirty if revert failed
|
||||
undo();
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// Emit file change event
|
||||
this._onDidStateChange.fire(StateChange.REVERTED);
|
||||
}
|
||||
|
||||
async load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
|
||||
@@ -437,21 +446,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
|
||||
const settingsType = this.getTypeIfSettings();
|
||||
if (settingsType) {
|
||||
/* __GDPR__
|
||||
"settingsRead" : {
|
||||
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
|
||||
type SettingsReadClassification = {
|
||||
settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
|
||||
} else {
|
||||
/* __GDPR__
|
||||
"fileGet" : {
|
||||
"${include}": [
|
||||
"${FileTelemetryData}"
|
||||
]
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
|
||||
type FileGetClassification = {} & FileTelemetryDataFragment;
|
||||
|
||||
this.telemetryService.publicLog2<TelemetryData, FileGetClassification>('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -577,7 +580,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
|
||||
|
||||
// Cancel any currently running auto saves to make this the one that succeeds
|
||||
this.cancelPendingAutoSave();
|
||||
this.autoSaveDisposable.clear();
|
||||
|
||||
// Create new save timer and store it for disposal as needed
|
||||
const handle = setTimeout(() => {
|
||||
@@ -588,25 +591,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
}, this.autoSaveAfterMillies);
|
||||
|
||||
this.autoSaveDisposable = toDisposable(() => clearTimeout(handle));
|
||||
this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle));
|
||||
}
|
||||
|
||||
private cancelPendingAutoSave(): void {
|
||||
if (this.autoSaveDisposable) {
|
||||
this.autoSaveDisposable.dispose();
|
||||
this.autoSaveDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
save(options: ISaveOptions = Object.create(null)): Promise<void> {
|
||||
async save(options: ISaveOptions = Object.create(null)): Promise<void> {
|
||||
if (!this.isResolved()) {
|
||||
return Promise.resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('save() - enter', this.resource);
|
||||
|
||||
// Cancel any currently running auto saves to make this the one that succeeds
|
||||
this.cancelPendingAutoSave();
|
||||
this.autoSaveDisposable.clear();
|
||||
|
||||
return this.doSave(this.versionId, options);
|
||||
}
|
||||
@@ -626,7 +622,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
if (this.saveSequentializer.hasPendingSave(versionId)) {
|
||||
this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource);
|
||||
|
||||
return this.saveSequentializer.pendingSave || Promise.resolve(undefined);
|
||||
return this.saveSequentializer.pendingSave || Promise.resolve();
|
||||
}
|
||||
|
||||
// Return early if not dirty (unless forced) or version changed meanwhile
|
||||
@@ -639,7 +635,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
if ((!options.force && !this.dirty) || versionId !== this.versionId) {
|
||||
this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource);
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Return if currently saving by storing this save request as the next save that should happen.
|
||||
@@ -750,21 +746,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Telemetry
|
||||
const settingsType = this.getTypeIfSettings();
|
||||
if (settingsType) {
|
||||
/* __GDPR__
|
||||
"settingsWritten" : {
|
||||
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
|
||||
type SettingsWrittenClassification = {
|
||||
settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
|
||||
} else {
|
||||
/* __GDPR__
|
||||
"filePUT" : {
|
||||
"${include}": [
|
||||
"${FileTelemetryData}"
|
||||
]
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
|
||||
type FilePutClassfication = {} & FileTelemetryDataFragment;
|
||||
this.telemetryService.publicLog2<TelemetryData, FilePutClassfication>('filePUT', this.getTelemetryData(options.reason));
|
||||
}
|
||||
}, error => {
|
||||
this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
|
||||
@@ -792,22 +780,22 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
// Check for global settings file
|
||||
if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) {
|
||||
if (isEqual(this.resource, this.environmentService.settingsResource)) {
|
||||
return 'global-settings';
|
||||
}
|
||||
|
||||
// Check for keybindings file
|
||||
if (isEqual(this.resource, URI.file(this.environmentService.appKeybindingsPath), !isLinux)) {
|
||||
if (isEqual(this.resource, this.environmentService.keybindingsResource)) {
|
||||
return 'keybindings';
|
||||
}
|
||||
|
||||
// Check for locale file
|
||||
if (isEqual(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
|
||||
if (isEqual(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'locale.json'))) {
|
||||
return 'locale';
|
||||
}
|
||||
|
||||
// Check for snippets
|
||||
if (isEqualOrParent(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'snippets')))) {
|
||||
if (isEqualOrParent(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) {
|
||||
return 'snippets';
|
||||
}
|
||||
|
||||
@@ -827,30 +815,22 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
return '';
|
||||
}
|
||||
|
||||
private getTelemetryData(reason: number | undefined): object {
|
||||
private getTelemetryData(reason: number | undefined): TelemetryData {
|
||||
const ext = extname(this.resource);
|
||||
const fileName = basename(this.resource);
|
||||
const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path;
|
||||
const telemetryData = {
|
||||
mimeType: guessMimeTypes(path).join(', '),
|
||||
mimeType: guessMimeTypes(this.resource).join(', '),
|
||||
ext,
|
||||
path: hash(path),
|
||||
reason
|
||||
reason,
|
||||
whitelistedjson: undefined as string | undefined
|
||||
};
|
||||
|
||||
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
|
||||
telemetryData['whitelistedjson'] = fileName;
|
||||
}
|
||||
|
||||
/* __GDPR__FRAGMENT__
|
||||
"FileTelemetryData" : {
|
||||
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
return telemetryData;
|
||||
}
|
||||
|
||||
@@ -1050,8 +1030,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
this.inOrphanMode = false;
|
||||
this.inErrorMode = false;
|
||||
|
||||
this.cancelPendingAutoSave();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,28 +15,28 @@ import { ResourceMap } from 'vs/base/common/map';
|
||||
export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager {
|
||||
|
||||
private readonly _onModelDisposed: Emitter<URI> = this._register(new Emitter<URI>());
|
||||
get onModelDisposed(): Event<URI> { return this._onModelDisposed.event; }
|
||||
readonly onModelDisposed: Event<URI> = this._onModelDisposed.event;
|
||||
|
||||
private readonly _onModelContentChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelContentChanged(): Event<TextFileModelChangeEvent> { return this._onModelContentChanged.event; }
|
||||
readonly onModelContentChanged: Event<TextFileModelChangeEvent> = this._onModelContentChanged.event;
|
||||
|
||||
private readonly _onModelDirty: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelDirty(): Event<TextFileModelChangeEvent> { return this._onModelDirty.event; }
|
||||
readonly onModelDirty: Event<TextFileModelChangeEvent> = this._onModelDirty.event;
|
||||
|
||||
private readonly _onModelSaveError: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelSaveError(): Event<TextFileModelChangeEvent> { return this._onModelSaveError.event; }
|
||||
readonly onModelSaveError: Event<TextFileModelChangeEvent> = this._onModelSaveError.event;
|
||||
|
||||
private readonly _onModelSaved: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelSaved(): Event<TextFileModelChangeEvent> { return this._onModelSaved.event; }
|
||||
readonly onModelSaved: Event<TextFileModelChangeEvent> = this._onModelSaved.event;
|
||||
|
||||
private readonly _onModelReverted: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelReverted(): Event<TextFileModelChangeEvent> { return this._onModelReverted.event; }
|
||||
readonly onModelReverted: Event<TextFileModelChangeEvent> = this._onModelReverted.event;
|
||||
|
||||
private readonly _onModelEncodingChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelEncodingChanged(): Event<TextFileModelChangeEvent> { return this._onModelEncodingChanged.event; }
|
||||
readonly onModelEncodingChanged: Event<TextFileModelChangeEvent> = this._onModelEncodingChanged.event;
|
||||
|
||||
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
|
||||
get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> { return this._onModelOrphanedChanged.event; }
|
||||
readonly onModelOrphanedChanged: Event<TextFileModelChangeEvent> = this._onModelOrphanedChanged.event;
|
||||
|
||||
private _onModelsDirtyEvent: Event<TextFileModelChangeEvent[]>;
|
||||
private _onModelsSaveError: Event<TextFileModelChangeEvent[]>;
|
||||
|
||||
@@ -31,7 +31,6 @@ import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
@@ -50,13 +49,13 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
|
||||
get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> { return this._onAutoSaveConfigurationChange.event; }
|
||||
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration> = this._onAutoSaveConfigurationChange.event;
|
||||
|
||||
private readonly _onFilesAssociationChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
get onFilesAssociationChange(): Event<void> { return this._onFilesAssociationChange.event; }
|
||||
readonly onFilesAssociationChange: Event<void> = this._onFilesAssociationChange.event;
|
||||
|
||||
private readonly _onWillMove = this._register(new Emitter<IWillMoveEvent>());
|
||||
get onWillMove(): Event<IWillMoveEvent> { return this._onWillMove.event; }
|
||||
readonly onWillMove: Event<IWillMoveEvent> = this._onWillMove.event;
|
||||
|
||||
private _models: TextFileEditorModelManager;
|
||||
get models(): ITextFileEditorModelManager { return this._models; }
|
||||
@@ -73,7 +72,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
|
||||
@IUntitledEditorService protected readonly untitledEditorService: IUntitledEditorService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
@IInstantiationService protected instantiationService: IInstantiationService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@@ -119,7 +118,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
}));
|
||||
}
|
||||
|
||||
private beforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
protected beforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
|
||||
// Dirty files need treatment on shutdown
|
||||
const dirty = this.getDirty();
|
||||
@@ -152,7 +151,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
|
||||
// If hot exit is enabled, backup dirty files and allow to exit without confirmation
|
||||
if (this.isHotExitEnabled) {
|
||||
return this.backupBeforeShutdown(dirty, this.models, reason).then(didBackup => {
|
||||
return this.backupBeforeShutdown(dirty, reason).then(didBackup => {
|
||||
if (didBackup) {
|
||||
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
|
||||
}
|
||||
@@ -171,7 +170,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
return this.confirmBeforeShutdown();
|
||||
}
|
||||
|
||||
private async backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise<boolean> {
|
||||
private async backupBeforeShutdown(dirtyToBackup: URI[], reason: ShutdownReason): Promise<boolean> {
|
||||
const windowCount = await this.windowsService.getWindowCount();
|
||||
|
||||
// When quit is requested skip the confirm callback and attempt to backup all workspaces.
|
||||
@@ -212,24 +211,24 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.backupAll(dirtyToBackup, textFileEditorModelManager);
|
||||
await this.backupAll(dirtyToBackup);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): Promise<void> {
|
||||
private backupAll(dirtyToBackup: URI[]): Promise<void> {
|
||||
|
||||
// split up between files and untitled
|
||||
const filesToBackup: ITextFileEditorModel[] = [];
|
||||
const untitledToBackup: URI[] = [];
|
||||
dirtyToBackup.forEach(s => {
|
||||
if (this.fileService.canHandleResource(s)) {
|
||||
const model = textFileEditorModelManager.get(s);
|
||||
dirtyToBackup.forEach(dirty => {
|
||||
if (this.fileService.canHandleResource(dirty)) {
|
||||
const model = this.models.get(dirty);
|
||||
if (model) {
|
||||
filesToBackup.push(model);
|
||||
}
|
||||
} else if (s.scheme === Schemas.untitled) {
|
||||
untitledToBackup.push(s);
|
||||
} else if (dirty.scheme === Schemas.untitled) {
|
||||
untitledToBackup.push(dirty);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -436,14 +435,14 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
}
|
||||
|
||||
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */));
|
||||
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
|
||||
|
||||
await this.revertAll(dirtyFiles, { soft: true });
|
||||
|
||||
return this.fileService.del(resource, options);
|
||||
}
|
||||
|
||||
async move(source: URI, target: URI, overwrite?: boolean): Promise<void> {
|
||||
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
|
||||
const waitForPromises: Promise<unknown>[] = [];
|
||||
|
||||
// Event
|
||||
@@ -467,7 +466,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
}
|
||||
|
||||
// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
|
||||
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
|
||||
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source));
|
||||
const dirtyTargetModelUris: URI[] = [];
|
||||
if (dirtySourceModels.length) {
|
||||
await Promise.all(dirtySourceModels.map(async sourceModel => {
|
||||
@@ -475,7 +474,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
let targetModelResource: URI;
|
||||
|
||||
// If the source is the actual model, just use target as new resource
|
||||
if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) {
|
||||
if (isEqual(sourceModelResource, source)) {
|
||||
targetModelResource = target;
|
||||
}
|
||||
|
||||
@@ -498,10 +497,12 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
|
||||
// Rename to target
|
||||
try {
|
||||
await this.fileService.move(source, target, overwrite);
|
||||
const stat = await this.fileService.move(source, target, overwrite);
|
||||
|
||||
// Load models that were dirty before
|
||||
await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel)));
|
||||
|
||||
return stat;
|
||||
} catch (error) {
|
||||
|
||||
// In case of an error, discard any dirty target backups that were made
|
||||
@@ -536,7 +537,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
|
||||
async confirmSave(resources?: URI[]): Promise<ConfirmResult> {
|
||||
if (this.environmentService.isExtensionDevelopment) {
|
||||
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
|
||||
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
|
||||
}
|
||||
|
||||
const resourcesToConfirm = this.getDirty(resources);
|
||||
@@ -591,11 +592,11 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
// split up between files and untitled
|
||||
const filesToSave: URI[] = [];
|
||||
const untitledToSave: URI[] = [];
|
||||
toSave.forEach(s => {
|
||||
if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === Schemas.untitled) {
|
||||
untitledToSave.push(s);
|
||||
toSave.forEach(resourceToSave => {
|
||||
if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && resourceToSave.scheme === Schemas.untitled) {
|
||||
untitledToSave.push(resourceToSave);
|
||||
} else {
|
||||
filesToSave.push(s);
|
||||
filesToSave.push(resourceToSave);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -646,18 +647,19 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async promptForPath(resource: URI, defaultUri: URI): Promise<URI | undefined> {
|
||||
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
|
||||
|
||||
// Help user to find a name for the file by opening it first
|
||||
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } });
|
||||
|
||||
return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri));
|
||||
return this.fileDialogService.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems));
|
||||
}
|
||||
|
||||
private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions {
|
||||
private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {
|
||||
const options: ISaveDialogOptions = {
|
||||
defaultUri,
|
||||
title: nls.localize('saveAsTitle', "Save As")
|
||||
title: nls.localize('saveAsTitle', "Save As"),
|
||||
availableFileSystems,
|
||||
};
|
||||
|
||||
// Filters are only enabled on Windows where they work properly
|
||||
@@ -763,7 +765,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
dialogPath = this.suggestFileName(resource);
|
||||
}
|
||||
|
||||
targetResource = await this.promptForPath(resource, dialogPath);
|
||||
targetResource = await this.promptForPath(resource, dialogPath, options ? options.availableFileSystems : undefined);
|
||||
}
|
||||
|
||||
if (!targetResource) {
|
||||
@@ -854,14 +856,15 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
return false;
|
||||
}
|
||||
|
||||
// take over encoding, mode and model value from source model
|
||||
// take over encoding, mode (only if more specific) and model value from source model
|
||||
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
|
||||
if (sourceModel.isResolved() && targetModel.isResolved()) {
|
||||
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
|
||||
|
||||
const mode = sourceModel.textEditorModel.getLanguageIdentifier();
|
||||
if (mode.language !== PLAINTEXT_MODE_ID) {
|
||||
targetModel.textEditorModel.setMode(mode); // only use if more specific than plain/text
|
||||
const sourceMode = sourceModel.textEditorModel.getLanguageIdentifier();
|
||||
const targetMode = targetModel.textEditorModel.getLanguageIdentifier();
|
||||
if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) {
|
||||
targetModel.textEditorModel.setMode(sourceMode); // only use if more specific than plain/text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,7 +904,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
return joinPath(lastActiveFolder, untitledFileName);
|
||||
}
|
||||
|
||||
return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: posix.sep + untitledFileName });
|
||||
return untitledResource.with({ path: untitledFileName });
|
||||
}
|
||||
|
||||
async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface ITextFileService extends IDisposable {
|
||||
/**
|
||||
* Move a file. If the file is dirty, its contents will be preserved and restored.
|
||||
*/
|
||||
move(source: URI, target: URI, overwrite?: boolean): Promise<void>;
|
||||
move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
|
||||
|
||||
/**
|
||||
* Brings up the confirm dialog to either save, don't save or cancel.
|
||||
@@ -136,12 +136,12 @@ export interface ITextFileService extends IDisposable {
|
||||
confirmSave(resources?: URI[]): Promise<ConfirmResult>;
|
||||
|
||||
/**
|
||||
* Convinient fast access to the current auto save mode.
|
||||
* Convenient fast access to the current auto save mode.
|
||||
*/
|
||||
getAutoSaveMode(): AutoSaveMode;
|
||||
|
||||
/**
|
||||
* Convinient fast access to the raw configured auto save settings.
|
||||
* Convenient fast access to the raw configured auto save settings.
|
||||
*/
|
||||
getAutoSaveConfiguration(): IAutoSaveConfiguration;
|
||||
}
|
||||
@@ -428,6 +428,7 @@ export interface ISaveOptions {
|
||||
overwriteEncoding?: boolean;
|
||||
skipSaveParticipants?: boolean;
|
||||
writeElevated?: boolean;
|
||||
availableFileSystems?: string[];
|
||||
}
|
||||
|
||||
export interface ILoadOptions {
|
||||
@@ -467,6 +468,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
|
||||
|
||||
backup(target?: URI): Promise<void>;
|
||||
|
||||
hasBackup(): boolean;
|
||||
|
||||
isDirty(): boolean;
|
||||
|
||||
isResolved(): this is IResolvedTextFileEditorModel;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IFileStatWithMetadata, ICreateFileOptions, FileOperationError, FileOper
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { exists, stat, chmod, rimraf, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs';
|
||||
import { join, dirname } from 'vs/base/common/path';
|
||||
import { isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
@@ -390,7 +390,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
|
||||
const defaultEncodingOverrides: IEncodingOverride[] = [];
|
||||
|
||||
// Global settings
|
||||
defaultEncodingOverrides.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: UTF8 });
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 });
|
||||
|
||||
// Workspace files
|
||||
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
|
||||
@@ -490,7 +490,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
|
||||
for (const override of this.encodingOverrides) {
|
||||
|
||||
// check if the resource is child of encoding override path
|
||||
if (override.parent && isEqualOrParent(resource, override.parent, !isLinux /* ignorecase */)) {
|
||||
if (override.parent && isEqualOrParent(resource, override.parent)) {
|
||||
return override.encoding;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { tmpdir } from 'os';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join, basename } from 'vs/base/common/path';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
@@ -76,7 +76,7 @@ suite('Files - TextFileService i/o', () => {
|
||||
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice');
|
||||
|
||||
let accessor: ServiceAccessor;
|
||||
let disposables: IDisposable[] = [];
|
||||
const disposables = new DisposableStore();
|
||||
let service: ITextFileService;
|
||||
let testDir: string;
|
||||
|
||||
@@ -88,8 +88,8 @@ suite('Files - TextFileService i/o', () => {
|
||||
const fileService = new FileService(logService);
|
||||
|
||||
const fileProvider = new DiskFileSystemProvider(logService);
|
||||
disposables.push(fileService.registerProvider(Schemas.file, fileProvider));
|
||||
disposables.push(fileProvider);
|
||||
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
|
||||
disposables.add(fileProvider);
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.set(IFileService, fileService);
|
||||
@@ -108,7 +108,7 @@ suite('Files - TextFileService i/o', () => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledEditorService.revertAll();
|
||||
|
||||
disposables = dispose(disposables);
|
||||
disposables.clear();
|
||||
|
||||
await rimraf(parentDir, RimRafMode.MOVE);
|
||||
});
|
||||
@@ -247,7 +247,10 @@ suite('Files - TextFileService i/o', () => {
|
||||
}
|
||||
|
||||
test('write - use encoding (cp1252)', async () => {
|
||||
await testEncodingKeepsData(URI.file(join(testDir, 'some_cp1252.txt')), 'cp1252', ['ObjectCount = LoadObjects("Öffentlicher Ordner");', '', 'Private = "Persönliche Information"', ''].join(isWindows ? '\r\n' : '\n'));
|
||||
const filePath = join(testDir, 'some_cp1252.txt');
|
||||
const contents = await readFile(filePath, 'utf8');
|
||||
const eol = /\r\n/.test(contents) ? '\r\n' : '\n';
|
||||
await testEncodingKeepsData(URI.file(filePath), 'cp1252', ['ObjectCount = LoadObjects("Öffentlicher Ordner");', '', 'Private = "Persönliche Information"', ''].join(eol));
|
||||
});
|
||||
|
||||
test('write - use encoding (shiftjis)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user