diff --git a/extensions/vscode-api-tests/src/memfs.ts b/extensions/vscode-api-tests/src/memfs.ts index 2b4a5f751ab..1b4b1e5adf7 100644 --- a/extensions/vscode-api-tests/src/memfs.ts +++ b/extensions/vscode-api-tests/src/memfs.ts @@ -48,9 +48,12 @@ class Directory implements vscode.FileStat { export type Entry = File | Directory; -export class MemFS implements vscode.FileSystemProvider { +export class TestFS implements vscode.FileSystemProvider { - readonly scheme = 'fake-fs'; + constructor( + readonly scheme: string, + readonly isCaseSensitive: boolean + ) { } readonly root = new Directory(''); @@ -161,12 +164,22 @@ export class MemFS implements vscode.FileSystemProvider { let parts = uri.path.split('/'); let entry: Entry = this.root; for (const part of parts) { + const partLow = part.toLowerCase(); if (!part) { continue; } let child: Entry | undefined; if (entry instanceof Directory) { - child = entry.entries.get(part); + if (this.isCaseSensitive) { + child = entry.entries.get(part); + } else { + for (let [key, value] of entry.entries) { + if (key.toLowerCase() === partLow) { + child = value; + break; + } + } + } } if (!child) { if (!silent) { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 2a58c4630e0..ff6a2418f4d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName, disposeAll, testFs, delay, withLogDisabled } from '../utils'; import { join, posix, basename } from 'path'; import * as fs from 'fs'; +import { TestFS } from '../memfs'; suite('vscode API - workspace', () => { @@ -163,6 +164,40 @@ suite('vscode API - workspace', () => { }); }); + test('openTextDocument, actual casing first', async function () { + + const fs = new TestFS('this-fs', false); + const reg = vscode.workspace.registerFileSystemProvider(fs.scheme, fs, { isCaseSensitive: fs.isCaseSensitive }); + + let uriOne = vscode.Uri.parse('this-fs:/one'); + let uriTwo = vscode.Uri.parse('this-fs:/two'); + let uriONE = vscode.Uri.parse('this-fs:/ONE'); // same resource, different uri + let uriTWO = vscode.Uri.parse('this-fs:/TWO'); + + fs.writeFile(uriOne, Buffer.from('one'), { create: true, overwrite: true }); + fs.writeFile(uriTwo, Buffer.from('two'), { create: true, overwrite: true }); + + // lower case (actual case) comes first + let docOne = await vscode.workspace.openTextDocument(uriOne); + assert.equal(docOne.uri.toString(), uriOne.toString()); + + let docONE = await vscode.workspace.openTextDocument(uriONE); + assert.equal(docONE === docOne, true); + assert.equal(docONE.uri.toString(), uriOne.toString()); + assert.equal(docONE.uri.toString() !== uriONE.toString(), true); // yep + + // upper case (NOT the actual case) comes first + let docTWO = await vscode.workspace.openTextDocument(uriTWO); + assert.equal(docTWO.uri.toString(), uriTWO.toString()); + + let docTwo = await vscode.workspace.openTextDocument(uriTwo); + assert.equal(docTWO === docTwo, true); + assert.equal(docTwo.uri.toString(), uriTWO.toString()); + assert.equal(docTwo.uri.toString() !== uriTwo.toString(), true); // yep + + reg.dispose(); + }); + test('eol, read', () => { const a = createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index e270cd73adf..3c34028feb4 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { MemFS } from './memfs'; +import { TestFS } from './memfs'; import * as assert from 'assert'; export function rndName() { return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); } -export const testFs = new MemFS(); -vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs); +export const testFs = new TestFS('fake-fs', true); +vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensitive: testFs.isCaseSensitive }); export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { let fakeFile: vscode.Uri; diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index f0138dc77cb..10a3454cc7d 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -16,8 +16,9 @@ import { ExtHostContext, ExtHostDocumentsShape, IExtHostContext, MainThreadDocum import { ITextEditorModel } from 'vs/workbench/common/editor'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { toLocalResource, isEqualOrParent } from 'vs/base/common/resources'; +import { toLocalResource, isEqualOrParent, extUri } from 'vs/base/common/resources'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export class BoundModelReferenceCollection { @@ -78,6 +79,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { private readonly _textFileService: ITextFileService; private readonly _fileService: IFileService; private readonly _environmentService: IWorkbenchEnvironmentService; + private readonly _uriIdentityService: IUriIdentityService; private readonly _toDispose = new DisposableStore(); private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; @@ -93,6 +95,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IUriIdentityService uriIdentityService: IUriIdentityService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService ) { this._modelService = modelService; @@ -100,6 +103,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { this._textFileService = textFileService; this._fileService = fileService; this._environmentService = environmentService; + this._uriIdentityService = uriIdentityService; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); @@ -179,33 +183,37 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { return this._textFileService.save(URI.revive(uri)).then(target => !!target); } - $tryOpenDocument(_uri: UriComponents): Promise { - const uri = URI.revive(_uri); - if (!uri.scheme || !(uri.fsPath || uri.authority)) { + $tryOpenDocument(uriData: UriComponents): Promise { + const inputUri = URI.revive(uriData); + if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) { return Promise.reject(new Error(`Invalid uri. Scheme and authority or path must be set.`)); } - let promise: Promise; - switch (uri.scheme) { + const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri); + + let promise: Promise; + switch (canonicalUri.scheme) { case Schemas.untitled: - promise = this._handleUntitledScheme(uri); + promise = this._handleUntitledScheme(canonicalUri); break; case Schemas.file: default: - promise = this._handleAsResourceInput(uri); + promise = this._handleAsResourceInput(canonicalUri); break; } - return promise.then(success => { - if (!success) { - return Promise.reject(new Error('cannot open ' + uri.toString())); - } else if (!this._modelIsSynced.has(uri.toString())) { - return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: Files above 50MB cannot be synchronized with extensions.')); + return promise.then(documentUri => { + if (!documentUri) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}`)); + } else if (!extUri.isEqual(documentUri, canonicalUri)) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`)); + } else if (!this._modelIsSynced.has(canonicalUri.toString())) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`)); } else { - return undefined; + return canonicalUri; } }, err => { - return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: ' + toErrorMessage(err))); + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`)); }); } @@ -213,21 +221,20 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined); } - private _handleAsResourceInput(uri: URI): Promise { + private _handleAsResourceInput(uri: URI): Promise { return this._textModelResolverService.createModelReference(uri).then(ref => { this._modelReferenceCollection.add(uri, ref); - const result = !!ref.object; - return result; + return ref.object.textEditorModel.uri; }); } - private _handleUntitledScheme(uri: URI): Promise { + private _handleUntitledScheme(uri: URI): Promise { const asLocalUri = toLocalResource(uri, this._environmentService.configuration.remoteAuthority); return this._fileService.resolve(asLocalUri).then(stats => { // don't create a new file ontop of an existing file return Promise.reject(new Error('file already exists')); }, err => { - return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined).then(resource => !!resource); + return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined); }); } diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 5d430ea017d..a8cd6ddfee8 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -28,6 +28,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; namespace delta { @@ -328,11 +329,12 @@ export class MainThreadDocumentsAndEditors { @IBulkEditService bulkEditService: IBulkEditService, @IPanelService panelService: IPanelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); - const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, workingCopyFileService)); + const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService)); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService)); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9ae78a0269c..0097dea20ee 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -715,8 +715,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return uriPromise.then(uri => { - return extHostDocuments.ensureDocumentData(uri).then(() => { - return extHostDocuments.getDocument(uri); + return extHostDocuments.ensureDocumentData(uri).then(documentData => { + return documentData.document; }); }); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ce8e267e10c..b662b07ac07 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -220,7 +220,7 @@ export interface MainThreadDocumentContentProvidersShape extends IDisposable { export interface MainThreadDocumentsShape extends IDisposable { $tryCreateDocument(options?: { language?: string; content?: string; }): Promise; - $tryOpenDocument(uri: UriComponents): Promise; + $tryOpenDocument(uri: UriComponents): Promise; $trySaveDocument(uri: UriComponents): Promise; } diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index 4e8bb4d1252..af4a3f9de52 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -84,9 +84,10 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { let promise = this._documentLoader.get(uri.toString()); if (!promise) { - promise = this._proxy.$tryOpenDocument(uri).then(() => { + promise = this._proxy.$tryOpenDocument(uri).then(uriData => { this._documentLoader.delete(uri.toString()); - return assertIsDefined(this._documentsAndEditors.getDocument(uri)); + const canonicalUri = URI.revive(uriData); + return assertIsDefined(this._documentsAndEditors.getDocument(canonicalUri)); }, err => { this._documentLoader.delete(uri.toString()); return Promise.reject(err); diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 065874bd281..4f4ba509d9a 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -26,6 +26,7 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestTextResourcePropertiesService, TestWorkingCopyFileService } from 'vs/workbench/test/common/workbenchTestServices'; +import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; suite('MainThreadDocumentsAndEditors', () => { @@ -89,7 +90,8 @@ suite('MainThreadDocumentsAndEditors', () => { } }, TestEnvironmentService, - new TestWorkingCopyFileService() + new TestWorkingCopyFileService(), + new UriIdentityService(fileService), ); });