Merge from vscode 0fde6619172c9f04c41f2e816479e432cc974b8b (#5199)

This commit is contained in:
Anthony Dresser
2019-04-24 22:26:02 -07:00
committed by GitHub
parent d63f07d29a
commit 34457880c7
86 changed files with 1254 additions and 702 deletions

View File

@@ -34,6 +34,7 @@ interface FileQuickPickItem extends IQuickPickItem {
enum UpdateResult {
Updated,
Updating,
NotUpdated,
InvalidPath
}
@@ -51,6 +52,7 @@ export class RemoteFileDialog {
private allowFolderSelection: boolean;
private remoteAuthority: string | undefined;
private requiresTrailing: boolean;
private trailing: string | undefined;
private scheme: string = REMOTE_HOST_SCHEME;
private contextKey: IContextKey<boolean>;
private userEnteredPathSegment: string;
@@ -149,7 +151,6 @@ export class RemoteFileDialog {
this.allowFileSelection = !!this.options.canSelectFiles;
this.hidden = false;
let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri;
let trailing: string | undefined;
let stat: IFileStat | undefined;
let ext: string = resources.extname(homedir);
if (this.options.defaultUri) {
@@ -160,14 +161,14 @@ export class RemoteFileDialog {
}
if (!stat || !stat.isDirectory) {
homedir = resources.dirname(this.options.defaultUri);
trailing = resources.basename(this.options.defaultUri);
this.trailing = resources.basename(this.options.defaultUri);
}
// append extension
if (isSave && !ext && this.options.filters) {
for (let i = 0; i < this.options.filters.length; i++) {
if (this.options.filters[i].extensions[0] !== '*') {
ext = '.' + this.options.filters[i].extensions[0];
trailing = trailing ? trailing + ext : ext;
this.trailing = this.trailing ? this.trailing + ext : ext;
break;
}
}
@@ -176,6 +177,7 @@ export class RemoteFileDialog {
return new Promise<URI | undefined>(async (resolve) => {
this.filePickBox = this.quickInputService.createQuickPick<FileQuickPickItem>();
this.filePickBox.busy = true;
this.filePickBox.matchOnLabel = false;
this.filePickBox.autoFocusOnList = false;
this.filePickBox.ignoreFocusOut = true;
@@ -221,6 +223,7 @@ export class RemoteFileDialog {
this.options.availableFileSystems.shift();
}
this.options.defaultUri = undefined;
this.filePickBox.hide();
if (this.requiresTrailing) {
return this.fileDialogService.showSaveDialog(this.options).then(result => {
doResolve(this, result);
@@ -264,7 +267,7 @@ export class RemoteFileDialog {
// onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything
if (this.isChangeFromUser()) {
// If the user has just entered more bad path, don't change anything
if (value !== this.constructFullUserPath() && !this.isBadSubpath(value)) {
if (!equalsIgnoreCase(value, this.constructFullUserPath()) && !this.isBadSubpath(value)) {
this.filePickBox.validationMessage = undefined;
const valueUri = this.remoteUriFrom(this.trimTrailingSlash(this.filePickBox.value));
let updated: UpdateResult = UpdateResult.NotUpdated;
@@ -288,12 +291,13 @@ export class RemoteFileDialog {
this.filePickBox.show();
this.contextKey.set(true);
await this.updateItems(homedir, trailing);
if (trailing) {
this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - ext.length];
await this.updateItems(homedir, this.trailing);
if (this.trailing) {
this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length];
} else {
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
}
this.filePickBox.busy = false;
});
}
@@ -302,7 +306,7 @@ export class RemoteFileDialog {
}
private isChangeFromUser(): boolean {
if ((this.filePickBox.value === this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment))
if (equalsIgnoreCase(this.filePickBox.value, this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment))
&& (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined))) {
return false;
}
@@ -314,6 +318,7 @@ export class RemoteFileDialog {
}
private async onDidAccept(): Promise<URI | undefined> {
this.filePickBox.busy = true;
let resolveValue: URI | undefined;
let navigateValue: URI | undefined;
const trimmedPickBoxValue = ((this.filePickBox.value.length > 1) && this.endsWithSlash(this.filePickBox.value)) ? this.filePickBox.value.substr(0, this.filePickBox.value.length - 1) : this.filePickBox.value;
@@ -351,21 +356,25 @@ export class RemoteFileDialog {
if (resolveValue) {
resolveValue = this.addPostfix(resolveValue);
if (await this.validate(resolveValue)) {
return Promise.resolve(resolveValue);
this.filePickBox.busy = false;
return resolveValue;
}
} else if (navigateValue) {
// Try to navigate into the folder
await this.updateItems(navigateValue);
// Try to navigate into the folder.
await this.updateItems(navigateValue, this.trailing);
} else {
// validation error. Path does not exist.
}
return Promise.resolve(undefined);
this.filePickBox.busy = false;
return undefined;
}
private async tryUpdateItems(value: string, valueUri: URI): Promise<UpdateResult> {
if (value[value.length - 1] === '~') {
await this.updateItems(this.userHome);
if (this.filePickBox.busy) {
this.badPath = undefined;
return UpdateResult.Updating;
} else if (value[value.length - 1] === '~') {
await this.updateItems(this.userHome);
return UpdateResult.Updated;
} else if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) {
let stat: IFileStat | undefined;
@@ -409,7 +418,7 @@ export class RemoteFileDialog {
const inputBasename = resources.basename(this.remoteUriFrom(value));
// Make sure that the folder whose children we are currently viewing matches the path in the input
const userPath = this.constructFullUserPath();
if (userPath === value.substring(0, userPath.length)) {
if (equalsIgnoreCase(userPath, value.substring(0, userPath.length))) {
let hasMatch = false;
for (let i = 0; i < this.filePickBox.items.length; i++) {
const item = <FileQuickPickItem>this.filePickBox.items[i];
@@ -424,7 +433,7 @@ export class RemoteFileDialog {
this.filePickBox.activeItems = [];
}
} else {
if (inputBasename !== resources.basename(this.currentFolder)) {
if (!equalsIgnoreCase(inputBasename, resources.basename(this.currentFolder))) {
this.userEnteredPathSegment = inputBasename;
} else {
this.userEnteredPathSegment = '';
@@ -442,24 +451,34 @@ export class RemoteFileDialog {
}
const itemBasename = quickPickItem.label;
// Either force the autocomplete, or the old value should be one smaller than the new value and match the new value.
if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) {
if (itemBasename === '..') {
// Don't match on the up directory item ever.
this.userEnteredPathSegment = startingValue;
this.autoCompletePathSegment = '';
this.activeItem = quickPickItem;
if (force) {
// clear any selected text
this.insertText(this.userEnteredPathSegment, '');
}
return false;
} else if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) {
this.userEnteredPathSegment = startingBasename;
this.activeItem = quickPickItem;
// Changing the active items will trigger the onDidActiveItemsChanged. Clear the autocomplete first, then set it after.
this.autoCompletePathSegment = '';
this.filePickBox.activeItems = [quickPickItem];
this.autoCompletePathSegment = itemBasename.substr(startingBasename.length);
this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename.substr(startingBasename.length));
this.insertText(startingValue + this.autoCompletePathSegment, this.autoCompletePathSegment);
this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length];
return true;
} else if (force && (quickPickItem.label !== (this.userEnteredPathSegment + this.autoCompletePathSegment))) {
} else if (force && (!equalsIgnoreCase(quickPickItem.label, (this.userEnteredPathSegment + this.autoCompletePathSegment)))) {
this.userEnteredPathSegment = '';
this.autoCompletePathSegment = itemBasename;
this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename);
this.activeItem = quickPickItem;
this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder, true).length, this.filePickBox.value.length];
// use insert text to preserve undo buffer
this.insertText(this.pathAppend(this.currentFolder, itemBasename), itemBasename);
this.filePickBox.valueSelection = [this.filePickBox.value.length - itemBasename.length, this.filePickBox.value.length];
this.insertText(this.pathAppend(this.currentFolder, this.autoCompletePathSegment), this.autoCompletePathSegment);
this.filePickBox.valueSelection = [this.filePickBox.value.length - this.autoCompletePathSegment.length, this.filePickBox.value.length];
return true;
} else {
this.userEnteredPathSegment = startingBasename;
@@ -506,21 +525,24 @@ export class RemoteFileDialog {
return ((path.length > 1) && this.endsWithSlash(path)) ? path.substr(0, path.length - 1) : path;
}
private yesNoPrompt(message: string): Promise<boolean> {
private yesNoPrompt(uri: URI, message: string): Promise<boolean> {
interface YesNoItem extends IQuickPickItem {
value: boolean;
}
const prompt = this.quickInputService.createQuickPick<YesNoItem>();
const no = nls.localize('remoteFileDialog.no', 'No');
prompt.items = [{ label: no, value: false }, { label: nls.localize('remoteFileDialog.yes', 'Yes'), value: true }];
prompt.title = message;
prompt.placeholder = no;
prompt.ignoreFocusOut = true;
prompt.ok = true;
prompt.customButton = true;
prompt.customLabel = nls.localize('remoteFileDialog.cancel', 'Cancel');
prompt.value = this.pathFromUri(uri);
let isResolving = false;
return new Promise<boolean>(resolve => {
prompt.onDidAccept(() => {
isResolving = true;
prompt.hide();
resolve(prompt.selectedItems ? prompt.selectedItems[0].value : false);
resolve(true);
});
prompt.onDidHide(() => {
if (!isResolving) {
@@ -531,6 +553,12 @@ export class RemoteFileDialog {
this.filePickBox.items = this.filePickBox.items;
prompt.dispose();
});
prompt.onDidChangeValue(() => {
prompt.hide();
});
prompt.onDidCustom(() => {
prompt.hide();
});
prompt.show();
});
}
@@ -554,7 +582,7 @@ export class RemoteFileDialog {
// Replacing a file.
// Show a yes/no prompt
const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri));
return this.yesNoPrompt(message);
return this.yesNoPrompt(uri, message);
} else if (!this.isValidBaseName(resources.basename(uri))) {
// Filename not allowed
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.');
@@ -587,15 +615,24 @@ export class RemoteFileDialog {
this.userEnteredPathSegment = trailing ? trailing : '';
this.autoCompletePathSegment = '';
const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true);
this.currentFolder = this.remoteUriFrom(this.pathFromUri(newFolder, true));
const oldFolder = this.currentFolder;
const newFolderPath = this.pathFromUri(newFolder, true);
this.currentFolder = this.remoteUriFrom(newFolderPath);
return this.createItems(this.currentFolder).then(items => {
this.filePickBox.items = items;
if (this.allowFolderSelection) {
this.filePickBox.activeItems = [];
}
if (!equalsIgnoreCase(this.filePickBox.value, newValue)) {
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
this.insertText(newValue, newValue);
// the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory.
if (!equalsIgnoreCase(this.filePickBox.value.substring(0, newValue.length), newValue)) {
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
this.insertText(newValue, newValue);
} else if (equalsIgnoreCase(this.pathFromUri(resources.dirname(oldFolder), true), newFolderPath)) {
// This is the case where the user went up one dir. We need to make sure that we remove the final dir.
this.filePickBox.valueSelection = [newFolderPath.length, this.filePickBox.value.length];
this.insertText(newValue, '');
}
}
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
this.filePickBox.busy = false;

View File

@@ -530,16 +530,11 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Untitled file support
const untitledInput = <IUntitledResourceInput>input;
if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) {
if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
// {{SQL CARBON EDIT}}
let modeId: string = untitledInput.language ? untitledInput.language : getFileMode(this.instantiationService, untitledInput.resource);
return convertEditorInput(this.untitledEditorService.createOrGet(
untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource,
modeId,
untitledInput.contents,
untitledInput.encoding
), undefined, this.instantiationService);
return convertEditorInput(this.untitledEditorService.createOrGet(untitledInput.resource, modeId, untitledInput.contents, untitledInput.encoding), undefined, this.instantiationService);
}
// Resource Editor Support

View File

@@ -8,7 +8,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor';
import { URI } from 'vs/base/common/uri';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor';
import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { workbenchInstantiationService, TestStorageService, NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService';
@@ -27,6 +27,8 @@ import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { timeout } from 'vs/base/common/async';
import { toResource } from 'vs/base/test/common/utils';
import { IFileService } from 'vs/platform/files/common/files';
import { Disposable } from 'vs/base/common/lifecycle';
// {{SQL CARBON EDIT}} - Disable editor tests
/*
@@ -66,8 +68,17 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
this.gotDisposed = true;
}
}
*/
suite('Editor service', () => {/*
class FileServiceProvider extends Disposable {
constructor(scheme: string, @IFileService fileService: IFileService) {
super();
this._register(fileService.registerProvider(scheme, new NullFileSystemProvider()));
}
}
*/suite('Editor service', () => {/*
function registerTestEditorInput(): void {
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput));
}
@@ -250,9 +261,23 @@ suite('Editor service', () => {/*
assert(input instanceof UntitledEditorInput);
// Untyped Input (untitled with file path)
input = service.createInput({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
// Untyped Input (untitled with untitled resource)
input = service.createInput({ resource: URI.parse('untitled://Untitled-1'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok(!(input as UntitledEditorInput).hasAssociatedFilePath);
// Untyped Input (untitled with custom resource)
const provider = instantiationService.createInstance(FileServiceProvider, 'untitled-custom');
input = service.createInput({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
provider.dispose();
});
test('delegate', function (done) {

View File

@@ -481,7 +481,7 @@ function readWindowsCaCertificates() {
}
async function readMacCaCertificates() {
const stdout = (await promisify(cp.execFile)('/usr/bin/security', ['find-certificate', '-a', '-p'], { encoding: 'utf8' })).stdout;
const stdout = (await promisify(cp.execFile)('/usr/bin/security', ['find-certificate', '-a', '-p'], { encoding: 'utf8', maxBuffer: 1024 * 1024 })).stdout;
const seen = {};
const certs = stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g)
.filter(pem => !!pem.length && !seen[pem] && (seen[pem] = true));

View File

@@ -157,7 +157,7 @@ export class FileService extends Disposable implements IFileService {
}
// Bubble up any other error as is
throw error;
throw this.ensureError(error);
}
}
@@ -206,7 +206,7 @@ export class FileService extends Disposable implements IFileService {
isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly),
mtime: stat.mtime,
size: stat.size,
etag: etag(stat.mtime, stat.size)
etag: etag({ mtime: stat.mtime, size: stat.size })
};
// check to recurse for directories
@@ -306,7 +306,7 @@ export class FileService extends Disposable implements IFileService {
await this.doWriteUnbuffered(provider, resource, bufferOrReadable);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file ({0})", error.toString()), toFileOperationResult(error), options);
throw new FileOperationError(localize('err.write', "Unable to write file ({0})", this.ensureError(error).toString()), toFileOperationResult(error), options);
}
return this.resolve(resource, { resolveMetadata: true });
@@ -337,7 +337,10 @@ export class FileService extends Disposable implements IFileService {
// check for size is a weaker check because it can return a false negative if the file has changed
// but to the same length. This is a compromise we take to avoid having to produce checksums of
// the file content for comparison which would be much slower to compute.
if (options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.mtime < stat.mtime && options.etag !== etag(stat.size, options.mtime)) {
if (
options && typeof options.mtime === 'number' && typeof options.etag === 'string' &&
options.etag !== ETAG_DISABLED && options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size })
) {
throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options);
}
@@ -398,7 +401,7 @@ export class FileService extends Disposable implements IFileService {
value: fileStream
};
} catch (error) {
throw new FileOperationError(localize('err.read', "Unable to read file ({0})", error.toString()), toFileOperationResult(error), options);
throw new FileOperationError(localize('err.read', "Unable to read file ({0})", this.ensureError(error).toString()), toFileOperationResult(error), options);
}
}
@@ -462,7 +465,7 @@ export class FileService extends Disposable implements IFileService {
stream.write(buffer.slice(0, lastChunkLength));
}
} catch (error) {
throw error;
throw this.ensureError(error);
} finally {
await provider.close(handle);
}
@@ -492,8 +495,8 @@ export class FileService extends Disposable implements IFileService {
throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
// Return early if file not modified since
if (options && options.etag === stat.etag) {
// Return early if file not modified since (unless disabled)
if (options && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {
throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options);
}
@@ -863,7 +866,7 @@ export class FileService extends Disposable implements IFileService {
posInFile += chunk.byteLength;
}
} catch (error) {
throw error;
throw this.ensureError(error);
} finally {
await provider.close(handle);
}
@@ -929,7 +932,7 @@ export class FileService extends Disposable implements IFileService {
}
} while (bytesRead > 0);
} catch (error) {
throw error;
throw this.ensureError(error);
} finally {
await Promise.all([
typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),
@@ -960,7 +963,7 @@ export class FileService extends Disposable implements IFileService {
const buffer = await sourceProvider.readFile(source);
await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);
} catch (error) {
throw error;
throw this.ensureError(error);
} finally {
await targetProvider.close(targetHandle);
}
@@ -991,6 +994,14 @@ export class FileService extends Disposable implements IFileService {
return true;
}
private ensureError(error?: Error): Error {
if (!error) {
return new Error(localize('unknownError', "Unknown Error")); // https://github.com/Microsoft/vscode/issues/72798
}
return error;
}
private throwIfTooLarge(totalBytesRead: number, options?: IReadFileOptions): boolean {
// Return early if file is too large to load

View File

@@ -355,7 +355,7 @@ suite('Disk File Service', () => {
test('resolve - folder symbolic link', async () => {
if (isWindows) {
return; // not happy
return; // not reliable on windows
}
const link = URI.file(join(testDir, 'deep-link'));
@@ -369,7 +369,7 @@ suite('Disk File Service', () => {
test('resolve - file symbolic link', async () => {
if (isWindows) {
return; // not happy
return; // not reliable on windows
}
const link = URI.file(join(testDir, 'lorem.txt-linked'));
@@ -382,7 +382,7 @@ suite('Disk File Service', () => {
test('resolve - invalid symbolic link does not break', async () => {
if (isWindows) {
return; // not happy
return; // not reliable on windows
}
const link = URI.file(join(testDir, 'foo'));
@@ -824,12 +824,24 @@ suite('Disk File Service', () => {
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - small file - buffered / readonly', () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly);
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - small file - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - small file - unbuffered / readonly', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly);
return testReadFile(URI.file(join(testDir, 'small.txt')));
});
test('readFile - large file - buffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
@@ -1200,6 +1212,26 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile - buffered - readonly throws', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
assert.equal(content, 'Small File');
const newContent = 'Updates to the small file';
let error: Error;
try {
await service.writeFile(resource, VSBuffer.fromString(newContent));
} catch (err) {
error = err;
}
assert.ok(error!);
});
test('writeFile - unbuffered', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
@@ -1228,6 +1260,26 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile - unbuffered - readonly throws', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly);
const resource = URI.file(join(testDir, 'small.txt'));
const content = readFileSync(resource.fsPath);
assert.equal(content, 'Small File');
const newContent = 'Updates to the small file';
let error: Error;
try {
await service.writeFile(resource, VSBuffer.fromString(newContent));
} catch (err) {
error = err;
}
assert.ok(error!);
});
test('writeFile (large file) - multiple parallel writes queue up', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
@@ -1336,20 +1388,25 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (error when writing to file that has been updated meanwhile)', async () => {
test('writeFile - error when writing to file that has been updated meanwhile', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
const stat = await service.resolve(resource);
const content = readFileSync(resource.fsPath);
const content = readFileSync(resource.fsPath).toString();
assert.equal(content, 'Small File');
const newContent = 'Updates to the small file';
await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime });
const newContentLeadingToError = newContent + newContent;
const fakeMtime = 1000;
const fakeSize = 1000;
let error: FileOperationError | undefined = undefined;
try {
await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: etag(0, 0), mtime: 0 });
await service.writeFile(resource, VSBuffer.fromString(newContentLeadingToError), { etag: etag({ mtime: fakeMtime, size: fakeSize }), mtime: fakeMtime });
} catch (err) {
error = err;
}
@@ -1359,6 +1416,32 @@ suite('Disk File Service', () => {
assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
});
test('writeFile - no error when writing to file where size is the same', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
const stat = await service.resolve(resource);
const content = readFileSync(resource.fsPath).toString();
assert.equal(content, 'Small File');
const newContent = content; // same content
await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime });
const newContentLeadingToNoError = newContent; // writing the same content should be OK
const fakeMtime = 1000;
const actualSize = newContent.length;
let error: FileOperationError | undefined = undefined;
try {
await service.writeFile(resource, VSBuffer.fromString(newContentLeadingToNoError), { etag: etag({ mtime: fakeMtime, size: actualSize }), mtime: fakeMtime });
} catch (err) {
error = err;
}
assert.ok(!error);
});
test('watch - file', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
writeFileSync(toWatch.fsPath, 'Init');
@@ -1370,7 +1453,7 @@ suite('Disk File Service', () => {
test('watch - file symbolic link', async done => {
if (isWindows) {
return done(); // not happy
return done(); // watch tests are flaky on other platforms
}
const toWatch = URI.file(join(testDir, 'lorem.txt-linked'));
@@ -1383,7 +1466,7 @@ suite('Disk File Service', () => {
test('watch - file - multiple writes', done => {
if (isWindows) {
return done(); // not happy
return done(); // watch tests are flaky on other platforms
}
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
@@ -1446,6 +1529,10 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - change file', done => {
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch3'));
mkdirSync(watchDir.fsPath);
@@ -1458,6 +1545,10 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - add file', done => {
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch4'));
mkdirSync(watchDir.fsPath);
@@ -1469,6 +1560,10 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - delete file', done => {
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch5'));
mkdirSync(watchDir.fsPath);
@@ -1481,6 +1576,10 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - add folder', done => {
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch6'));
mkdirSync(watchDir.fsPath);
@@ -1492,8 +1591,8 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - delete folder', done => {
if (isWindows) {
return done(); // not happy
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch7'));
@@ -1508,8 +1607,8 @@ suite('Disk File Service', () => {
});
test('watch - folder (non recursive) - symbolic link - change file', async done => {
if (isWindows) {
return done(); // not happy
if (!isLinux) {
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'deep-link'));
@@ -1525,7 +1624,7 @@ suite('Disk File Service', () => {
test('watch - folder (non recursive) - rename file', done => {
if (!isLinux) {
return done(); // not happy
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch8'));
@@ -1543,7 +1642,7 @@ suite('Disk File Service', () => {
test('watch - folder (non recursive) - rename file (different case)', done => {
if (!isLinux) {
return done(); // not happy
return done(); // watch tests are flaky on other platforms
}
const watchDir = URI.file(join(testDir, 'watch8'));

View File

@@ -137,7 +137,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
if (this.defaultSettingsRawResource.toString() === uri.toString()) {
const defaultRawSettingsEditorModel = this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER));
const defaultRawSettingsEditorModel = this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL));
const languageSelection = this.modeService.create('jsonc');
const model = this._register(this.modelService.createModel(defaultRawSettingsEditorModel.content, languageSelection, uri));
return Promise.resolve(model);
@@ -159,7 +159,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
if (this.userSettingsResource.toString() === uri.toString()) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.USER, uri);
return this.createEditableSettingsEditorModel(ConfigurationTarget.USER_LOCAL, uri);
}
const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE);
@@ -209,8 +209,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
jsonEditor;
return jsonEditor ?
this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, group) :
this.openOrSwitchSettings2(ConfigurationTarget.USER, undefined, options, group);
this.openOrSwitchSettings(ConfigurationTarget.USER_LOCAL, this.userSettingsResource, options, group) :
this.openOrSwitchSettings2(ConfigurationTarget.USER_LOCAL, undefined, options, group);
}
async openRemoteSettings(): Promise<IEditor | null> {
@@ -377,7 +377,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
public createSettings2EditorModel(): Settings2EditorModel {
return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER));
return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL));
}
private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Promise<IEditor | null> {
@@ -419,7 +419,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget {
if (this.userSettingsResource.toString() === resource.toString()) {
return ConfigurationTarget.USER;
return ConfigurationTarget.USER_LOCAL;
}
const workspaceSettingsResource = this.workspaceSettingsResource;
@@ -432,11 +432,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return ConfigurationTarget.WORKSPACE_FOLDER;
}
return ConfigurationTarget.USER;
return ConfigurationTarget.USER_LOCAL;
}
private getConfigurationTargetFromDefaultSettingsResource(uri: URI) {
return this.isDefaultWorkspaceSettingsResource(uri) ? ConfigurationTarget.WORKSPACE : this.isDefaultFolderSettingsResource(uri) ? ConfigurationTarget.WORKSPACE_FOLDER : ConfigurationTarget.USER;
return this.isDefaultWorkspaceSettingsResource(uri) ?
ConfigurationTarget.WORKSPACE :
this.isDefaultFolderSettingsResource(uri) ?
ConfigurationTarget.WORKSPACE_FOLDER :
ConfigurationTarget.USER_LOCAL;
}
private isDefaultSettingsResource(uri: URI): boolean {

View File

@@ -309,7 +309,7 @@ export class ProgressService2 implements IProgressService2 {
disposables.push(attachDialogStyler(dialog, this._themeService));
dialog.show().then(() => {
if (options.cancellable && typeof onDidCancel === 'function') {
if (typeof onDidCancel === 'function') {
onDidCancel();
}

View File

@@ -31,6 +31,7 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
/**
* 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.
@@ -300,7 +301,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Decide on etag
let etag: string | undefined;
if (forceReadFromDisk) {
etag = undefined; // reset ETag if we enforce to read from disk
etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk
} else if (this.lastResolvedDiskStat) {
etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
}
@@ -826,10 +827,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private getTelemetryData(reason: number | undefined): object {
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(this.resource.fsPath).join(', '),
mimeType: guessMimeTypes(path).join(', '),
ext,
path: hash(this.resource.fsPath),
path: hash(path),
reason
};

View File

@@ -871,13 +871,20 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return false;
}
// take over encoding and model value from source model
// take over encoding, mode and model value from source model
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
if (targetModel.textEditorModel) {
const snapshot = sourceModel.createSnapshot();
if (snapshot) {
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot));
}
if (sourceModel.textEditorModel) {
const language = sourceModel.textEditorModel.getLanguageIdentifier();
if (language.id > 1) {
targetModel.textEditorModel.setMode(language); // only use if more specific than plain/text
}
}
}
// save model

View File

@@ -30,6 +30,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { textmateColorsSchemaId, registerColorThemeSchemas, textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
// implementation
// {{SQL CARBON EDIT}}
@@ -480,7 +481,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
this.doSetFileIconTheme(newIconTheme);
// remember theme data for a quick restore
if (newIconTheme.isLoaded) {
if (newIconTheme.isLoaded && newIconTheme.location && !getRemoteAuthority(newIconTheme.location)) {
this.storageService.store(PERSISTED_ICON_THEME_STORAGE_KEY, newIconTheme.toStorageData(), StorageScope.GLOBAL);
}