Skip to content

Commit

Permalink
💄 file on disk provider
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed May 15, 2019
1 parent f642a83 commit 5b8b82c
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 67 deletions.
Expand Up @@ -392,7 +392,7 @@ export class DebugService implements IDebugService {
return this.showError(err.message).then(() => false);
}
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved on disk and that you have a debug extension installed for that file type."))
return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type."))
.then(() => false);
}

Expand Down
Expand Up @@ -160,11 +160,11 @@ function appendEditorTitleContextMenuItem(id: string, title: string, when: Conte
}

// Editor Title Menu for Conflict Resolution
appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), {
appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite file contents"), {
light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check.svg`)),
dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check-inverse.svg`))
}, -10, acceptLocalChangesCommand);
appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), {
appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to file contents"), {
light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo.svg`)),
dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo-inverse.svg`))
}, -9, revertLocalChangesCommand);
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/files/browser/fileCommands.ts
Expand Up @@ -10,7 +10,7 @@ import { IWindowsService, IWindowService, IURIToOpen, IOpenSettings, INewWindowO
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExplorerFocusCondition, FileOnDiskContentProvider, VIEWLET_ID, IExplorerService, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files';
import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
Expand Down Expand Up @@ -319,7 +319,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
if (providerDisposables.length === 0) {
registerEditorListener = true;

const provider = instantiationService.createInstance(FileOnDiskContentProvider);
const provider = instantiationService.createInstance(TextFileContentProvider);
providerDisposables.push(provider);
providerDisposables.push(textModelService.registerTextModelContentProvider(COMPARE_WITH_SAVED_SCHEMA, provider));
}
Expand All @@ -328,9 +328,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
const uri = getResourceForCommand(resource, accessor.get(IListService), editorService);
if (uri && fileService.canHandleResource(uri)) {
const name = basename(uri);
const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name);
const editorLabel = nls.localize('modifiedLabel', "{0} (in file) ↔ {1}", name, name);

editorService.openEditor({ leftResource: resourceToFileOnDisk(COMPARE_WITH_SAVED_SCHEMA, uri), rightResource: uri, label: editorLabel }).then(() => {
TextFileContentProvider.open(uri, COMPARE_WITH_SAVED_SCHEMA, editorLabel, editorService).then(() => {

// Dispose once no more diff editor is opened with the scheme
if (registerEditorListener) {
Expand Down
21 changes: 7 additions & 14 deletions src/vs/workbench/contrib/files/browser/saveErrorHandler.ts
Expand Up @@ -19,7 +19,7 @@ import { ResourceMap } from 'vs/base/common/map';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files';
import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { IModelService } from 'vs/editor/common/services/modelService';
import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands';
Expand All @@ -39,7 +39,7 @@ export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution';

const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError';

const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content on disk with your changes.");
const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes.");

// A handler for save error happening with conflict resolution actions
export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution {
Expand All @@ -61,7 +61,7 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I
this.messages = new ResourceMap<INotificationHandle>();
this.conflictResolutionContext = new RawContextKey<boolean>(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService);

const provider = this._register(instantiationService.createInstance(FileOnDiskContentProvider));
const provider = this._register(instantiationService.createInstance(TextFileContentProvider));
this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider));

// Hook into model
Expand Down Expand Up @@ -125,7 +125,7 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I

// Otherwise show the message that will lead the user into the save conflict editor.
else {
message = nls.localize('staleSaveError', "Failed to save '{0}': The content on disk is newer. Please compare your version with the one on disk.", basename(resource));
message = nls.localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents.", basename(resource));

actions.primary!.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model));
}
Expand Down Expand Up @@ -244,16 +244,9 @@ class ResolveSaveConflictAction extends Action {
if (!this.model.isDisposed()) {
const resource = this.model.getResource();
const name = basename(resource);
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);

return this.editorService.openEditor(
{
leftResource: resourceToFileOnDisk(CONFLICT_RESOLUTION_SCHEME, resource),
rightResource: resource,
label: editorLabel,
options: { pinned: true }
}
).then(() => {
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);

return TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }).then(() => {
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
return; // return if this message is ignored
}
Expand Down
Expand Up @@ -219,7 +219,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
private decorateLabel(label: string): string {
const model = this.textFileService.models.get(this.resource);
if (model && model.hasState(ModelState.ORPHAN)) {
return localize('orphanedFile', "{0} (deleted from disk)", label);
return localize('orphanedFile', "{0} (deleted)", label);
}

if (model && model.isReadonly()) {
Expand Down
50 changes: 31 additions & 19 deletions src/vs/workbench/contrib/files/common/files.ts
Expand Up @@ -23,6 +23,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
import { once } from 'vs/base/common/functional';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

/**
* Explorer viewlet id.
Expand Down Expand Up @@ -58,8 +60,8 @@ export interface IExplorerService {
isCut(stat: ExplorerItem): boolean;

/**
* Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to
* resolve the path from the disk in case the explorer is not yet expanded to the file yet.
* Selects and reveal the file element provided by the given resource if its found in the explorer.
* Will try to resolve the path in case the explorer is not yet expanded to the file yet.
*/
select(resource: URI, reveal?: boolean): Promise<void>;
}
Expand Down Expand Up @@ -131,32 +133,42 @@ export const SortOrderConfiguration = {

export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified';

export function resourceToFileOnDisk(scheme: string, resource: URI): URI {
return resource.with({ scheme, query: JSON.stringify({ scheme: resource.scheme }) });
}

export function fileOnDiskToResource(resource: URI): URI {
return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null });
}

export class FileOnDiskContentProvider implements ITextModelContentProvider {
export class TextFileContentProvider implements ITextModelContentProvider {
private fileWatcherDisposable: IDisposable | undefined;

constructor(
@ITextFileService private readonly textFileService: ITextFileService,
@IFileService private readonly fileService: IFileService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService
) {
) { }

static open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise<void> {
return editorService.openEditor(
{
leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource),
rightResource: resource,
label,
options
}
).then();
}

private static resourceToTextFile(scheme: string, resource: URI): URI {
return resource.with({ scheme, query: JSON.stringify({ scheme: resource.scheme }) });
}

private static textFileToResource(resource: URI): URI {
return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null });
}

provideTextContent(resource: URI): Promise<ITextModel> {
const savedFileResource = fileOnDiskToResource(resource);
const savedFileResource = TextFileContentProvider.textFileToResource(resource);

// Make sure our file from disk is resolved up to date
// Make sure our text file is resolved up to date
return this.resolveEditorModel(resource).then(codeEditorModel => {

// Make sure to keep contents on disk up to date when it changes
// Make sure to keep contents up to date when it changes
if (!this.fileWatcherDisposable) {
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
if (changes.contains(savedFileResource, FileChangeType.UPDATED)) {
Expand All @@ -179,18 +191,18 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider {
private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise<ITextModel>;
private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise<ITextModel | null>;
private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise<ITextModel | null> {
const savedFileResource = fileOnDiskToResource(resource);
const savedFileResource = TextFileContentProvider.textFileToResource(resource);

return this.textFileService.readStream(savedFileResource).then(content => {
let codeEditorModel = this.modelService.getModel(resource);
if (codeEditorModel) {
this.modelService.updateModel(codeEditorModel, content.value);
} else if (createAsNeeded) {
const fileOnDiskModel = this.modelService.getModel(savedFileResource);
const textFileModel = this.modelService.getModel(savedFileResource);

let languageSelector: ILanguageSelection;
if (fileOnDiskModel) {
languageSelector = this.modeService.create(fileOnDiskModel.getModeId());
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.fsPath);
}
Expand Down
Expand Up @@ -7,7 +7,7 @@ import * as assert from 'assert';
import { URI } from 'vs/base/common/uri';
import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files';
import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';

Expand All @@ -29,10 +29,10 @@ suite('Files - FileOnDiskContentProvider', () => {
});

test('provideTextContent', async () => {
const provider = instantiationService.createInstance(FileOnDiskContentProvider);
const provider = instantiationService.createInstance(TextFileContentProvider);
const uri = URI.parse('testFileOnDiskContentProvider://foo');

const content = await provider.provideTextContent(resourceToFileOnDisk('conflictResolution', uri));
const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) }));

assert.equal(snapshotToString(content.createSnapshot()), 'Hello Html');
assert.equal(accessor.fileService.getLastReadFileUri().toString(), uri.toString());
Expand Down

0 comments on commit 5b8b82c

Please sign in to comment.