diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json
index 31316129b7f75..9ea6c3cec1bba 100644
--- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json
+++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json
@@ -4,7 +4,7 @@
"If you want to provide a fix or improvement, please create a pull request against the original repository.",
"Once accepted there, we are happy to receive an update request."
],
- "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/a4c4dafb226a4c1d037a52434aa1154d9dabad8b",
+ "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/3508c88a4ac6112934e0c34de7942c67682b2321",
"name": "JavaScript (with React support)",
"scopeName": "source.js",
"patterns": [
@@ -2416,7 +2416,7 @@
"patterns": [
{
"begin": "(? {
test('Should use label as function name', async () => {
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 b3b91a036f5e0..dc700abdf1b89 100644
--- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts
+++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts
@@ -287,6 +287,30 @@ suite('workspace-namespace', () => {
});
});
+ test('events: onDidSaveTextDocument fires even for non dirty file when saved', () => {
+ return createRandomFile().then(file => {
+ let disposables: vscode.Disposable[] = [];
+
+ let onDidSaveTextDocument = false;
+ disposables.push(vscode.workspace.onDidSaveTextDocument(e => {
+ assert.ok(pathEquals(e.uri.fsPath, file.fsPath));
+ onDidSaveTextDocument = true;
+ }));
+
+ return vscode.workspace.openTextDocument(file).then(doc => {
+ return vscode.window.showTextDocument(doc).then(() => {
+ return vscode.commands.executeCommand('workbench.action.files.save').then(() => {
+ assert.ok(onDidSaveTextDocument);
+
+ disposeAll(disposables);
+
+ return deleteFile(file);
+ });
+ });
+ });
+ });
+ });
+
test('openTextDocument, with selection', function () {
return createRandomFile('foo\nbar\nbar').then(file => {
return vscode.workspace.openTextDocument(file).then(doc => {
diff --git a/package.json b/package.json
index 0b033e15b0f88..85af8fa34de86 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "code-oss-dev",
- "version": "1.34.0",
+ "version": "1.35.0",
"distro": "b9fb31b7caa6d9f506aa458342ab71183bc90b52",
"author": {
"name": "Microsoft Corporation"
diff --git a/src/typings/vscode-xterm.d.ts b/src/typings/vscode-xterm.d.ts
index e425a37c43f16..569af6027a64c 100644
--- a/src/typings/vscode-xterm.d.ts
+++ b/src/typings/vscode-xterm.d.ts
@@ -813,5 +813,7 @@ declare module 'vscode-xterm' {
* @return Whether a result was found.
*/
findPrevious(term: string, findOptions: ISearchOptions): boolean;
+
+ addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
}
}
diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts
index de19c7c03f8e2..70e70de33a119 100644
--- a/src/vs/base/common/mime.ts
+++ b/src/vs/base/common/mime.ts
@@ -197,7 +197,11 @@ function guessMimeTypeByFirstline(firstLine: string): string | null {
}
if (firstLine.length > 0) {
- for (const association of registeredAssociations) {
+
+ // We want to prioritize associations based on the order they are registered so that the last registered
+ // association wins over all other. This is for https://github.com/Microsoft/vscode/issues/20074
+ for (let i = registeredAssociations.length - 1; i >= 0; i--) {
+ const association = registeredAssociations[i];
if (!association.firstline) {
continue;
}
@@ -230,10 +234,11 @@ export function isUnspecific(mime: string[] | string): boolean {
* 2. Otherwise, if there are other extensions, suggest the first one.
* 3. Otherwise, suggest the prefix.
*/
-export function suggestFilename(langId: string | null, prefix: string): string {
+export function suggestFilename(mode: string | undefined, prefix: string): string {
const extensions = registeredAssociations
- .filter(assoc => !assoc.userConfigured && assoc.extension && assoc.id === langId)
+ .filter(assoc => !assoc.userConfigured && assoc.extension && assoc.id === mode)
.map(assoc => assoc.extension);
+
const extensionsWithDotFirst = coalesce(extensions)
.filter(assoc => startsWith(assoc, '.'));
diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts
index 75f6d531fe7d4..4cea0feb56533 100644
--- a/src/vs/base/test/common/mime.test.ts
+++ b/src/vs/base/test/common/mime.test.ts
@@ -6,6 +6,7 @@ import * as assert from 'assert';
import { guessMimeTypes, registerTextMime, suggestFilename } from 'vs/base/common/mime';
suite('Mime', () => {
+
test('Dynamically Register Text Mime', () => {
let guess = guessMimeTypes('foo.monaco');
assert.deepEqual(guess, ['application/unknown']);
@@ -56,6 +57,11 @@ suite('Mime', () => {
registerTextMime({ id: 'docker', filepattern: 'dockerfile*', mime: 'text/looser' });
guess = guessMimeTypes('dockerfile');
assert.deepEqual(guess, ['text/winner', 'text/plain']);
+
+ registerTextMime({ id: 'azure-looser', mime: 'text/azure-looser', firstline: /azure/ });
+ registerTextMime({ id: 'azure-winner', mime: 'text/azure-winner', firstline: /azure/ });
+ guess = guessMimeTypes('azure', 'azure');
+ assert.deepEqual(guess, ['text/azure-winner', 'text/plain']);
});
test('Specificity priority 1', () => {
diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js
index dbec9eadbbb44..28e3fefafcc1a 100644
--- a/src/vs/code/electron-browser/workbench/workbench.js
+++ b/src/vs/code/electron-browser/workbench/workbench.js
@@ -87,8 +87,8 @@ function showPartsSplash(configuration) {
const style = document.createElement('style');
style.className = 'initialShellColors';
document.head.appendChild(style);
- document.body.className = `monaco-shell ${baseTheme}`;
- style.innerHTML = `.monaco-shell { background-color: ${shellBackground}; color: ${shellForeground}; }`;
+ document.body.className = baseTheme;
+ style.innerHTML = `body { background-color: ${shellBackground}; color: ${shellForeground}; }`;
if (data && data.layoutInfo) {
// restore parts if possible (we might not always store layout info)
diff --git a/src/vs/code/electron-browser/workbench/workbench.nodeless.html b/src/vs/code/electron-browser/workbench/workbench.nodeless.html
index e37abfdf5ae28..de77f56d68ef0 100644
--- a/src/vs/code/electron-browser/workbench/workbench.nodeless.html
+++ b/src/vs/code/electron-browser/workbench/workbench.nodeless.html
@@ -4,7 +4,7 @@
-
+
diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts
index fdfb2dcbc043c..915cf30ba61b2 100644
--- a/src/vs/code/electron-main/window.ts
+++ b/src/vs/code/electron-main/window.ts
@@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { IStateService } from 'vs/platform/state/common/state';
-import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron';
+import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display } from 'electron';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -740,32 +740,30 @@ export class CodeWindow extends Disposable implements ICodeWindow {
// Single Monitor: be strict about x/y positioning
if (displays.length === 1) {
- const displayBounds = displays[0].bounds;
-
- // Careful with maximized: in that mode x/y can well be negative!
- if (state.mode !== WindowMode.Maximized && displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) {
- if (state.x < displayBounds.x) {
- state.x = displayBounds.x; // prevent window from falling out of the screen to the left
+ const displayWorkingArea = this.getWorkingArea(displays[0]);
+ if (state.mode !== WindowMode.Maximized && displayWorkingArea) {
+ if (state.x < displayWorkingArea.x) {
+ state.x = displayWorkingArea.x; // prevent window from falling out of the screen to the left
}
- if (state.y < displayBounds.y) {
- state.y = displayBounds.y; // prevent window from falling out of the screen to the top
+ if (state.y < displayWorkingArea.y) {
+ state.y = displayWorkingArea.y; // prevent window from falling out of the screen to the top
}
- if (state.x > (displayBounds.x + displayBounds.width)) {
- state.x = displayBounds.x; // prevent window from falling out of the screen to the right
+ if (state.x > (displayWorkingArea.x + displayWorkingArea.width)) {
+ state.x = displayWorkingArea.x; // prevent window from falling out of the screen to the right
}
- if (state.y > (displayBounds.y + displayBounds.height)) {
- state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom
+ if (state.y > (displayWorkingArea.y + displayWorkingArea.height)) {
+ state.y = displayWorkingArea.y; // prevent window from falling out of the screen to the bottom
}
- if (state.width > displayBounds.width) {
- state.width = displayBounds.width; // prevent window from exceeding display bounds width
+ if (state.width > displayWorkingArea.width) {
+ state.width = displayWorkingArea.width; // prevent window from exceeding display bounds width
}
- if (state.height > displayBounds.height) {
- state.height = displayBounds.height; // prevent window from exceeding display bounds height
+ if (state.height > displayWorkingArea.height) {
+ state.height = displayWorkingArea.height; // prevent window from exceeding display bounds height
}
}
@@ -791,12 +789,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
// Multi Monitor (non-fullscreen): be less strict because metrics can be crazy
const bounds = { x: state.x, y: state.y, width: state.width, height: state.height };
const display = screen.getDisplayMatching(bounds);
+ const displayWorkingArea = this.getWorkingArea(display);
if (
- display && // we have a display matching the desired bounds
- bounds.x < display.bounds.x + display.bounds.width && // prevent window from falling out of the screen to the right
- bounds.y < display.bounds.y + display.bounds.height && // prevent window from falling out of the screen to the bottom
- bounds.x + bounds.width > display.bounds.x && // prevent window from falling out of the screen to the left
- bounds.y + bounds.height > display.bounds.y // prevent window from falling out of the scree nto the top
+ display && // we have a display matching the desired bounds
+ displayWorkingArea && // we have valid working area bounds
+ bounds.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right
+ bounds.y < displayWorkingArea.y + displayWorkingArea.height && // prevent window from falling out of the screen to the bottom
+ bounds.x + bounds.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left
+ bounds.y + bounds.height > displayWorkingArea.y // prevent window from falling out of the scree nto the top
) {
if (state.mode === WindowMode.Maximized) {
const defaults = defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
@@ -812,6 +812,24 @@ export class CodeWindow extends Disposable implements ICodeWindow {
return null;
}
+ private getWorkingArea(display: Display): Rectangle | undefined {
+
+ // Prefer the working area of the display to account for taskbars on the
+ // desktop being positioned somewhere (https://github.com/Microsoft/vscode/issues/50830).
+ //
+ // Linux X11 sessions sometimes report wrong display bounds, so we validate
+ // the reported sizes are positive.
+ if (display.workArea.width > 0 && display.workArea.height > 0) {
+ return display.workArea;
+ }
+
+ if (display.bounds.width > 0 && display.bounds.height > 0) {
+ return display.bounds;
+ }
+
+ return undefined;
+ }
+
getBounds(): Electron.Rectangle {
const pos = this._win.getPosition();
const dimension = this._win.getSize();
diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts
index ca49de2528ff4..af148c324af47 100644
--- a/src/vs/editor/common/services/getIconClasses.ts
+++ b/src/vs/editor/common/services/getIconClasses.ts
@@ -19,14 +19,11 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe
// Get the path and name of the resource. For data-URIs, we need to parse specially
let name: string | undefined;
- let path: string | undefined;
if (resource.scheme === Schemas.data) {
const metadata = DataUri.parseMetaData(resource);
name = metadata.get(DataUri.META_DATA_LABEL);
- path = name;
} else {
name = cssEscape(basenameOrAuthority(resource).toLowerCase());
- path = resource.path.toLowerCase();
}
// Folders
@@ -47,46 +44,60 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe
classes.push(`ext-file-icon`); // extra segment to increase file-ext score
}
- // Configured Language
- let configuredLangId: string | null = getConfiguredLangId(modelService, modeService, resource);
- configuredLangId = configuredLangId || (path ? modeService.getModeIdByFilepathOrFirstLine(path) : null);
- if (configuredLangId) {
- classes.push(`${cssEscape(configuredLangId)}-lang-file-icon`);
+ // Detected Mode
+ const detectedModeId = detectModeId(modelService, modeService, resource);
+ if (detectedModeId) {
+ classes.push(`${cssEscape(detectedModeId)}-lang-file-icon`);
}
}
}
return classes;
}
-export function getConfiguredLangId(modelService: IModelService, modeService: IModeService, resource: uri): string | null {
- let configuredLangId: string | null = null;
- if (resource) {
- let modeId: string | null = null;
+export function detectModeId(modelService: IModelService, modeService: IModeService, resource: uri): string | null {
+ if (!resource) {
+ return null; // we need a resource at least
+ }
- // Data URI: check for encoded metadata
- if (resource.scheme === Schemas.data) {
- const metadata = DataUri.parseMetaData(resource);
- const mime = metadata.get(DataUri.META_DATA_MIME);
+ let modeId: string | null = null;
- if (mime) {
- modeId = modeService.getModeId(mime);
- }
- }
+ // Data URI: check for encoded metadata
+ if (resource.scheme === Schemas.data) {
+ const metadata = DataUri.parseMetaData(resource);
+ const mime = metadata.get(DataUri.META_DATA_MIME);
- // Any other URI: check for model if existing
- else {
- const model = modelService.getModel(resource);
- if (model) {
- modeId = model.getLanguageIdentifier().language;
- }
+ if (mime) {
+ modeId = modeService.getModeId(mime);
}
+ }
- if (modeId && modeId !== PLAINTEXT_MODE_ID) {
- configuredLangId = modeId; // only take if the mode is specific (aka no just plain text)
+ // Any other URI: check for model if existing
+ else {
+ const model = modelService.getModel(resource);
+ if (model) {
+ modeId = model.getModeId();
}
}
- return configuredLangId;
+ // only take if the mode is specific (aka no just plain text)
+ if (modeId && modeId !== PLAINTEXT_MODE_ID) {
+ return modeId;
+ }
+
+ // otherwise fallback to path based detection
+ let path: string | undefined;
+ if (resource.scheme === Schemas.data) {
+ const metadata = DataUri.parseMetaData(resource);
+ path = metadata.get(DataUri.META_DATA_LABEL);
+ } else {
+ path = resource.path.toLowerCase();
+ }
+
+ if (path) {
+ return modeService.getModeIdByFilepathOrFirstLine(path);
+ }
+
+ return null; // finally - we do not know the mode id
}
export function cssEscape(val: string): string {
diff --git a/src/vs/editor/common/services/resolverService.ts b/src/vs/editor/common/services/resolverService.ts
index 442813334d595..958469e16f1ab 100644
--- a/src/vs/editor/common/services/resolverService.ts
+++ b/src/vs/editor/common/services/resolverService.ts
@@ -5,7 +5,7 @@
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
-import { ITextModel } from 'vs/editor/common/model';
+import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -46,6 +46,12 @@ export interface ITextEditorModel extends IEditorModel {
*/
readonly textEditorModel: ITextModel | null;
+ /**
+ * Creates a snapshot of the model's contents.
+ */
+ createSnapshot(this: IResolvedTextEditorModel): ITextSnapshot;
+ createSnapshot(this: ITextEditorModel): ITextSnapshot | null;
+
isReadonly(): boolean;
}
diff --git a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts
index 15e90604ed6f9..f0fb3b3e6bf48 100644
--- a/src/vs/editor/contrib/codeAction/codeActionTrigger.ts
+++ b/src/vs/editor/contrib/codeAction/codeActionTrigger.ts
@@ -20,8 +20,12 @@ export class CodeActionKind {
public readonly value: string
) { }
+ public equals(other: CodeActionKind): boolean {
+ return this.value === other.value;
+ }
+
public contains(other: CodeActionKind): boolean {
- return this.value === other.value || startsWith(other.value, this.value + CodeActionKind.sep);
+ return this.equals(other) || startsWith(other.value, this.value + CodeActionKind.sep);
}
public intersects(other: CodeActionKind): boolean {
diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts
index b6468cddfc1da..349f140365786 100644
--- a/src/vs/editor/standalone/browser/simpleServices.ts
+++ b/src/vs/editor/standalone/browser/simpleServices.ts
@@ -19,7 +19,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation';
import { IPosition, Position as Pos } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
-import { ITextModel } from 'vs/editor/common/model';
+import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
import { TextEdit, WorkspaceEdit, isResourceTextEdit } from 'vs/editor/common/modes';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -67,6 +67,10 @@ export class SimpleModel implements IResolvedTextEditorModel {
return this.model;
}
+ public createSnapshot(): ITextSnapshot {
+ return this.model.createSnapshot();
+ }
+
public isReadonly(): boolean {
return false;
}
diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts
index 203bd80327be3..160ea244b45d3 100644
--- a/src/vs/platform/editor/common/editor.ts
+++ b/src/vs/platform/editor/common/editor.ts
@@ -63,7 +63,7 @@ export interface IBaseResourceInput {
export interface IResourceInput extends IBaseResourceInput {
/**
- * The resource URL of the resource to open.
+ * The resource URI of the resource to open.
*/
resource: URI;
@@ -71,6 +71,12 @@ export interface IResourceInput extends IBaseResourceInput {
* The encoding of the text input if known.
*/
readonly encoding?: string;
+
+ /**
+ * The identifier of the language mode of the text input
+ * if known to use when displaying the contents.
+ */
+ readonly mode?: string;
}
export interface IEditorOptions {
diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts
index 8a9db6d0aca24..4429cc10574ca 100644
--- a/src/vs/platform/history/electron-main/historyMainService.ts
+++ b/src/vs/platform/history/electron-main/historyMainService.ts
@@ -299,11 +299,11 @@ export class HistoryMainService implements IHistoryMainService {
description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService));
args = `--folder-uri "${workspace.toString()}"`;
} else {
- description = nls.localize('codeWorkspace', "Code Workspace");
+ description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService));
args = `--file-uri "${workspace.configPath.toString()}"`;
}
- return {
+ return {
type: 'task',
title,
description,
diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts
index 608480b1aac84..df147f419c3c3 100644
--- a/src/vs/platform/instantiation/common/instantiation.ts
+++ b/src/vs/platform/instantiation/common/instantiation.ts
@@ -125,7 +125,7 @@ function storeServiceDependency(id: Function, target: Function, index: number, o
/**
* A *only* valid way to create a {{ServiceIdentifier}}.
*/
-export function createDecorator(serviceId: string): { (...args: any[]): void; type: T; } {
+export function createDecorator(serviceId: string): ServiceIdentifier {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts
index c18e4fc136389..af99b8a772064 100644
--- a/src/vs/workbench/api/browser/mainThreadDocuments.ts
+++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts
@@ -228,10 +228,10 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
});
}
- private _doCreateUntitled(resource?: URI, modeId?: string, initialValue?: string): Promise {
+ private _doCreateUntitled(resource?: URI, mode?: string, initialValue?: string): Promise {
return this._untitledEditorService.loadOrCreate({
resource,
- modeId,
+ mode,
initialValue,
useResourcePath: Boolean(resource && resource.path)
}).then(model => {
diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
index 27ae35cfcd502..76bed28b12ee0 100644
--- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
+++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
@@ -266,14 +266,18 @@ class CodeActionOnSaveParticipant implements ISaveParticipant {
const codeActionsOnSave = Object.keys(setting)
.filter(x => setting[x]).map(x => new CodeActionKind(x))
.sort((a, b) => {
- if (a.value === CodeActionKind.SourceFixAll.value) {
- return -1;
- }
- if (b.value === CodeActionKind.SourceFixAll.value) {
+ if (CodeActionKind.SourceFixAll.contains(a)) {
+ if (CodeActionKind.SourceFixAll.contains(b)) {
+ return 0;
+ }
return 1;
}
+ if (CodeActionKind.SourceFixAll.contains(b)) {
+ return -1;
+ }
return 0;
});
+
if (!codeActionsOnSave.length) {
return undefined;
}
@@ -289,11 +293,8 @@ class CodeActionOnSaveParticipant implements ISaveParticipant {
reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout));
}, timeout)),
this.applyOnSaveActions(model, codeActionsOnSave, tokenSource.token)
- ]).then(() => {
- tokenSource.cancel();
- }, (e) => {
+ ]).finally(() => {
tokenSource.cancel();
- return Promise.reject(e);
});
}
diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts
index 5a010d83f0072..bb9505c65f930 100644
--- a/src/vs/workbench/api/node/extHostTerminalService.ts
+++ b/src/vs/workbench/api/node/extHostTerminalService.ts
@@ -20,6 +20,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDebugService';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
+import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal';
const RENDERER_NO_PROCESS_ID = -1;
@@ -470,7 +471,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
.inspect(key.substr(key.lastIndexOf('.') + 1));
return this._apiInspectConfigToPlain(setting);
};
- terminalEnvironment.mergeDefaultShellPathAndArgs(shellLaunchConfig, fetchSetting, isWorkspaceShellAllowed || false);
+ terminalEnvironment.mergeDefaultShellPathAndArgs(shellLaunchConfig, fetchSetting, isWorkspaceShellAllowed || false, getDefaultShell(platform.platform));
}
// Get the initial cwd
diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts
index 5c4a13a5587f0..3751e3bf4b27a 100644
--- a/src/vs/workbench/browser/dnd.ts
+++ b/src/vs/workbench/browser/dnd.ts
@@ -240,7 +240,7 @@ export class ResourcesDropHandler {
return this.backupFileService.resolveBackupContent(droppedDirtyEditor.backupResource!).then(content => {
// Set the contents of to the resource to the target
- return this.backupFileService.backupResource(droppedDirtyEditor.resource, content!.create(this.getDefaultEOL()).createSnapshot(true));
+ return this.backupFileService.backupResource(droppedDirtyEditor.resource, content.value.create(this.getDefaultEOL()).createSnapshot(true));
}).then(() => false, () => false /* ignore any error */);
}
diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts
index d4cc5b6851f5d..8eaff3f7e373a 100644
--- a/src/vs/workbench/browser/labels.ts
+++ b/src/vs/workbench/browser/labels.ts
@@ -21,7 +21,7 @@ import { ITextModel } from 'vs/editor/common/model';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { Event, Emitter } from 'vs/base/common/event';
import { ILabelService } from 'vs/platform/label/common/label';
-import { getIconClasses, getConfiguredLangId } from 'vs/editor/common/services/getIconClasses';
+import { getIconClasses, detectModeId } from 'vs/editor/common/services/getIconClasses';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { withNullAsUndefined } from 'vs/base/common/types';
@@ -121,7 +121,16 @@ export class ResourceLabels extends Disposable {
return; // ignore transitions in files from no mode to specific mode because this happens each time a model is created
}
- this._widgets.forEach(widget => widget.notifyModelModeChanged(e));
+ this._widgets.forEach(widget => widget.notifyModelModeChanged(e.model));
+ }));
+
+ // notify when model is added
+ this._register(this.modelService.onModelAdded(model => {
+ if (!model.uri) {
+ return; // we need the resource to compare
+ }
+
+ this._widgets.forEach(widget => widget.notifyModelAdded(model));
}));
// notify when file decoration changes
@@ -228,7 +237,7 @@ class ResourceLabelWidget extends IconLabel {
private label?: IResourceLabelProps;
private options?: IResourceLabelOptions;
private computedIconClasses?: string[];
- private lastKnownConfiguredLangId?: string;
+ private lastKnownDetectedModeId?: string;
private computedPathLabel?: string;
private needsRedraw?: Redraw;
@@ -258,13 +267,21 @@ class ResourceLabelWidget extends IconLabel {
}
}
- notifyModelModeChanged(e: { model: ITextModel; oldModeId: string; }): void {
+ notifyModelModeChanged(model: ITextModel): void {
+ this.handleModelEvent(model);
+ }
+
+ notifyModelAdded(model: ITextModel): void {
+ this.handleModelEvent(model);
+ }
+
+ private handleModelEvent(model: ITextModel): void {
if (!this.label || !this.label.resource) {
return; // only update if label exists
}
- if (e.model.uri.toString() === this.label.resource.toString()) {
- if (this.lastKnownConfiguredLangId !== e.model.getLanguageIdentifier().language) {
+ if (model.uri.toString() === this.label.resource.toString()) {
+ if (this.lastKnownDetectedModeId !== model.getModeId()) {
this.render(true); // update if the language id of the model has changed from our last known state
}
}
@@ -367,7 +384,7 @@ class ResourceLabelWidget extends IconLabel {
clear(): void {
this.label = undefined;
this.options = undefined;
- this.lastKnownConfiguredLangId = undefined;
+ this.lastKnownDetectedModeId = undefined;
this.computedIconClasses = undefined;
this.computedPathLabel = undefined;
@@ -388,10 +405,10 @@ class ResourceLabelWidget extends IconLabel {
}
if (this.label) {
- const configuredLangId = this.label.resource ? withNullAsUndefined(getConfiguredLangId(this.modelService, this.modeService, this.label.resource)) : undefined;
- if (this.lastKnownConfiguredLangId !== configuredLangId) {
+ const detectedModeId = this.label.resource ? withNullAsUndefined(detectModeId(this.modelService, this.modeService, this.label.resource)) : undefined;
+ if (this.lastKnownDetectedModeId !== detectedModeId) {
clearIconCache = true;
- this.lastKnownConfiguredLangId = configuredLangId;
+ this.lastKnownDetectedModeId = detectedModeId;
}
}
@@ -465,7 +482,7 @@ class ResourceLabelWidget extends IconLabel {
this.label = undefined;
this.options = undefined;
- this.lastKnownConfiguredLangId = undefined;
+ this.lastKnownDetectedModeId = undefined;
this.computedIconClasses = undefined;
this.computedPathLabel = undefined;
}
diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts
index 474cd6f53e561..7814e0986ba3a 100644
--- a/src/vs/workbench/browser/nodeless.simpleservices.ts
+++ b/src/vs/workbench/browser/nodeless.simpleservices.ts
@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
-import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
-import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
+import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
+import { ITextSnapshot } from 'vs/editor/common/model';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { keys, ResourceMap } from 'vs/base/common/map';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
@@ -86,20 +86,20 @@ export class SimpleBackupFileService implements IBackupFileService {
return Promise.resolve(undefined);
}
- backupResource(resource: URI, content: ITextSnapshot, versionId?: number): Promise {
+ backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise {
const backupResource = this.toBackupResource(resource);
this.backups.set(backupResource.toString(), content);
return Promise.resolve();
}
- resolveBackupContent(backupResource: URI): Promise {
+ resolveBackupContent(backupResource: URI): Promise> {
const snapshot = this.backups.get(backupResource.toString());
if (snapshot) {
- return Promise.resolve(createTextBufferFactoryFromSnapshot(snapshot));
+ return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
}
- return Promise.resolve(undefined);
+ return Promise.reject('Unexpected backup resource to resolve');
}
getWorkspaceFileBackups(): Promise {
diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts
index 414b807ea7045..a8abe040f01f2 100644
--- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts
+++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts
@@ -104,7 +104,7 @@ Registry.as(EditorExtensions.Editors).registerEditor(
interface ISerializedUntitledEditorInput {
resource: string;
resourceJSON: object;
- modeId: string | null;
+ modeId: string | undefined;
encoding: string;
}
@@ -131,7 +131,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory {
const serialized: ISerializedUntitledEditorInput = {
resource: resource.toString(), // Keep for backwards compatibility
resourceJSON: resource.toJSON(),
- modeId: untitledEditorInput.getModeId(),
+ modeId: untitledEditorInput.getMode(),
encoding: untitledEditorInput.getEncoding()
};
@@ -142,10 +142,10 @@ class UntitledEditorInputFactory implements IEditorInputFactory {
return instantiationService.invokeFunction(accessor => {
const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput);
const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource);
- const language = deserialized.modeId;
+ const mode = deserialized.modeId;
const encoding = deserialized.encoding;
- return accessor.get(IEditorService).createInput({ resource, language, encoding, forceUntitled: true }) as UntitledEditorInput;
+ return accessor.get(IEditorService).createInput({ resource, mode, encoding, forceUntitled: true }) as UntitledEditorInput;
});
}
}
diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts
index b6c197e5b66af..5c6df3b90ae16 100644
--- a/src/vs/workbench/browser/parts/editor/editorStatus.ts
+++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts
@@ -14,11 +14,11 @@ import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { Action } from 'vs/base/common/actions';
import { Language } from 'vs/base/common/platform';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
-import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor as IBaseEditor, IEditorInput, SideBySideEditor } from 'vs/workbench/common/editor';
+import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor as IBaseEditor, IEditorInput, SideBySideEditor, IModeSupport } from 'vs/workbench/common/editor';
import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IEditorAction } from 'vs/editor/common/editorCommon';
-import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
+import { EndOfLineSequence } from 'vs/editor/common/model';
import { IModelLanguageChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { TrimTrailingWhitespaceAction } from 'vs/editor/contrib/linesOperations/linesOperations';
import { IndentUsingSpaces, IndentUsingTabs, DetectIndentation, IndentationToSpacesAction, IndentationToTabsAction } from 'vs/editor/contrib/indentation/indentation';
@@ -59,7 +59,15 @@ class SideBySideEditorEncodingSupport implements IEncodingSupport {
}
setEncoding(encoding: string, mode: EncodingMode): void {
- [this.master, this.details].forEach(s => s.setEncoding(encoding, mode));
+ [this.master, this.details].forEach(editor => editor.setEncoding(encoding, mode));
+ }
+}
+
+class SideBySideEditorModeSupport implements IModeSupport {
+ constructor(private master: IModeSupport, private details: IModeSupport) { }
+
+ setMode(mode: string): void {
+ [this.master, this.details].forEach(editor => editor.setMode(mode));
}
}
@@ -83,7 +91,7 @@ function toEditorWithEncodingSupport(input: IEditorInput): IEncodingSupport | nu
}
// File or Resource Editor
- let encodingSupport = input as IFileEditorInput;
+ const encodingSupport = input as IFileEditorInput;
if (areFunctions(encodingSupport.setEncoding, encodingSupport.getEncoding)) {
return encodingSupport;
}
@@ -92,14 +100,41 @@ function toEditorWithEncodingSupport(input: IEditorInput): IEncodingSupport | nu
return null;
}
+function toEditorWithModeSupport(input: IEditorInput): IModeSupport | null {
+
+ // Untitled Editor
+ if (input instanceof UntitledEditorInput) {
+ return input;
+ }
+
+ // Side by Side (diff) Editor
+ if (input instanceof SideBySideEditorInput) {
+ const masterModeSupport = toEditorWithModeSupport(input.master);
+ const detailsModeSupport = toEditorWithModeSupport(input.details);
+
+ if (masterModeSupport && detailsModeSupport) {
+ return new SideBySideEditorModeSupport(masterModeSupport, detailsModeSupport);
+ }
+
+ return masterModeSupport;
+ }
+
+ // File or Resource Editor
+ const modeSupport = input as IFileEditorInput;
+ if (typeof modeSupport.setMode === 'function') {
+ return modeSupport;
+ }
+
+ // Unsupported for any other editor
+ return null;
+}
+
interface IEditorSelectionStatus {
selections?: Selection[];
charactersSelected?: number;
}
class StateChange {
- _stateChangeBrand: void;
-
indentation: boolean = false;
selectionStatus: boolean = false;
mode: boolean = false;
@@ -120,7 +155,7 @@ class StateChange {
this.metadata = this.metadata || other.metadata;
}
- public hasChanges(): boolean {
+ hasChanges(): boolean {
return this.indentation
|| this.selectionStatus
|| this.mode
@@ -179,42 +214,49 @@ class State {
change.selectionStatus = true;
}
}
+
if ('indentation' in update) {
if (this._indentation !== update.indentation) {
this._indentation = update.indentation;
change.indentation = true;
}
}
+
if ('mode' in update) {
if (this._mode !== update.mode) {
this._mode = update.mode;
change.mode = true;
}
}
+
if ('encoding' in update) {
if (this._encoding !== update.encoding) {
this._encoding = update.encoding;
change.encoding = true;
}
}
+
if ('EOL' in update) {
if (this._EOL !== update.EOL) {
this._EOL = update.EOL;
change.EOL = true;
}
}
+
if ('tabFocusMode' in update) {
if (this._tabFocusMode !== update.tabFocusMode) {
this._tabFocusMode = update.tabFocusMode;
change.tabFocusMode = true;
}
}
+
if ('screenReaderMode' in update) {
if (this._screenReaderMode !== update.screenReaderMode) {
this._screenReaderMode = update.screenReaderMode;
change.screenReaderMode = true;
}
}
+
if ('metadata' in update) {
if (this._metadata !== update.metadata) {
this._metadata = update.metadata;
@@ -236,7 +278,6 @@ const nlsTabFocusMode = nls.localize('tabFocusModeEnabled', "Tab Moves Focus");
const nlsScreenReaderDetected = nls.localize('screenReaderDetected', "Screen Reader Optimized");
const nlsScreenReaderDetectedTitle = nls.localize('screenReaderDetectedExtra', "If you are not using a Screen Reader, please change the setting `editor.accessibilitySupport` to \"off\".");
-
class StatusBarItem {
private _showing = true;
@@ -248,15 +289,15 @@ class StatusBarItem {
this.element.title = title;
}
- public set textContent(value: string) {
+ set textContent(value: string) {
this.element.textContent = value;
}
- public set onclick(value: () => void) {
+ set onclick(value: () => void) {
this.element.onclick = value;
}
- public setVisible(shouldShow: boolean): void {
+ setVisible(shouldShow: boolean): void {
if (shouldShow !== this._showing) {
this._showing = shouldShow;
this.element.style.display = shouldShow ? '' : 'none';
@@ -264,7 +305,6 @@ class StatusBarItem {
}
}
-
export class EditorStatus implements IStatusbarItem {
private state: State;
private element: HTMLElement;
@@ -661,7 +701,7 @@ export class EditorStatus implements IStatusbarItem {
this.updateState(update);
}
- private _promptedScreenReader: boolean = false;
+ private promptedScreenReader: boolean = false;
private onScreenReaderModeChange(editorWidget: ICodeEditor | undefined): void {
let screenReaderMode = false;
@@ -673,8 +713,8 @@ export class EditorStatus implements IStatusbarItem {
const screenReaderConfiguration = this.configurationService.getValue('editor').accessibilitySupport;
if (screenReaderConfiguration === 'auto') {
// show explanation
- if (!this._promptedScreenReader) {
- this._promptedScreenReader = true;
+ if (!this.promptedScreenReader) {
+ this.promptedScreenReader = true;
setTimeout(() => {
this.onScreenReaderModeClick();
}, 100);
@@ -948,43 +988,28 @@ export class ChangeModeAction extends Action {
// Change mode for active editor
const activeEditor = this.editorService.activeEditor;
- const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
- const models: ITextModel[] = [];
- if (isCodeEditor(activeTextEditorWidget)) {
- const codeEditorModel = activeTextEditorWidget.getModel();
- if (codeEditorModel) {
- models.push(codeEditorModel);
- }
- } else if (isDiffEditor(activeTextEditorWidget)) {
- const diffEditorModel = activeTextEditorWidget.getModel();
- if (diffEditorModel) {
- if (diffEditorModel.original) {
- models.push(diffEditorModel.original);
- }
- if (diffEditorModel.modified) {
- models.push(diffEditorModel.modified);
+ if (activeEditor) {
+ const modeSupport = toEditorWithModeSupport(activeEditor);
+ if (modeSupport) {
+
+ // Find mode
+ let languageSelection: ILanguageSelection | undefined;
+ if (pick === autoDetectMode) {
+ if (textModel) {
+ const resource = toResource(activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
+ if (resource) {
+ languageSelection = this.modeService.createByFilepathOrFirstLine(resource.fsPath, textModel.getLineContent(1));
+ }
+ }
+ } else {
+ languageSelection = this.modeService.createByLanguageName(pick.label);
}
- }
- }
- // Find mode
- let languageSelection: ILanguageSelection | undefined;
- if (pick === autoDetectMode) {
- if (textModel) {
- const resource = toResource(activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
- if (resource) {
- languageSelection = this.modeService.createByFilepathOrFirstLine(resource.fsPath, textModel.getLineContent(1));
+ // Change mode
+ if (typeof languageSelection !== 'undefined') {
+ modeSupport.setMode(languageSelection.languageIdentifier.language);
}
}
- } else {
- languageSelection = this.modeService.createByLanguageName(pick.label);
- }
-
- // Change mode
- if (typeof languageSelection !== 'undefined') {
- for (const textModel of models) {
- this.modelService.setMode(textModel, languageSelection);
- }
}
});
}
@@ -1159,6 +1184,7 @@ export class ChangeEncodingAction extends Action {
if (!activeControl) {
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
}
+
const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeControl.input);
if (!encodingSupport) {
return this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]);
@@ -1249,10 +1275,12 @@ export class ChangeEncodingAction extends Action {
if (!encoding) {
return;
}
+
const activeControl = this.editorService.activeControl;
if (!activeControl) {
return;
}
+
const encodingSupport = toEditorWithEncodingSupport(activeControl.input);
if (typeof encoding.id !== 'undefined' && encodingSupport && encodingSupport.getEncoding() !== encoding.id) {
encodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding
diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts
index a509721f06451..8ecf2131c31e4 100644
--- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts
+++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts
@@ -370,7 +370,7 @@ class InlineImageView {
dispose: () => combinedDisposable(disposables).dispose()
};
- const cacheKey = descriptor.resource.toString();
+ const cacheKey = `${descriptor.resource.toString()}:${descriptor.etag}`;
let ctrlPressed = false;
let altPressed = false;
diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts
index 80915b758e203..37bd1a1eee6c3 100644
--- a/src/vs/workbench/common/editor.ts
+++ b/src/vs/workbench/common/editor.ts
@@ -144,7 +144,7 @@ export interface IEditorControl extends ICompositeControl { }
export interface IFileInputFactory {
- createFileInput(resource: URI, encoding: string | undefined, instantiationService: IInstantiationService): IFileEditorInput;
+ createFileInput(resource: URI, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput;
isFileInput(obj: any): obj is IFileEditorInput;
}
@@ -209,7 +209,7 @@ export interface IUntitledResourceInput extends IBaseResourceInput {
/**
* Optional language of the untitled resource.
*/
- language?: string;
+ mode?: string;
/**
* Optional contents of the untitled resource.
@@ -505,19 +505,35 @@ export interface IEncodingSupport {
setEncoding(encoding: string, mode: EncodingMode): void;
}
+export interface IModeSupport {
+
+ /**
+ * Sets the language mode of the input.
+ */
+ setMode(mode: string): void;
+}
+
/**
* This is a tagging interface to declare an editor input being capable of dealing with files. It is only used in the editor registry
* to register this kind of input to the platform.
*/
-export interface IFileEditorInput extends IEditorInput, IEncodingSupport {
+export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeSupport {
+ /**
+ * Gets the resource this editor is about.
+ */
getResource(): URI;
/**
- * Sets the preferred encodingt to use for this input.
+ * Sets the preferred encoding to use for this input.
*/
setPreferredEncoding(encoding: string): void;
+ /**
+ * Sets the preferred language mode to use for this input.
+ */
+ setPreferredMode(mode: string): void;
+
/**
* Forces this file input to open as binary instead of text.
*/
diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts
index 7fed290b43bef..aad672b42e55e 100644
--- a/src/vs/workbench/common/editor/resourceEditorInput.ts
+++ b/src/vs/workbench/common/editor/resourceEditorInput.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { EditorInput, ITextEditorModel } from 'vs/workbench/common/editor';
+import { EditorInput, ITextEditorModel, IModeSupport } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { IReference } from 'vs/base/common/lifecycle';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -13,16 +13,18 @@ import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorMo
* A read-only text editor input whos contents are made of the provided resource that points to an existing
* code editor model.
*/
-export class ResourceEditorInput extends EditorInput {
+export class ResourceEditorInput extends EditorInput implements IModeSupport {
static readonly ID: string = 'workbench.editors.resourceEditorInput';
+ private cachedModel: ResourceEditorModel | null;
private modelReference: Promise> | null;
constructor(
private name: string,
private description: string | null,
private readonly resource: URI,
+ private preferredMode: string | undefined,
@ITextModelService private readonly textModelResolverService: ITextModelService
) {
super();
@@ -62,6 +64,18 @@ export class ResourceEditorInput extends EditorInput {
}
}
+ setMode(mode: string): void {
+ this.setPreferredMode(mode);
+
+ if (this.cachedModel) {
+ this.cachedModel.setMode(mode);
+ }
+ }
+
+ setPreferredMode(mode: string): void {
+ this.preferredMode = mode;
+ }
+
resolve(): Promise {
if (!this.modelReference) {
this.modelReference = this.textModelResolverService.createModelReference(this.resource);
@@ -70,6 +84,7 @@ export class ResourceEditorInput extends EditorInput {
return this.modelReference.then(ref => {
const model = ref.object;
+ // Ensure the resolved model is of expected type
if (!(model instanceof ResourceEditorModel)) {
ref.dispose();
this.modelReference = null;
@@ -77,6 +92,13 @@ export class ResourceEditorInput extends EditorInput {
return Promise.reject(new Error(`Unexpected model for ResourceInput: ${this.resource}`));
}
+ this.cachedModel = model;
+
+ // Set mode if we have a preferred mode configured
+ if (this.preferredMode) {
+ model.setMode(this.preferredMode);
+ }
+
return model;
});
}
@@ -87,7 +109,7 @@ export class ResourceEditorInput extends EditorInput {
}
if (otherInput instanceof ResourceEditorInput) {
- let otherResourceEditorInput = otherInput;
+ const otherResourceEditorInput = otherInput;
// Compare by properties
return otherResourceEditorInput.resource.toString() === this.resource.toString();
@@ -102,6 +124,8 @@ export class ResourceEditorInput extends EditorInput {
this.modelReference = null;
}
+ this.cachedModel = null;
+
super.dispose();
}
}
diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts
index 8177f3464575e..ae5d8ff2e907a 100644
--- a/src/vs/workbench/common/editor/resourceEditorModel.ts
+++ b/src/vs/workbench/common/editor/resourceEditorModel.ts
@@ -19,12 +19,18 @@ export class ResourceEditorModel extends BaseTextEditorModel {
@IModelService modelService: IModelService
) {
super(modelService, modeService, resource);
-
- // TODO@Joao: force this class to dispose the underlying model
- this.createdEditorModel = true;
}
isReadonly(): boolean {
return true;
}
+
+ dispose(): void {
+ // TODO@Joao: force this class to dispose the underlying model
+ if (this.textEditorModelHandle) {
+ this.modelService.destroyModel(this.textEditorModelHandle);
+ }
+
+ super.dispose();
+ }
}
\ No newline at end of file
diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts
index 0d7f807b4d167..a8644fbc39e0b 100644
--- a/src/vs/workbench/common/editor/textEditorModel.ts
+++ b/src/vs/workbench/common/editor/textEditorModel.ts
@@ -4,21 +4,21 @@
*--------------------------------------------------------------------------------------------*/
import { ITextModel, ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
-import { EditorModel } from 'vs/workbench/common/editor';
+import { EditorModel, IModeSupport } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IDisposable } from 'vs/base/common/lifecycle';
+import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
/**
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
*/
-export abstract class BaseTextEditorModel extends EditorModel implements ITextEditorModel {
+export abstract class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport {
+ protected textEditorModelHandle: URI | null;
+ private createdEditorModel: boolean;
- protected createdEditorModel: boolean;
-
- private textEditorModelHandle: URI | null;
private modelDisposeListener: IDisposable | null;
constructor(
@@ -64,12 +64,25 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd
abstract isReadonly(): boolean;
+ setMode(mode: string): void {
+ if (!this.isResolved()) {
+ return;
+ }
+
+ if (!mode || mode === this.textEditorModel.getModeId()) {
+ return;
+ }
+
+ this.modelService.setMode(this.textEditorModel, this.modeService.create(mode));
+ }
+
/**
- * Creates the text editor model with the provided value, modeId (can be comma separated for multiple values) and optional resource URL.
+ * Creates the text editor model with the provided value, optional preferred mode
+ * (can be comma separated for multiple values) and optional resource URL.
*/
- protected createTextEditorModel(value: ITextBufferFactory, resource: URI | undefined, modeId?: string): EditorModel {
+ protected createTextEditorModel(value: ITextBufferFactory, resource: URI | undefined, preferredMode?: string): EditorModel {
const firstLineText = this.getFirstLineText(value);
- const languageSelection = this.getOrCreateMode(this.modeService, modeId, firstLineText);
+ const languageSelection = this.getOrCreateMode(resource, this.modeService, preferredMode, firstLineText);
return this.doCreateTextEditorModel(value, languageSelection, resource);
}
@@ -83,8 +96,7 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd
// Make sure we clean up when this model gets disposed
this.registerModelDisposeListener(model);
} else {
- this.modelService.updateModel(model, value);
- this.modelService.setMode(model, languageSelection);
+ this.updateTextEditorModel(value, languageSelection.languageIdentifier.language);
}
this.textEditorModelHandle = model.uri;
@@ -110,28 +122,42 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd
*
* @param firstLineText optional first line of the text buffer to set the mode on. This can be used to guess a mode from content.
*/
- protected getOrCreateMode(modeService: IModeService, modeId: string | undefined, firstLineText?: string): ILanguageSelection {
- return modeService.create(modeId);
+ protected getOrCreateMode(resource: URI | undefined, modeService: IModeService, preferredMode: string | undefined, firstLineText?: string): ILanguageSelection {
+
+ // lookup mode via resource path if the provided mode is unspecific
+ if (!preferredMode || preferredMode === PLAINTEXT_MODE_ID) {
+ return modeService.createByFilepathOrFirstLine(resource ? resource.fsPath : null, firstLineText);
+ }
+
+ // otherwise take the preferred mode for granted
+ return modeService.create(preferredMode);
}
/**
* Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op.
*/
- protected updateTextEditorModel(newValue: ITextBufferFactory): void {
- if (!this.textEditorModel) {
+ protected updateTextEditorModel(newValue: ITextBufferFactory, preferredMode?: string): void {
+ if (!this.isResolved()) {
return;
}
+ // contents
this.modelService.updateModel(this.textEditorModel, newValue);
+
+ // mode (only if specific and changed)
+ if (preferredMode && preferredMode !== PLAINTEXT_MODE_ID && this.textEditorModel.getModeId() !== preferredMode) {
+ this.modelService.setMode(this.textEditorModel, this.modeService.create(preferredMode));
+ }
}
+ createSnapshot(this: IResolvedTextEditorModel): ITextSnapshot;
+ createSnapshot(this: ITextEditorModel): ITextSnapshot | null;
createSnapshot(): ITextSnapshot | null {
- const model = this.textEditorModel;
- if (model) {
- return model.createSnapshot(true /* Preserve BOM */);
+ if (!this.textEditorModel) {
+ return null;
}
- return null;
+ return this.textEditorModel.createSnapshot(true /* preserve BOM */);
}
isResolved(): this is IResolvedTextEditorModel {
diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts
index dea43a4c3220e..37bd6eab9afd4 100644
--- a/src/vs/workbench/common/editor/untitledEditorInput.ts
+++ b/src/vs/workbench/common/editor/untitledEditorInput.ts
@@ -9,7 +9,7 @@ import { memoize } from 'vs/base/common/decorators';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { basename } from 'vs/base/common/path';
import { basenameOrAuthority, dirname } from 'vs/base/common/resources';
-import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity } from 'vs/workbench/common/editor';
+import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity, IModeSupport } from 'vs/workbench/common/editor';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
@@ -20,12 +20,12 @@ import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverServ
/**
* An editor input to be used for untitled text buffers.
*/
-export class UntitledEditorInput extends EditorInput implements IEncodingSupport {
+export class UntitledEditorInput extends EditorInput implements IEncodingSupport, IModeSupport {
static readonly ID: string = 'workbench.editors.untitledEditorInput';
- private cachedModel: UntitledEditorModel;
- private modelResolve?: Promise;
+ private cachedModel: UntitledEditorModel | null;
+ private modelResolve: Promise | null;
private readonly _onDidModelChangeContent: Emitter = this._register(new Emitter());
get onDidModelChangeContent(): Event { return this._onDidModelChangeContent.event; }
@@ -36,7 +36,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
constructor(
private readonly resource: URI,
private readonly _hasAssociatedFilePath: boolean,
- private readonly modeId: string,
+ private preferredMode: string,
private readonly initialValue: string,
private preferredEncoding: string,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -58,14 +58,6 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
return this.resource;
}
- getModeId(): string | null {
- if (this.cachedModel) {
- return this.cachedModel.getModeId();
- }
-
- return this.modeId;
- }
-
getName(): string {
return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path;
}
@@ -168,9 +160,9 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
suggestFileName(): string {
if (!this.hasAssociatedFilePath) {
if (this.cachedModel) {
- const modeId = this.cachedModel.getModeId();
- if (modeId !== PLAINTEXT_MODE_ID) { // do not suggest when the mode ID is simple plain text
- return suggestFilename(modeId, this.getName());
+ const mode = this.cachedModel.getMode();
+ if (mode !== PLAINTEXT_MODE_ID) { // do not suggest when the mode ID is simple plain text
+ return suggestFilename(mode, this.getName());
}
}
}
@@ -194,6 +186,22 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
}
}
+ setMode(mode: string): void {
+ this.preferredMode = mode;
+
+ if (this.cachedModel) {
+ this.cachedModel.setMode(mode);
+ }
+ }
+
+ getMode(): string | undefined {
+ if (this.cachedModel) {
+ return this.cachedModel.getMode();
+ }
+
+ return this.preferredMode;
+ }
+
resolve(): Promise {
// Join a model resolve if we have had one before
@@ -209,7 +217,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
}
private createModel(): UntitledEditorModel {
- const model = this._register(this.instantiationService.createInstance(UntitledEditorModel, this.modeId, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding));
+ const model = this._register(this.instantiationService.createInstance(UntitledEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding));
// re-emit some events from the model
this._register(model.onDidChangeContent(() => this._onDidModelChangeContent.fire()));
@@ -235,7 +243,8 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport
}
dispose(): void {
- this.modelResolve = undefined;
+ this.cachedModel = null;
+ this.modelResolve = null;
super.dispose();
}
diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts
index 4f8901e3001b0..a658a8f379387 100644
--- a/src/vs/workbench/common/editor/untitledEditorModel.ts
+++ b/src/vs/workbench/common/editor/untitledEditorModel.ts
@@ -6,9 +6,8 @@
import { IEncodingSupport } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { URI } from 'vs/base/common/uri';
-import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files';
-import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
+import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { Event, Emitter } from 'vs/base/common/event';
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -37,7 +36,7 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin
private configuredEncoding: string;
constructor(
- private readonly modeId: string,
+ private readonly preferredMode: string,
private readonly resource: URI,
private _hasAssociatedFilePath: boolean,
private readonly initialValue: string,
@@ -58,14 +57,6 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin
return this._hasAssociatedFilePath;
}
- protected getOrCreateMode(modeService: IModeService, modeId: string, firstLineText?: string): ILanguageSelection {
- if (!modeId || modeId === PLAINTEXT_MODE_ID) {
- return modeService.createByFilepathOrFirstLine(this.resource.fsPath, firstLineText); // lookup mode via resource path if the provided modeId is unspecific
- }
-
- return super.getOrCreateMode(modeService, modeId, firstLineText);
- }
-
private registerListeners(): void {
// Config Changes
@@ -88,12 +79,12 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin
return this.versionId;
}
- getModeId(): string | null {
+ getMode(): string | undefined {
if (this.textEditorModel) {
- return this.textEditorModel.getLanguageIdentifier().language;
+ return this.textEditorModel.getModeId();
}
- return this.modeId;
+ return this.preferredMode;
}
getEncoding(): string {
@@ -134,36 +125,44 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin
this.contentChangeEventScheduler.schedule();
}
+ backup(): Promise {
+ if (this.isResolved()) {
+ return this.backupFileService.backupResource(this.resource, this.createSnapshot(), this.versionId);
+ }
+
+ return Promise.resolve();
+ }
+
load(): Promise {
// Check for backups first
- return this.backupFileService.loadBackupResource(this.resource).then((backupResource) => {
+ return this.backupFileService.loadBackupResource(this.resource).then(backupResource => {
if (backupResource) {
return this.backupFileService.resolveBackupContent(backupResource);
}
- return undefined;
- }).then(backupTextBufferFactory => {
- const hasBackup = !!backupTextBufferFactory;
+ return Promise.resolve(undefined);
+ }).then(backup => {
+ const hasBackup = !!backup;
// untitled associated to file path are dirty right away as well as untitled with content
- this.setDirty(this._hasAssociatedFilePath || hasBackup);
+ this.setDirty(this._hasAssociatedFilePath || hasBackup || !!this.initialValue);
let untitledContents: ITextBufferFactory;
- if (backupTextBufferFactory) {
- untitledContents = backupTextBufferFactory;
+ if (backup) {
+ untitledContents = backup.value;
} else {
untitledContents = createTextBufferFactory(this.initialValue || '');
}
// Create text editor model if not yet done
if (!this.textEditorModel) {
- this.createTextEditorModel(untitledContents, this.resource, this.modeId);
+ this.createTextEditorModel(untitledContents, this.resource, this.preferredMode);
}
// Otherwise update
else {
- this.updateTextEditorModel(untitledContents);
+ this.updateTextEditorModel(untitledContents, this.preferredMode);
}
// Encoding
diff --git a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts b/src/vs/workbench/contrib/backup/common/backupModelTracker.ts
index 2b245986c9b03..aa16c7330367c 100644
--- a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts
+++ b/src/vs/workbench/contrib/backup/common/backupModelTracker.ts
@@ -66,10 +66,7 @@ export class BackupModelTracker extends Disposable implements IWorkbenchContribu
if (!this.configuredAutoSaveAfterDelay) {
const model = this.textFileService.models.get(event.resource);
if (model) {
- const snapshot = model.createSnapshot();
- if (snapshot) {
- this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
- }
+ model.backup();
}
}
}
@@ -77,12 +74,7 @@ export class BackupModelTracker extends Disposable implements IWorkbenchContribu
private onUntitledModelChanged(resource: Uri): void {
if (this.untitledEditorService.isDirty(resource)) {
- this.untitledEditorService.loadOrCreate({ resource }).then(model => {
- const snapshot = model.createSnapshot();
- if (snapshot) {
- this.backupFileService.backupResource(resource, snapshot, model.getVersionId());
- }
- });
+ this.untitledEditorService.loadOrCreate({ resource }).then(model => model.backup());
} else {
this.discardBackup(resource);
}
diff --git a/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.css b/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.css
index 113a5a1fbb440..2804246129860 100644
--- a/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.css
+++ b/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.css
@@ -3,43 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-.monaco-editor .codelens-decoration {
- overflow: hidden;
- display: inline-block;
- text-overflow: ellipsis;
-}
-
-.monaco-editor .codelens-decoration > span,
-.monaco-editor .codelens-decoration > a {
- -moz-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
- white-space: nowrap;
- vertical-align: sub;
-}
-
-.monaco-editor .codelens-decoration > a {
- text-decoration: none;
-}
-
-.monaco-editor .codelens-decoration > a:hover {
- text-decoration: underline;
- cursor: pointer;
-}
-
-.monaco-editor .codelens-decoration.invisible-cl {
- opacity: 0;
-}
-
-@keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
-@-moz-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
-@-o-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
-@-webkit-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
-
-.monaco-editor .codelens-decoration.fadein {
- -webkit-animation: fadein 0.5s linear;
- -moz-animation: fadein 0.5s linear;
- -o-animation: fadein 0.5s linear;
- animation: fadein 0.5s linear;
+.monaco-editor .code-inset {
+ z-index: 10;
}
diff --git a/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.ts b/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.ts
index 5522eb6a8403d..d5e16dc47a9bd 100644
--- a/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.ts
+++ b/src/vs/workbench/contrib/codeinset/electron-browser/codeInsetWidget.ts
@@ -155,6 +155,7 @@ export class CodeInsetWidget {
}
const div = document.createElement('div');
+ div.className = 'code-inset';
webview.mountTo(div);
webview.onMessage((e: { type: string, payload: any }) => {
// The webview contents can use a "size-info" message to report its size.
diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts
index 5a106b857a707..ede015a2dc1ba 100644
--- a/src/vs/workbench/contrib/files/browser/fileActions.ts
+++ b/src/vs/workbench/contrib/files/browser/fileActions.ts
@@ -860,7 +860,7 @@ class ClipboardContentProvider implements ITextModelContentProvider {
) { }
provideTextContent(resource: URI): Promise {
- const model = this.modelService.createModel(this.clipboardService.readText(), this.modeService.create('text/plain'), resource);
+ const model = this.modelService.createModel(this.clipboardService.readText(), this.modeService.createByFilepathOrFirstLine(resource.path), resource);
return Promise.resolve(model);
}
diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts
index e090fa12cc22d..ef84bbdaf897c 100644
--- a/src/vs/workbench/contrib/files/browser/files.contribution.ts
+++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts
@@ -42,8 +42,8 @@ import { Schemas } from 'vs/base/common/network';
// Viewlet Action
export class OpenExplorerViewletAction extends ShowViewletAction {
- public static readonly ID = VIEWLET_ID;
- public static readonly LABEL = nls.localize('showExplorerViewlet', "Show Explorer");
+ static readonly ID = VIEWLET_ID;
+ static readonly LABEL = nls.localize('showExplorerViewlet', "Show Explorer");
constructor(
id: string,
@@ -124,8 +124,8 @@ Registry.as(EditorExtensions.Editors).registerEditor(
// Register default file input factory
Registry.as(EditorInputExtensions.EditorInputFactories).registerFileInputFactory({
- createFileInput: (resource, encoding, instantiationService): IFileEditorInput => {
- return instantiationService.createInstance(FileEditorInput, resource, encoding);
+ createFileInput: (resource, encoding, mode, instantiationService): IFileEditorInput => {
+ return instantiationService.createInstance(FileEditorInput, resource, encoding, mode);
},
isFileInput: (obj): obj is IFileEditorInput => {
@@ -137,6 +137,7 @@ interface ISerializedFileInput {
resource: string;
resourceJSON: object;
encoding?: string;
+ modeId?: string;
}
// Register Editor Input Factory
@@ -144,25 +145,27 @@ class FileEditorInputFactory implements IEditorInputFactory {
constructor() { }
- public serialize(editorInput: EditorInput): string {
+ serialize(editorInput: EditorInput): string {
const fileEditorInput = editorInput;
const resource = fileEditorInput.getResource();
const fileInput: ISerializedFileInput = {
resource: resource.toString(), // Keep for backwards compatibility
resourceJSON: resource.toJSON(),
- encoding: fileEditorInput.getEncoding()
+ encoding: fileEditorInput.getEncoding(),
+ modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data
};
return JSON.stringify(fileInput);
}
- public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput {
+ deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput {
return instantiationService.invokeFunction(accessor => {
const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput);
const resource = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource);
const encoding = fileInput.encoding;
+ const mode = fileInput.modeId;
- return accessor.get(IEditorService).createInput({ resource, encoding, forceFile: true }) as FileEditorInput;
+ return accessor.get(IEditorService).createInput({ resource, encoding, mode, forceFile: true }) as FileEditorInput;
});
}
}
diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts
index 2c5d1068a2089..02eab17958bd9 100644
--- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts
+++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts
@@ -24,8 +24,11 @@ import { ILabelService } from 'vs/platform/label/common/label';
*/
export class FileEditorInput extends EditorInput implements IFileEditorInput {
private preferredEncoding: string;
+ private preferredMode: string;
+
private forceOpenAsBinary: boolean;
private forceOpenAsText: boolean;
+
private textModelReference: Promise> | null;
private name: string;
@@ -35,6 +38,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
constructor(
private resource: URI,
preferredEncoding: string | undefined,
+ preferredMode: string | undefined,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITextFileService private readonly textFileService: ITextFileService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@@ -46,6 +50,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
this.setPreferredEncoding(preferredEncoding);
}
+ if (preferredMode) {
+ this.setPreferredMode(preferredMode);
+ }
+
this.registerListeners();
}
@@ -89,7 +97,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
}
setEncoding(encoding: string, mode: EncodingMode): void {
- this.preferredEncoding = encoding;
+ this.setPreferredEncoding(encoding);
const textModel = this.textFileService.models.get(this.resource);
if (textModel) {
@@ -102,6 +110,24 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
this.forceOpenAsText = true; // encoding is a good hint to open the file as text
}
+ getPreferredMode(): string | undefined {
+ return this.preferredMode;
+ }
+
+ setMode(mode: string): void {
+ this.setPreferredMode(mode);
+
+ const textModel = this.textFileService.models.get(this.resource);
+ if (textModel) {
+ textModel.setMode(mode);
+ }
+ }
+
+ setPreferredMode(mode: string): void {
+ this.preferredMode = mode;
+ this.forceOpenAsText = true; // mode is a good hint to open the file as text
+ }
+
setForceOpenAsText(): void {
this.forceOpenAsText = true;
this.forceOpenAsBinary = false;
@@ -251,6 +277,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
// Resolve as text
return this.textFileService.models.loadOrCreate(this.resource, {
+ mode: this.preferredMode,
encoding: this.preferredEncoding,
reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model
allowBinary: this.forceOpenAsText,
diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts
index a134f8f6ab2e5..674d1f2a1e406 100644
--- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts
+++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts
@@ -16,6 +16,7 @@ import { FileOperationResult, FileOperationError } from 'vs/platform/files/commo
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
+import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
class ServiceAccessor {
constructor(
@@ -36,10 +37,10 @@ suite('Files - FileEditorInput', () => {
accessor = instantiationService.createInstance(ServiceAccessor);
});
- test('Basics', function () {
- let input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined);
- const otherInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, 'foo/bar/otherfile.js'), undefined);
- const otherInputSame = instantiationService.createInstance(FileEditorInput, toResource.call(this, 'foo/bar/file.js'), undefined);
+ test('Basics', async function () {
+ let input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined, undefined);
+ const otherInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, 'foo/bar/otherfile.js'), undefined, undefined);
+ const otherInputSame = instantiationService.createInstance(FileEditorInput, toResource.call(this, 'foo/bar/file.js'), undefined, undefined);
assert(input.matches(input));
assert(input.matches(otherInputSame));
@@ -54,52 +55,65 @@ suite('Files - FileEditorInput', () => {
assert.strictEqual(toResource.call(this, '/foo/bar/file.js').fsPath, input.getResource().fsPath);
assert(input.getResource() instanceof URI);
- input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar.html'), undefined);
+ input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar.html'), undefined, undefined);
- const inputToResolve: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined);
- const sameOtherInput: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined);
+ const inputToResolve: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined, undefined);
+ const sameOtherInput: FileEditorInput = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined, undefined);
- return inputToResolve.resolve().then(resolved => {
- assert.ok(inputToResolve.isResolved());
+ let resolved = await inputToResolve.resolve();
+ assert.ok(inputToResolve.isResolved());
- const resolvedModelA = resolved;
- return inputToResolve.resolve().then(resolved => {
- assert(resolvedModelA === resolved); // OK: Resolved Model cached globally per input
+ const resolvedModelA = resolved;
+ resolved = await inputToResolve.resolve();
+ assert(resolvedModelA === resolved); // OK: Resolved Model cached globally per input
- return sameOtherInput.resolve().then(otherResolved => {
- assert(otherResolved === resolvedModelA); // OK: Resolved Model cached globally per input
+ const otherResolved = await sameOtherInput.resolve();
+ assert(otherResolved === resolvedModelA); // OK: Resolved Model cached globally per input
+ inputToResolve.dispose();
- inputToResolve.dispose();
+ resolved = await inputToResolve.resolve();
+ assert(resolvedModelA === resolved); // Model is still the same because we had 2 clients
+ inputToResolve.dispose();
+ sameOtherInput.dispose();
+ resolvedModelA.dispose();
- return inputToResolve.resolve().then(resolved => {
- assert(resolvedModelA === resolved); // Model is still the same because we had 2 clients
+ resolved = await inputToResolve.resolve();
+ assert(resolvedModelA !== resolved); // Different instance, because input got disposed
- inputToResolve.dispose();
- sameOtherInput.dispose();
+ const stat = (resolved as TextFileEditorModel).getStat();
+ resolved = await inputToResolve.resolve();
+ await timeout(0);
+ assert(stat !== (resolved as TextFileEditorModel).getStat()); // Different stat, because resolve always goes to the server for refresh
+ });
+
+ test('preferred mode', async function () {
+ const mode = 'file-input-test';
+ ModesRegistry.registerLanguage({
+ id: mode,
+ });
- resolvedModelA.dispose();
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined, mode);
+ assert.equal(input.getPreferredMode(), mode);
- return inputToResolve.resolve().then(resolved => {
- assert(resolvedModelA !== resolved); // Different instance, because input got disposed
+ const model = await input.resolve() as TextFileEditorModel;
+ assert.equal(model.textEditorModel!.getModeId(), mode);
- let stat = (resolved as TextFileEditorModel).getStat();
- return inputToResolve.resolve().then(resolved => {
- return timeout(0).then(() => { // due to file editor input using `reload: { async: true }`
- assert(stat !== (resolved as TextFileEditorModel).getStat()); // Different stat, because resolve always goes to the server for refresh
- });
- });
- });
- });
- });
- });
- });
+ input.setMode('text');
+ assert.equal(input.getPreferredMode(), 'text');
+ assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID);
+
+ const input2 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/file.js'), undefined, undefined);
+ input2.setPreferredMode(mode);
+
+ const model2 = await input2.resolve() as TextFileEditorModel;
+ assert.equal(model2.textEditorModel!.getModeId(), mode);
});
test('matches', function () {
- const input1 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
- const input2 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
- const input3 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/other.js'), undefined);
- const input2Upper = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/UPDATEFILE.js'), undefined);
+ const input1 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
+ const input2 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
+ const input3 = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/other.js'), undefined, undefined);
+ const input2Upper = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/UPDATEFILE.js'), undefined, undefined);
assert.strictEqual(input1.matches(null), false);
assert.strictEqual(input1.matches(input1), true);
@@ -109,70 +123,58 @@ suite('Files - FileEditorInput', () => {
assert.strictEqual(input1.matches(input2Upper), false);
});
- test('getEncoding/setEncoding', function () {
- const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
+ test('getEncoding/setEncoding', async function () {
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
input.setEncoding('utf16', EncodingMode.Encode);
assert.equal(input.getEncoding(), 'utf16');
- return input.resolve().then((resolved: TextFileEditorModel) => {
- assert.equal(input.getEncoding(), resolved.getEncoding());
-
- resolved.dispose();
- });
+ const resolved = await input.resolve() as TextFileEditorModel;
+ assert.equal(input.getEncoding(), resolved.getEncoding());
+ resolved.dispose();
});
- test('save', function () {
- const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
+ test('save', async function () {
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
- return input.resolve().then((resolved: TextFileEditorModel) => {
- resolved.textEditorModel!.setValue('changed');
- assert.ok(input.isDirty());
+ const resolved = await input.resolve() as TextFileEditorModel;
+ resolved.textEditorModel!.setValue('changed');
+ assert.ok(input.isDirty());
- return input.save().then(() => {
- assert.ok(!input.isDirty());
-
- resolved.dispose();
- });
- });
+ await input.save();
+ assert.ok(!input.isDirty());
+ resolved.dispose();
});
- test('revert', function () {
- const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
+ test('revert', async function () {
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
- return input.resolve().then((resolved: TextFileEditorModel) => {
- resolved.textEditorModel!.setValue('changed');
- assert.ok(input.isDirty());
+ const resolved = await input.resolve() as TextFileEditorModel;
+ resolved.textEditorModel!.setValue('changed');
+ assert.ok(input.isDirty());
- return input.revert().then(() => {
- assert.ok(!input.isDirty());
-
- resolved.dispose();
- });
- });
+ await input.revert();
+ assert.ok(!input.isDirty());
+ resolved.dispose();
});
- test('resolve handles binary files', function () {
- const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
+ test('resolve handles binary files', async function () {
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
accessor.textFileService.setResolveTextContentErrorOnce(new TextFileOperationError('error', TextFileOperationResult.FILE_IS_BINARY));
- return input.resolve().then(resolved => {
- assert.ok(resolved);
-
- resolved.dispose();
- });
+ const resolved = await input.resolve();
+ assert.ok(resolved);
+ resolved.dispose();
});
- test('resolve handles too large files', function () {
- const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined);
+ test('resolve handles too large files', async function () {
+ const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined);
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_TOO_LARGE));
- return input.resolve().then(resolved => {
- assert.ok(resolved);
-
- resolved.dispose();
- });
+ const resolved = await input.resolve();
+ assert.ok(resolved);
+ resolved.dispose();
});
});
diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts
index 0b6ef5a245afc..37762b7c66fce 100644
--- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts
+++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts
@@ -201,19 +201,34 @@ async function showFormatterPick(accessor: ServicesAccessor, model: ITextModel,
const overrides = { resource: model.uri, overrideIdentifier: model.getModeId() };
const defaultFormatter = configService.getValue(DefaultFormatter.configName, overrides);
+ let defaultFormatterPick: IIndexedPick | undefined;
+
const picks = formatters.map((provider, index) => {
- return {
+ const isDefault = ExtensionIdentifier.equals(provider.extensionId, defaultFormatter);
+ const pick = {
index,
label: provider.displayName || '',
- description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined,
+ description: isDefault ? nls.localize('def', "(default)") : undefined,
};
+
+ if (isDefault) {
+ // autofocus default pick
+ defaultFormatterPick = pick;
+ }
+
+ return pick;
});
const configurePick: IQuickPickItem = {
label: nls.localize('config', "Configure Default Formatter...")
};
- const pick = await quickPickService.pick([...picks, { type: 'separator' }, configurePick], { placeHolder: nls.localize('format.placeHolder', "Select a formatter") });
+ const pick = await quickPickService.pick([...picks, { type: 'separator' }, configurePick],
+ {
+ placeHolder: nls.localize('format.placeHolder', "Select a formatter"),
+ activeItem: defaultFormatterPick
+ }
+ );
if (!pick) {
// dismissed
return undefined;
diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts
index c33c399e893ff..c1bbbd2f7c0cc 100644
--- a/src/vs/workbench/contrib/output/browser/logViewer.ts
+++ b/src/vs/workbench/contrib/output/browser/logViewer.ts
@@ -28,7 +28,7 @@ export class LogViewerInput extends ResourceEditorInput {
constructor(private outputChannelDescriptor: IFileOutputChannelDescriptor,
@ITextModelService textModelResolverService: ITextModelService
) {
- super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), textModelResolverService);
+ super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), undefined, textModelResolverService);
}
public getTypeId(): string {
diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts
index cb5a3e6410814..eb8ef512efd53 100644
--- a/src/vs/workbench/contrib/output/browser/outputServices.ts
+++ b/src/vs/workbench/contrib/output/browser/outputServices.ts
@@ -244,7 +244,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo
private createInput(channel: IOutputChannel): ResourceEditorInput {
const resource = URI.from({ scheme: OUTPUT_SCHEME, path: channel.id });
- return this.instantiationService.createInstance(ResourceEditorInput, nls.localize('output', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), resource);
+ return this.instantiationService.createInstance(ResourceEditorInput, nls.localize('output', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), resource, undefined);
}
private saveState(): void {
diff --git a/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts
index 831cad8a43e8c..bd40d800b9448 100644
--- a/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts
+++ b/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts
@@ -52,6 +52,7 @@ export class PerfviewInput extends ResourceEditorInput {
localize('name', "Startup Performance"),
null,
PerfviewInput.Uri,
+ undefined,
textModelResolverService
);
}
diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts
index 8746f2124976f..933e47b2e7370 100644
--- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts
+++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts
@@ -203,7 +203,7 @@ export class OpenFileHandler extends QuickOpenHandler {
const queryOptions: IFileQueryBuilderOptions = {
_reason: 'openFileHandler',
extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService),
- filePattern: query.value,
+ filePattern: query.original,
cacheKey
};
diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts
index 688ffaf6fe82e..4a7a6f9a9a73f 100644
--- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts
+++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts
@@ -1360,6 +1360,7 @@ class TaskService extends Disposable implements ITaskService {
this.modelService, this.configurationResolverService, this.telemetryService,
this.contextService, this._environmentService,
TaskService.OutputChannelId,
+ this.configurationService,
(workspaceFolder: IWorkspaceFolder) => {
if (!workspaceFolder) {
return undefined;
@@ -2012,7 +2013,7 @@ class TaskService extends Disposable implements ITaskService {
Severity.Info,
nls.localize('TaskService.ignoredFolder', 'The following workspace folders are ignored since they use task version 0.1.0: {0}', this.ignoredWorkspaceFolders.map(f => f.name).join(', ')),
[{
- label: nls.localize('TaskService.notAgain', 'Don\'t Show Again'),
+ label: nls.localize('TaskService.notAgain', "Don't Show Again"),
isSecondary: true,
run: () => {
this.storageService.store(TaskService.IgnoreTask010DonotShowAgain_key, true, StorageScope.WORKSPACE);
diff --git a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts
index b0302bd3d2396..2509f820f032c 100644
--- a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts
+++ b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts
@@ -44,6 +44,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { Schemas } from 'vs/base/common/network';
import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
+import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
interface TerminalData {
terminal: ITerminalInstance;
@@ -171,7 +172,8 @@ export class TerminalTaskSystem implements ITaskSystem {
private contextService: IWorkspaceContextService,
private environmentService: IWorkbenchEnvironmentService,
private outputChannelId: string,
- taskSystemInfoResolver: TaskSystemInfoResovler
+ private readonly configurationService: IConfigurationService,
+ taskSystemInfoResolver: TaskSystemInfoResovler,
) {
this.activeTasks = Object.create(null);
@@ -754,7 +756,7 @@ export class TerminalTaskSystem implements ITaskSystem {
let originalCommand = task.command.name;
if (isShellCommand) {
shellLaunchConfig = { name: terminalName, executable: undefined, args: undefined, waitOnExit };
- this.terminalService.configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig, platform);
+ this.terminalService.configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig, this.terminalService.getDefaultShell(platform), platform);
let shellSpecified: boolean = false;
let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell;
if (shellOptions) {
@@ -877,6 +879,9 @@ export class TerminalTaskSystem implements ITaskSystem {
if (options.env) {
shellLaunchConfig.env = options.env;
}
+
+ // Conpty doesn't do linefeeds in an expected way. Force winpty unless the user has requested otherwise.
+ shellLaunchConfig.forceWinpty = !this.configurationService.getValue('terminal.integrated.windowsAllowConptyTasks');
return shellLaunchConfig;
}
diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts
index d467ac1b6b3d3..eb7a270b494ba 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts
@@ -273,6 +273,11 @@ configurationRegistry.registerConfiguration({
description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."),
type: 'boolean',
default: false
+ },
+ 'terminal.integrated.windowsAllowConptyTasks': {
+ markdownDescription: nls.localize('terminal.integrated.windowsAllowConptyTasks', "Works in conjunction with the `#terminal.integrated.windowsEnableConpty#` setting. Both must be enabled for tasks to use conpty. Defaults to `false`."),
+ type: 'boolean',
+ default: false
}
}
});
diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts
index ff7f6b95f5b6f..07d8a21d41461 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminal.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts
@@ -6,7 +6,7 @@
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { ITerminalInstance, IWindowsShellHelper, ITerminalProcessManager, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
-import { IProcessEnvironment } from 'vs/base/common/platform';
+import { IProcessEnvironment, Platform } from 'vs/base/common/platform';
export const ITerminalInstanceService = createDecorator('terminalInstanceService');
@@ -17,6 +17,7 @@ export interface ITerminalInstanceService {
createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper;
createTerminalProcessManager(id: number, configHelper: ITerminalConfigHelper): ITerminalProcessManager;
createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess;
+ getDefaultShell(p: Platform): string;
}
export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper {
diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts
index cd39294d5292d..716e1f872cb79 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts
@@ -227,9 +227,9 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
return !!isWorkspaceShellAllowed;
}
- public mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride: platform.Platform = platform.platform): void {
+ public mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, defaultShell: string, platformOverride: platform.Platform = platform.platform): void {
const isWorkspaceShellAllowed = this.checkWorkspaceShellPermissions(platformOverride === platform.Platform.Windows ? platform.OperatingSystem.Windows : (platformOverride === platform.Platform.Mac ? platform.OperatingSystem.Macintosh : platform.OperatingSystem.Linux));
- mergeDefaultShellPathAndArgs(shell, (key) => this._workspaceConfigurationService.inspect(key), isWorkspaceShellAllowed, platformOverride);
+ mergeDefaultShellPathAndArgs(shell, (key) => this._workspaceConfigurationService.inspect(key), isWorkspaceShellAllowed, defaultShell, platformOverride);
}
private _toInteger(source: any, minimum: number, maximum: number, fallback: number): number {
diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
index 28b1d731cf739..94b419556afcc 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
@@ -25,7 +25,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
-import { IShellLaunchConfig, ITerminalDimensions, ITerminalInstance, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_PANEL_ID, IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal';
+import { IShellLaunchConfig, ITerminalDimensions, ITerminalInstance, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_PANEL_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE } from 'vs/workbench/contrib/terminal/common/terminal';
import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
@@ -440,6 +440,13 @@ export class TerminalInstance implements ITerminalInstance {
}
if (this._processManager.os === platform.OperatingSystem.Windows) {
this._xterm.winptyCompatInit();
+ // Force line data to be sent when the cursor is moved, the main purpose for
+ // this is because ConPTY will often not do a line feed but instead move the
+ // cursor, in which case we still want to send the current line's data to tasks.
+ this._xterm.addCsiHandler('H', () => {
+ this._onCursorMove();
+ return false;
+ });
}
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager);
});
@@ -970,10 +977,32 @@ export class TerminalInstance implements ITerminalInstance {
}
this._isExiting = true;
- let exitCodeMessage: string;
+ let exitCodeMessage: string | undefined;
+ // Create exit code message
if (exitCode) {
- exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
+ if (exitCode === SHELL_PATH_INVALID_EXIT_CODE) {
+ exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidPath', 'The terminal shell path does not exist: {0}', this._shellLaunchConfig.executable);
+ } else if (this._processManager && this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
+ let args = '';
+ if (typeof this._shellLaunchConfig.args === 'string') {
+ args = ` ${this._shellLaunchConfig.args}`;
+ } else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) {
+ args = ' ' + this._shellLaunchConfig.args.map(a => {
+ if (typeof a === 'string' && a.indexOf(' ') !== -1) {
+ return `'${a}'`;
+ }
+ return a;
+ }).join(' ');
+ }
+ if (this._shellLaunchConfig.executable) {
+ exitCodeMessage = nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode);
+ } else {
+ exitCodeMessage = nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode);
+ }
+ } else {
+ exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
+ }
}
this._logService.debug(`Terminal process exit (id: ${this.id})${this._processManager ? ' state ' + this._processManager.processState : ''}`);
@@ -981,8 +1010,8 @@ export class TerminalInstance implements ITerminalInstance {
// Only trigger wait on exit when the exit was *not* triggered by the
// user (via the `workbench.action.terminal.kill` command).
if (this._shellLaunchConfig.waitOnExit && (!this._processManager || this._processManager.processState !== ProcessState.KILLED_BY_USER)) {
- if (exitCode) {
- this._xterm.writeln(exitCodeMessage!);
+ if (exitCodeMessage) {
+ this._xterm.writeln(exitCodeMessage);
}
if (typeof this._shellLaunchConfig.waitOnExit === 'string') {
let message = this._shellLaunchConfig.waitOnExit;
@@ -997,29 +1026,14 @@ export class TerminalInstance implements ITerminalInstance {
}
} else {
this.dispose();
- if (exitCode) {
+ if (exitCodeMessage) {
if (this._processManager && this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
- let args = '';
- if (typeof this._shellLaunchConfig.args === 'string') {
- args = this._shellLaunchConfig.args;
- } else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) {
- args = ' ' + this._shellLaunchConfig.args.map(a => {
- if (typeof a === 'string' && a.indexOf(' ') !== -1) {
- return `'${a}'`;
- }
- return a;
- }).join(' ');
- }
- if (this._shellLaunchConfig.executable) {
- this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode));
- } else {
- this._notificationService.error(nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode));
- }
+ this._notificationService.error(exitCodeMessage);
} else {
if (this._configHelper.config.showExitAlert) {
- this._notificationService.error(exitCodeMessage!);
+ this._notificationService.error(exitCodeMessage);
} else {
- console.warn(exitCodeMessage!);
+ console.warn(exitCodeMessage);
}
}
}
@@ -1108,6 +1122,11 @@ export class TerminalInstance implements ITerminalInstance {
}
}
+ private _onCursorMove(): void {
+ const buffer = (this._xterm._core.buffer);
+ this._sendLineData(buffer, buffer.ybase + buffer.y);
+ }
+
private _sendLineData(buffer: any, lineIndex: number): void {
let lineData = buffer.translateBufferLineToString(lineIndex, true);
while (lineIndex >= 0 && buffer.lines.get(lineIndex--).isWrapped) {
diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts
index 7bcf1281bdd19..04447e63c4a78 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts
@@ -169,7 +169,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
private _launchProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): ITerminalChildProcess {
if (!shellLaunchConfig.executable) {
- this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig);
+ this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig, this._terminalInstanceService.getDefaultShell(platform.platform));
}
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file);
@@ -182,7 +182,8 @@ export class TerminalProcessManager implements ITerminalProcessManager {
const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.setLocaleVariables);
this._logService.debug(`Terminal process launching`, shellLaunchConfig, initialCwd, cols, rows, env);
- return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, this._configHelper.config.windowsEnableConpty);
+ const useConpty = (shellLaunchConfig.forceWinpty !== true) && this._configHelper.config.windowsEnableConpty;
+ return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty);
}
public setDimensions(cols: number, rows: number): void {
diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts
index f280a830240d7..094dcdb7e4539 100644
--- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts
+++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts
@@ -44,7 +44,7 @@ export abstract class TerminalService extends CommonTerminalService implements I
super(contextKeyService, panelService, lifecycleService, storageService, notificationService, dialogService, extensionService, fileService, remoteAgentService);
}
- protected abstract _getDefaultShell(p: platform.Platform): string;
+ public abstract getDefaultShell(p: platform.Platform): string;
public createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig);
@@ -101,7 +101,7 @@ export abstract class TerminalService extends CommonTerminalService implements I
}
// Never suggest if the setting is non-default already (ie. they set the setting manually)
- if (this.configHelper.config.shell.windows !== this._getDefaultShell(platform.Platform.Windows)) {
+ if (this.configHelper.config.shell.windows !== this.getDefaultShell(platform.Platform.Windows)) {
this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true, StorageScope.GLOBAL);
return;
}
diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts
index a1932f3934544..4cc7a9629127a 100644
--- a/src/vs/workbench/contrib/terminal/common/terminal.ts
+++ b/src/vs/workbench/contrib/terminal/common/terminal.ts
@@ -58,6 +58,7 @@ export const TERMINAL_CONFIG_SECTION = 'terminal.integrated';
export const DEFAULT_LETTER_SPACING = 0;
export const MINIMUM_LETTER_SPACING = -5;
export const DEFAULT_LINE_HEIGHT = 1;
+export const SHELL_PATH_INVALID_EXIT_CODE = -1;
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
@@ -112,7 +113,7 @@ export interface ITerminalConfigHelper {
/**
* Merges the default shell path and args into the provided launch configuration
*/
- mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, platformOverride?: platform.Platform): void;
+ mergeDefaultShellPathAndArgs(shell: IShellLaunchConfig, defaultShell: string, platformOverride?: platform.Platform): void;
/** Sets whether a workspace shell configuration is allowed or not */
setWorkspaceShellAllowed(isAllowed: boolean): void;
checkWorkspaceShellPermissions(osOverride?: platform.OperatingSystem): boolean;
@@ -192,6 +193,12 @@ export interface IShellLaunchConfig {
* provided as nothing will be inherited from the process or any configuration.
*/
strictEnv?: boolean;
+
+ /**
+ * Moving forward, conpty will be the default. However, there are cases where conpty is not ready
+ * to be the default. This property will force winpty to be used, even when conpty would normally be used.
+ */
+ forceWinpty?: boolean;
}
export interface ITerminalService {
@@ -253,6 +260,7 @@ export interface ITerminalService {
findPrevious(): void;
setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
+ getDefaultShell(p: platform.Platform): string;
selectDefaultWindowsShell(): Promise;
setWorkspaceShellAllowed(isAllowed: boolean): void;
@@ -688,7 +696,6 @@ export const enum ProcessState {
KILLED_BY_PROCESS
}
-
export interface ITerminalProcessExtHostProxy extends IDisposable {
readonly terminalId: number;
diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts
index ced4d525a88c2..1597d17998f40 100644
--- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts
+++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts
@@ -164,15 +164,14 @@ export function mergeDefaultShellPathAndArgs(
shell: IShellLaunchConfig,
fetchSetting: (key: string) => { user: string | string[] | undefined, value: string | string[] | undefined, default: string | string[] | undefined },
isWorkspaceShellAllowed: boolean,
+ defaultShell: string,
platformOverride: platform.Platform = platform.platform
): void {
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellConfigValue = fetchSetting(`terminal.integrated.shell.${platformKey}`);
- // const shellConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.shell.${platformKey}`);
const shellArgsConfigValue = fetchSetting(`terminal.integrated.shellArgs.${platformKey}`);
- // const shellArgsConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.shellArgs.${platformKey}`);
- shell.executable = (isWorkspaceShellAllowed ? shellConfigValue.value : shellConfigValue.user) || shellConfigValue.default;
+ shell.executable = (isWorkspaceShellAllowed ? shellConfigValue.value : shellConfigValue.user) || (shellConfigValue.default || defaultShell);
shell.args = (isWorkspaceShellAllowed ? shellArgsConfigValue.value : shellArgsConfigValue.user) || shellArgsConfigValue.default;
// Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's
diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts
index 2fc857e432bbf..01b3254479f38 100644
--- a/src/vs/workbench/contrib/terminal/common/terminalService.ts
+++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts
@@ -17,7 +17,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
-import { isWindows } from 'vs/base/common/platform';
+import { isWindows, Platform } from 'vs/base/common/platform';
import { basename } from 'vs/base/common/path';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { timeout } from 'vs/base/common/async';
@@ -106,6 +106,7 @@ export abstract class TerminalService implements ITerminalService {
public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
+ public abstract getDefaultShell(platform: Platform): string;
public abstract selectDefaultWindowsShell(): Promise;
public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
@@ -426,6 +427,9 @@ export abstract class TerminalService implements ITerminalService {
return Promise.resolve(null);
}
const current = potentialPaths.shift();
+ if (current! === '') {
+ return this._validateShellPaths(label, potentialPaths);
+ }
return this._fileService.exists(URI.file(current!)).then(exists => {
if (!exists) {
return this._validateShellPaths(label, potentialPaths);
diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts
index a55b93caa40c7..616bf9eba8a21 100644
--- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts
+++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts
@@ -22,19 +22,19 @@ configurationRegistry.registerConfiguration({
type: 'object',
properties: {
'terminal.integrated.shell.linux': {
- markdownDescription: nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
- type: 'string',
- default: getDefaultShell(platform.Platform.Linux)
+ markdownDescription: nls.localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getDefaultShell(platform.Platform.Linux)),
+ type: ['string', 'null'],
+ default: null
},
'terminal.integrated.shell.osx': {
- markdownDescription: nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
- type: 'string',
- default: getDefaultShell(platform.Platform.Mac)
+ markdownDescription: nls.localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getDefaultShell(platform.Platform.Mac)),
+ type: ['string', 'null'],
+ default: null
},
'terminal.integrated.shell.windows': {
- markdownDescription: nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
- type: 'string',
- default: getDefaultShell(platform.Platform.Windows)
+ markdownDescription: nls.localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", getDefaultShell(platform.Platform.Windows)),
+ type: ['string', 'null'],
+ default: null
}
}
});
diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts
index 9fedff9cb212e..b3c85613d8718 100644
--- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts
+++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts
@@ -10,9 +10,10 @@ import { ITerminalInstance, IWindowsShellHelper, ITerminalConfigHelper, ITermina
import { WindowsShellHelper } from 'vs/workbench/contrib/terminal/node/windowsShellHelper';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
-import { IProcessEnvironment } from 'vs/base/common/platform';
+import { IProcessEnvironment, Platform } from 'vs/base/common/platform';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
import * as typeAheadAddon from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon';
+import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal';
let Terminal: typeof XTermTerminal;
@@ -56,4 +57,8 @@ export class TerminalInstanceService implements ITerminalInstanceService {
public createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess {
return new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, windowsEnableConpty);
}
+
+ public getDefaultShell(p: Platform): string {
+ return getDefaultShell(p);
+ }
}
\ No newline at end of file
diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts
index d7e3026986edd..6ead83f5b676e 100644
--- a/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts
+++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts
@@ -98,7 +98,7 @@ export class TerminalService extends BrowserTerminalService implements ITerminal
});
}
- protected _getDefaultShell(p: platform.Platform): string {
+ public getDefaultShell(p: platform.Platform): string {
return getDefaultShell(p);
}
@@ -117,7 +117,28 @@ export class TerminalService extends BrowserTerminalService implements ITerminal
});
}
- private _detectWindowsShells(): Promise {
+ /**
+ * Get the executable file path of shell from registry.
+ * @param shellName The shell name to get the executable file path
+ * @returns `[]` or `[ 'path' ]`
+ */
+ private async _getShellPathFromRegistry(shellName: string): Promise {
+ const Registry = await import('vscode-windows-registry');
+
+ try {
+ const shellPath = Registry.GetStringRegKey('HKEY_LOCAL_MACHINE', `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${shellName}.exe`, '');
+
+ if (shellPath === undefined) {
+ return [];
+ }
+
+ return [shellPath];
+ } catch (error) {
+ return [];
+ }
+ }
+
+ private async _detectWindowsShells(): Promise {
// Determine the correct System32 path. We want to point to Sysnative
// when the 32-bit version of VS Code is running on a 64-bit machine.
// The reason for this is because PowerShell's important PSReadline
@@ -134,6 +155,7 @@ export class TerminalService extends BrowserTerminalService implements ITerminal
const expectedLocations = {
'Command Prompt': [`${system32Path}\\cmd.exe`],
PowerShell: [`${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`],
+ 'PowerShell Core': await this._getShellPathFromRegistry('pwsh'),
'WSL Bash': [`${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`],
'Git Bash': [
`${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`,
diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts
index c45954b11455c..eb25041699879 100644
--- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts
+++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts
@@ -17,7 +17,7 @@ import { exec } from 'child_process';
export class TerminalProcess implements ITerminalChildProcess, IDisposable {
private _exitCode: number;
private _closeTimeout: any;
- private _ptyProcess: pty.IPty;
+ private _ptyProcess: pty.IPty | undefined;
private _currentTitle: string = '';
private _processStartupComplete: Promise;
private _isDisposed: boolean = false;
@@ -69,37 +69,40 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
experimentalUseConpty: useConpty
};
- try {
- this._ptyProcess = pty.spawn(shellLaunchConfig.executable!, shellLaunchConfig.args || [], options);
- this._processStartupComplete = new Promise(c => {
- this.onProcessIdReady((pid) => {
- c();
- });
- });
- } catch (error) {
- // The only time this is expected to happen is when the file specified to launch with does not exist.
- this._exitCode = 2;
- this._queueProcessExit();
- this._processStartupComplete = Promise.resolve(undefined);
- return;
- }
- this._ptyProcess.on('data', (data) => {
+ // TODO: Need to verify whether executable is on $PATH, otherwise things like cmd.exe will break
+ // fs.stat(shellLaunchConfig.executable!, (err) => {
+ // if (err && err.code === 'ENOENT') {
+ // this._exitCode = SHELL_PATH_INVALID_EXIT_CODE;
+ // this._queueProcessExit();
+ // this._processStartupComplete = Promise.resolve(undefined);
+ // return;
+ // }
+ this.setupPtyProcess(shellLaunchConfig, options);
+ // });
+ }
+
+ private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void {
+ const ptyProcess = pty.spawn(shellLaunchConfig.executable!, shellLaunchConfig.args || [], options);
+ this._ptyProcess = ptyProcess;
+ this._processStartupComplete = new Promise(c => {
+ this.onProcessIdReady(() => c());
+ });
+ ptyProcess.on('data', (data) => {
this._onProcessData.fire(data);
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
this._queueProcessExit();
}
});
- this._ptyProcess.on('exit', (code) => {
+ ptyProcess.on('exit', (code) => {
this._exitCode = code;
this._queueProcessExit();
});
-
+ this._setupTitlePolling(ptyProcess);
// TODO: We should no longer need to delay this since pty.spawn is sync
setTimeout(() => {
- this._sendProcessId();
+ this._sendProcessId(ptyProcess);
}, 500);
- this._setupTitlePolling();
}
public dispose(): void {
@@ -114,15 +117,15 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
this._onProcessTitleChanged.dispose();
}
- private _setupTitlePolling() {
+ private _setupTitlePolling(ptyProcess: pty.IPty) {
// Send initial timeout async to give event listeners a chance to init
setTimeout(() => {
- this._sendProcessTitle();
+ this._sendProcessTitle(ptyProcess);
}, 0);
// Setup polling
this._titleInterval = setInterval(() => {
- if (this._currentTitle !== this._ptyProcess.process) {
- this._sendProcessTitle();
+ if (this._currentTitle !== ptyProcess.process) {
+ this._sendProcessTitle(ptyProcess);
}
}, 200);
}
@@ -146,7 +149,9 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
// Attempt to kill the pty, it may have already been killed at this
// point but we want to make sure
try {
- this._ptyProcess.kill();
+ if (this._ptyProcess) {
+ this._ptyProcess.kill();
+ }
} catch (ex) {
// Swallow, the pty has already been killed
}
@@ -155,15 +160,15 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
});
}
- private _sendProcessId() {
- this._onProcessIdReady.fire(this._ptyProcess.pid);
+ private _sendProcessId(ptyProcess: pty.IPty) {
+ this._onProcessIdReady.fire(ptyProcess.pid);
}
- private _sendProcessTitle(): void {
+ private _sendProcessTitle(ptyProcess: pty.IPty): void {
if (this._isDisposed) {
return;
}
- this._currentTitle = this._ptyProcess.process;
+ this._currentTitle = ptyProcess.process;
this._onProcessTitleChanged.fire(this._currentTitle);
}
@@ -176,7 +181,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
}
public input(data: string): void {
- if (this._isDisposed) {
+ if (this._isDisposed || !this._ptyProcess) {
return;
}
this._ptyProcess.write(data);
@@ -188,7 +193,9 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
}
// Ensure that cols and rows are always >= 1, this prevents a native
// exception in winpty.
- this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1));
+ if (this._ptyProcess) {
+ this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1));
+ }
}
public getInitialCwd(): Promise {
@@ -198,6 +205,10 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
public getCwd(): Promise {
if (platform.isMacintosh) {
return new Promise(resolve => {
+ if (!this._ptyProcess) {
+ resolve(this._initialCwd);
+ return;
+ }
exec('lsof -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => {
if (stdout !== '') {
resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1));
@@ -208,6 +219,10 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
if (platform.isLinux) {
return new Promise(resolve => {
+ if (!this._ptyProcess) {
+ resolve(this._initialCwd);
+ return;
+ }
fs.readlink('/proc/' + this._ptyProcess.pid + '/cwd', (err, linkedstr) => {
if (err) {
resolve(this._initialCwd);
diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts
index 756bfc7733a5f..7c65d7b895608 100644
--- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts
+++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts
@@ -236,7 +236,7 @@ class GenerateColorThemeAction extends Action {
}, null, '\t');
contents = contents.replace(/\"__/g, '//"');
- return this.editorService.openEditor({ contents, language: 'jsonc' });
+ return this.editorService.openEditor({ contents, mode: 'jsonc' });
}
}
diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js
index 133b3d28edd0d..2afb9ea3125b1 100644
--- a/src/vs/workbench/contrib/webview/browser/pre/main.js
+++ b/src/vs/workbench/contrib/webview/browser/pre/main.js
@@ -153,7 +153,7 @@ module.exports = function createWebviewManager(host) {
scrollTarget.scrollIntoView();
}
} else {
- host.postMessage('did-click-link', node.href);
+ host.postMessage('did-click-link', node.href.baseVal || node.href);
}
event.preventDefault();
break;
diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts
index de7017c8edd35..0fbffedbc3653 100644
--- a/src/vs/workbench/electron-browser/main.contribution.ts
+++ b/src/vs/workbench/electron-browser/main.contribution.ts
@@ -36,12 +36,12 @@ import { LogStorageAction } from 'vs/platform/storage/node/storageService';
if (isMacintosh) {
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open...', fileCategory);
- registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileFolderAction, OpenLocalFileFolderAction.ID, OpenLocalFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local...', fileCategory, RemoteFileDialogContext);
+ registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileFolderAction, OpenLocalFileFolderAction.ID, OpenLocalFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local...', fileCategory);
} else {
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileAction, OpenFileAction.ID, OpenFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open File...', fileCategory);
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }), 'File: Open Folder...', fileCategory);
- registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileAction, OpenLocalFileAction.ID, OpenLocalFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local File...', fileCategory, RemoteFileDialogContext);
- registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFolderAction, OpenLocalFolderAction.ID, OpenLocalFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }, RemoteFileDialogContext), 'File: Open Local Folder...', fileCategory, RemoteFileDialogContext);
+ registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileAction, OpenLocalFileAction.ID, OpenLocalFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local File...', fileCategory);
+ registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFolderAction, OpenLocalFolderAction.ID, OpenLocalFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }, RemoteFileDialogContext), 'File: Open Local Folder...', fileCategory);
}
registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenRecentAction, QuickOpenRecentAction.ID, QuickOpenRecentAction.LABEL), 'File: Quick Open Recent...', fileCategory);
diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts
index 7687183710ff4..6141ad0cfee4e 100644
--- a/src/vs/workbench/electron-browser/window.ts
+++ b/src/vs/workbench/electron-browser/window.ts
@@ -43,6 +43,8 @@ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessi
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { coalesce } from 'vs/base/common/arrays';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
+import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
+import { isEqual } from 'vs/base/common/resources';
const TextInputActions: IAction[] = [
new Action('undo', nls.localize('undo', "Undo"), undefined, true, () => Promise.resolve(document.execCommand('undo'))),
@@ -88,7 +90,8 @@ export class ElectronWindow extends Disposable {
@IIntegrityService private readonly integrityService: IIntegrityService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
- @IWorkspaceContextService private readonly contextService: IWorkspaceContextService
+ @IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
+ @ITextFileService private readonly textFileService: ITextFileService
) {
super();
@@ -228,11 +231,10 @@ export class ElectronWindow extends Disposable {
// Listen to editor closing (if we run with --wait)
const filesToWait = this.environmentService.configuration.filesToWait;
if (filesToWait) {
- const resourcesToWaitFor = coalesce(filesToWait.paths.map(p => p.fileUri));
const waitMarkerFile = filesToWait.waitMarkerFileUri;
- const listenerDispose = this.editorService.onDidCloseEditor(() => this.onEditorClosed(listenerDispose, resourcesToWaitFor, waitMarkerFile));
+ const resourcesToWaitFor = coalesce(filesToWait.paths.map(p => p.fileUri));
- this._register(listenerDispose);
+ this._register(this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor));
}
}
@@ -257,17 +259,6 @@ export class ElectronWindow extends Disposable {
}
}
- private onEditorClosed(listenerDispose: IDisposable, resourcesToWaitFor: URI[], waitMarkerFile: URI): void {
-
- // In wait mode, listen to changes to the editors and wait until the files
- // are closed that the user wants to wait for. When this happens we delete
- // the wait marker file to signal to the outside that editing is done.
- if (resourcesToWaitFor.every(resource => !this.editorService.isOpen({ resource }))) {
- listenerDispose.dispose();
- this.fileService.del(waitMarkerFile);
- }
- }
-
private onContextMenu(e: MouseEvent): void {
if (e.target instanceof HTMLElement) {
const target = e.target;
@@ -488,15 +479,50 @@ export class ElectronWindow extends Disposable {
// In wait mode, listen to changes to the editors and wait until the files
// are closed that the user wants to wait for. When this happens we delete
// the wait marker file to signal to the outside that editing is done.
- const resourcesToWaitFor = request.filesToWait.paths.map(p => URI.revive(p.fileUri));
const waitMarkerFile = URI.revive(request.filesToWait.waitMarkerFileUri);
- const unbind = this.editorService.onDidCloseEditor(() => {
- if (resourcesToWaitFor.every(resource => !this.editorService.isOpen({ resource }))) {
- unbind.dispose();
- this.fileService.del(waitMarkerFile);
+ const resourcesToWaitFor = coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri)));
+ this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor);
+ }
+ }
+
+ private trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): IDisposable {
+ const listener = this.editorService.onDidCloseEditor(async () => {
+ // In wait mode, listen to changes to the editors and wait until the files
+ // are closed that the user wants to wait for. When this happens we delete
+ // the wait marker file to signal to the outside that editing is done.
+ if (resourcesToWaitFor.every(resource => !this.editorService.isOpen({ resource }))) {
+ // If auto save is configured with the default delay (1s) it is possible
+ // to close the editor while the save still continues in the background. As such
+ // we have to also check if the files to wait for are dirty and if so wait
+ // for them to get saved before deleting the wait marker file.
+ const dirtyFilesToWait = this.textFileService.getDirty(resourcesToWaitFor);
+ if (dirtyFilesToWait.length > 0) {
+ await Promise.all(dirtyFilesToWait.map(async dirtyFileToWait => await this.joinResourceSaved(dirtyFileToWait)));
+ }
+
+ listener.dispose();
+ await this.fileService.del(waitMarkerFile);
+ }
+ });
+
+ return listener;
+ }
+
+ private joinResourceSaved(resource: URI): Promise {
+ return new Promise(resolve => {
+ if (!this.textFileService.isDirty(resource)) {
+ return resolve(); // return early if resource is not dirty
+ }
+
+ // Otherwise resolve promise when resource is saved
+ const listener = this.textFileService.models.onModelSaved(e => {
+ if (isEqual(resource, e.resource)) {
+ listener.dispose();
+
+ resolve();
}
});
- }
+ });
}
private openResources(resources: Array, diffMode: boolean): void {
diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts
index b1f30ade0a370..9d1b0208434bd 100644
--- a/src/vs/workbench/services/backup/common/backup.ts
+++ b/src/vs/workbench/services/backup/common/backup.ts
@@ -9,6 +9,11 @@ import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
export const IBackupFileService = createDecorator('backupFileService');
+export interface IResolvedBackup {
+ value: ITextBufferFactory;
+ meta?: T;
+}
+
/**
* A service that handles any I/O and state associated with the backup system.
*/
@@ -42,8 +47,10 @@ export interface IBackupFileService {
* @param resource The resource to back up.
* @param content The content of the resource as snapshot.
* @param versionId The version id of the resource to backup.
+ * @param meta The (optional) meta data of the resource to backup. This information
+ * can be restored later when loading the backup again.
*/
- backupResource(resource: URI, content: ITextSnapshot, versionId?: number): Promise;
+ backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise;
/**
* Gets a list of file backups for the current workspace.
@@ -55,10 +62,10 @@ export interface IBackupFileService {
/**
* Resolves the backup for the given resource.
*
- * @param value The contents from a backup resource as stream.
- * @return The backup file's backed up content as text buffer factory.
+ * @param resource The resource to get the backup for.
+ * @return The backup file's backed up content and metadata if available.
*/
- resolveBackupContent(backup: URI): Promise;
+ resolveBackupContent(resource: URI): Promise>;
/**
* Discards the backup associated with a resource if it exists..
diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts
index 21d8187ef26a4..850fe4b103a14 100644
--- a/src/vs/workbench/services/backup/node/backupFileService.ts
+++ b/src/vs/workbench/services/backup/node/backupFileService.ts
@@ -3,94 +3,112 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import * as path from 'vs/base/common/path';
-import * as crypto from 'crypto';
-import * as pfs from 'vs/base/node/pfs';
-import { URI as Uri } from 'vs/base/common/uri';
+import { join } from 'vs/base/common/path';
+import { joinPath } from 'vs/base/common/resources';
+import { createHash } from 'crypto';
+import { URI } from 'vs/base/common/uri';
+import { coalesce } from 'vs/base/common/arrays';
+import { equals, deepClone } from 'vs/base/common/objects';
import { ResourceQueue } from 'vs/base/common/async';
-import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
+import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
import { IFileService } from 'vs/platform/files/common/files';
import { readToMatchingString } from 'vs/base/node/stream';
-import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
+import { ITextSnapshot } from 'vs/editor/common/model';
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
-import { keys } from 'vs/base/common/map';
+import { keys, ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { VSBuffer } from 'vs/base/common/buffer';
import { TextSnapshotReadable } from 'vs/workbench/services/textfile/common/textfiles';
+import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
export interface IBackupFilesModel {
- resolve(backupRoot: string): Promise;
+ resolve(backupRoot: URI): Promise;
- add(resource: Uri, versionId?: number): void;
- has(resource: Uri, versionId?: number): boolean;
- get(): Uri[];
- remove(resource: Uri): void;
+ add(resource: URI, versionId?: number, meta?: object): void;
+ has(resource: URI, versionId?: number, meta?: object): boolean;
+ get(): URI[];
+ remove(resource: URI): void;
count(): number;
clear(): void;
}
-export class BackupFilesModel implements IBackupFilesModel {
- private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
-
- resolve(backupRoot: string): Promise {
- return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
-
- // For all supported schemas
- return Promise.all(backupSchemas.map(backupSchema => {
+interface IBackupCacheEntry {
+ versionId?: number;
+ meta?: object;
+}
- // Read backup directory for backups
- const backupSchemaPath = path.join(backupRoot, backupSchema);
- return pfs.readdir(backupSchemaPath).then(backupHashes => {
+export class BackupFilesModel implements IBackupFilesModel {
+ private cache: ResourceMap = new ResourceMap();
+
+ constructor(private fileService: IFileService) { }
+
+ async resolve(backupRoot: URI): Promise {
+ try {
+ const backupRootStat = await this.fileService.resolve(backupRoot);
+ if (backupRootStat.children) {
+ await Promise.all(backupRootStat.children
+ .filter(child => child.isDirectory)
+ .map(async backupSchema => {
+
+ // Read backup directory for backups
+ const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
+
+ // Remember known backups in our caches
+ if (backupSchemaStat.children) {
+ backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
+ }
+ }));
+ }
+ } catch (error) {
+ // ignore any errors
+ }
- // Remember known backups in our caches
- backupHashes.forEach(backupHash => {
- const backupResource = Uri.file(path.join(backupSchemaPath, backupHash));
- this.add(backupResource);
- });
- });
- }));
- }).then(() => this, error => this);
+ return this;
}
- add(resource: Uri, versionId = 0): void {
- this.cache[resource.toString()] = versionId;
+ add(resource: URI, versionId = 0, meta?: object): void {
+ this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache...
}
count(): number {
- return Object.keys(this.cache).length;
+ return this.cache.size;
}
- has(resource: Uri, versionId?: number): boolean {
- const cachedVersionId = this.cache[resource.toString()];
- if (typeof cachedVersionId !== 'number') {
+ has(resource: URI, versionId?: number, meta?: object): boolean {
+ const entry = this.cache.get(resource);
+ if (!entry) {
return false; // unknown resource
}
- if (typeof versionId === 'number') {
- return versionId === cachedVersionId; // if we are asked with a specific version ID, make sure to test for it
+ if (typeof versionId === 'number' && versionId !== entry.versionId) {
+ return false; // different versionId
+ }
+
+ if (meta && !equals(meta, entry.meta)) {
+ return false; // different metadata
}
return true;
}
- get(): Uri[] {
- return Object.keys(this.cache).map(k => Uri.parse(k));
+ get(): URI[] {
+ return this.cache.keys();
}
- remove(resource: Uri): void {
- delete this.cache[resource.toString()];
+ remove(resource: URI): void {
+ this.cache.delete(resource);
}
clear(): void {
- this.cache = Object.create(null);
+ this.cache.clear();
}
}
export class BackupFileService implements IBackupFileService {
- _serviceBrand: any;
+ _serviceBrand: ServiceIdentifier;
private impl: IBackupFileService;
@@ -116,15 +134,15 @@ export class BackupFileService implements IBackupFileService {
return this.impl.hasBackups();
}
- loadBackupResource(resource: Uri): Promise {
+ loadBackupResource(resource: URI): Promise {
return this.impl.loadBackupResource(resource);
}
- backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise {
- return this.impl.backupResource(resource, content, versionId);
+ backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise {
+ return this.impl.backupResource(resource, content, versionId, meta);
}
- discardResourceBackup(resource: Uri): Promise {
+ discardResourceBackup(resource: URI): Promise {
return this.impl.discardResourceBackup(resource);
}
@@ -132,26 +150,28 @@ export class BackupFileService implements IBackupFileService {
return this.impl.discardAllWorkspaceBackups();
}
- getWorkspaceFileBackups(): Promise {
+ getWorkspaceFileBackups(): Promise {
return this.impl.getWorkspaceFileBackups();
}
- resolveBackupContent(backup: Uri): Promise {
+ resolveBackupContent(backup: URI): Promise> {
return this.impl.resolveBackupContent(backup);
}
- toBackupResource(resource: Uri): Uri {
+ toBackupResource(resource: URI): URI {
return this.impl.toBackupResource(resource);
}
}
class BackupFileServiceImpl implements IBackupFileService {
- private static readonly META_MARKER = '\n';
+ private static readonly PREAMBLE_END_MARKER = '\n';
+ private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator
+ private static readonly PREAMBLE_MAX_LENGTH = 10000;
_serviceBrand: any;
- private backupWorkspacePath: string;
+ private backupWorkspacePath: URI;
private isShuttingDown: boolean;
private ready: Promise;
@@ -168,115 +188,165 @@ class BackupFileServiceImpl implements IBackupFileService {
}
initialize(backupWorkspacePath: string): void {
- this.backupWorkspacePath = backupWorkspacePath;
+ this.backupWorkspacePath = URI.file(backupWorkspacePath);
this.ready = this.init();
}
private init(): Promise {
- const model = new BackupFilesModel();
+ const model = new BackupFilesModel(this.fileService);
return model.resolve(this.backupWorkspacePath);
}
- hasBackups(): Promise {
- return this.ready.then(model => {
- return model.count() > 0;
- });
+ async hasBackups(): Promise {
+ const model = await this.ready;
+
+ return model.count() > 0;
}
- loadBackupResource(resource: Uri): Promise {
- return this.ready.then(model => {
+ async loadBackupResource(resource: URI): Promise {
+ const model = await this.ready;
- // Return directly if we have a known backup with that resource
- const backupResource = this.toBackupResource(resource);
- if (model.has(backupResource)) {
- return backupResource;
- }
+ // Return directly if we have a known backup with that resource
+ const backupResource = this.toBackupResource(resource);
+ if (model.has(backupResource)) {
+ return backupResource;
+ }
- return undefined;
- });
+ return undefined;
}
- backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise {
+ async backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise {
if (this.isShuttingDown) {
- return Promise.resolve();
+ return;
+ }
+
+ const model = await this.ready;
+
+ const backupResource = this.toBackupResource(resource);
+ if (model.has(backupResource, versionId, meta)) {
+ return; // return early if backup version id matches requested one
}
- return this.ready.then(model => {
- const backupResource = this.toBackupResource(resource);
- if (model.has(backupResource, versionId)) {
- return undefined; // return early if backup version id matches requested one
+ return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
+ let preamble: string | undefined = undefined;
+
+ // With Metadata: URI + META-START + Meta + END
+ if (meta) {
+ const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
+ if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) {
+ preamble = preambleWithMeta;
+ }
+ }
+
+ // Without Metadata: URI + END
+ if (!preamble) {
+ preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
}
- return this.ioOperationQueues.queueFor(backupResource).queue(() => {
- const preamble = `${resource.toString()}${BackupFileServiceImpl.META_MARKER}`;
+ // Update content with value
+ await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
- // Update content with value
- return this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble)).then(() => model.add(backupResource, versionId));
- });
+ // Update model
+ model.add(backupResource, versionId, meta);
});
}
- discardResourceBackup(resource: Uri): Promise {
- return this.ready.then(model => {
- const backupResource = this.toBackupResource(resource);
+ async discardResourceBackup(resource: URI): Promise {
+ const model = await this.ready;
+ const backupResource = this.toBackupResource(resource);
+
+ return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
+ await this.fileService.del(backupResource, { recursive: true });
- return this.ioOperationQueues.queueFor(backupResource).queue(() => {
- return pfs.rimraf(backupResource.fsPath, pfs.RimRafMode.MOVE).then(() => model.remove(backupResource));
- });
+ model.remove(backupResource);
});
}
- discardAllWorkspaceBackups(): Promise {
+ async discardAllWorkspaceBackups(): Promise {
this.isShuttingDown = true;
- return this.ready.then(model => {
- return pfs.rimraf(this.backupWorkspacePath, pfs.RimRafMode.MOVE).then(() => model.clear());
- });
+ const model = await this.ready;
+
+ await this.fileService.del(this.backupWorkspacePath, { recursive: true });
+
+ model.clear();
}
- getWorkspaceFileBackups(): Promise {
- return this.ready.then(model => {
- const readPromises: Promise[] = [];
+ async getWorkspaceFileBackups(): Promise {
+ const model = await this.ready;
- model.get().forEach(fileBackup => {
- readPromises.push(
- readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.META_MARKER, 2000, 10000).then(Uri.parse)
- );
- });
+ const backups = await Promise.all(model.get().map(async fileBackup => {
+ const backupPreamble = await readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH / 5, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
+ if (!backupPreamble) {
+ return undefined;
+ }
- return Promise.all(readPromises);
- });
+ // Preamble with metadata: URI + META-START + Meta + END
+ const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
+ if (metaStartIndex > 0) {
+ return URI.parse(backupPreamble.substring(0, metaStartIndex));
+ }
+
+ // Preamble without metadata: URI + END
+ else {
+ return URI.parse(backupPreamble);
+ }
+ }));
+
+ return coalesce(backups);
}
- resolveBackupContent(backup: Uri): Promise {
- return this.fileService.readFileStream(backup).then(content => {
+ async resolveBackupContent(backup: URI): Promise> {
+
+ // Metadata extraction
+ let metaRaw = '';
+ let metaEndFound = false;
- // Add a filter method to filter out everything until the meta marker
- let metaFound = false;
- const metaPreambleFilter = (chunk: VSBuffer) => {
- const chunkString = chunk.toString();
+ // Add a filter method to filter out everything until the meta end marker
+ const metaPreambleFilter = (chunk: VSBuffer) => {
+ const chunkString = chunk.toString();
- if (!metaFound && chunk) {
- const metaIndex = chunkString.indexOf(BackupFileServiceImpl.META_MARKER);
- if (metaIndex === -1) {
- return VSBuffer.fromString(''); // meta not yet found, return empty string
- }
+ if (!metaEndFound) {
+ const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER);
+ if (metaEndIndex === -1) {
+ metaRaw += chunkString;
- metaFound = true;
- return VSBuffer.fromString(chunkString.substr(metaIndex + 1)); // meta found, return everything after
+ return VSBuffer.fromString(''); // meta not yet found, return empty string
}
- return chunk;
- };
+ metaEndFound = true;
+ metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
- return createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
- });
+ return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after
+ }
+
+ return chunk;
+ };
+
+ // Read backup into factory
+ const content = await this.fileService.readFileStream(backup);
+ const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
+
+ // Trigger read for meta data extraction from the filter above
+ factory.getFirstLineText(1);
+
+ let meta: T | undefined;
+ const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
+ if (metaStartIndex !== -1) {
+ try {
+ meta = JSON.parse(metaRaw.substr(metaStartIndex + 1));
+ } catch (error) {
+ // ignore JSON parse errors
+ }
+ }
+
+ return { value: factory, meta };
}
- toBackupResource(resource: Uri): Uri {
- return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, hashPath(resource)));
+ toBackupResource(resource: URI): URI {
+ return joinPath(this.backupWorkspacePath, resource.scheme, hashPath(resource));
}
}
@@ -290,7 +360,7 @@ export class InMemoryBackupFileService implements IBackupFileService {
return Promise.resolve(this.backups.size > 0);
}
- loadBackupResource(resource: Uri): Promise {
+ loadBackupResource(resource: URI): Promise {
const backupResource = this.toBackupResource(resource);
if (this.backups.has(backupResource.toString())) {
return Promise.resolve(backupResource);
@@ -299,27 +369,27 @@ export class InMemoryBackupFileService implements IBackupFileService {
return Promise.resolve(undefined);
}
- backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise {
+ backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise {
const backupResource = this.toBackupResource(resource);
this.backups.set(backupResource.toString(), content);
return Promise.resolve();
}
- resolveBackupContent(backupResource: Uri): Promise {
+ resolveBackupContent(backupResource: URI): Promise> {
const snapshot = this.backups.get(backupResource.toString());
if (snapshot) {
- return Promise.resolve(createTextBufferFactoryFromSnapshot(snapshot));
+ return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
}
- return Promise.resolve(undefined);
+ return Promise.reject('Unexpected backup resource to resolve');
}
- getWorkspaceFileBackups(): Promise {
- return Promise.resolve(keys(this.backups).map(key => Uri.parse(key)));
+ getWorkspaceFileBackups(): Promise {
+ return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
}
- discardResourceBackup(resource: Uri): Promise {
+ discardResourceBackup(resource: URI): Promise {
this.backups.delete(this.toBackupResource(resource).toString());
return Promise.resolve();
@@ -331,17 +401,17 @@ export class InMemoryBackupFileService implements IBackupFileService {
return Promise.resolve();
}
- toBackupResource(resource: Uri): Uri {
- return Uri.file(path.join(resource.scheme, hashPath(resource)));
+ toBackupResource(resource: URI): URI {
+ return URI.file(join(resource.scheme, hashPath(resource)));
}
}
/*
* Exported only for testing
*/
-export function hashPath(resource: Uri): string {
+export function hashPath(resource: URI): string {
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
- return crypto.createHash('md5').update(str).digest('hex');
+ return createHash('md5').update(str).digest('hex');
}
registerSingleton(IBackupFileService, BackupFileService);
\ No newline at end of file
diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts
deleted file mode 100644
index fff74b2b70172..0000000000000
--- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts
+++ /dev/null
@@ -1,402 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as assert from 'assert';
-import * as platform from 'vs/base/common/platform';
-import * as crypto from 'crypto';
-import * as os from 'os';
-import * as fs from 'fs';
-import * as path from 'vs/base/common/path';
-import * as pfs from 'vs/base/node/pfs';
-import { URI as Uri } from 'vs/base/common/uri';
-import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
-import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
-import { getRandomTestPath } from 'vs/base/test/node/testUtils';
-import { DefaultEndOfLine } from 'vs/editor/common/model';
-import { Schemas } from 'vs/base/common/network';
-import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
-import { FileService } from 'vs/workbench/services/files/common/fileService';
-import { NullLogService } from 'vs/platform/log/common/log';
-import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
-import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
-import { parseArgs } from 'vs/platform/environment/node/argv';
-import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
-
-const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
-const backupHome = path.join(parentDir, 'Backups');
-const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
-
-const workspaceResource = Uri.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
-const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource));
-const fooFile = Uri.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
-const barFile = Uri.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
-const untitledFile = Uri.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
-const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
-const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
-const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
-
-class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
-
- private config: IWindowConfiguration;
-
- constructor(workspaceBackupPath: string) {
- super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
-
- this.config = Object.create(null);
- this.config.backupPath = workspaceBackupPath;
- }
-
- get configuration(): IWindowConfiguration {
- return this.config;
- }
-}
-
-class TestBackupFileService extends BackupFileService {
- constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
- const fileService = new FileService(new NullLogService());
- fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
- const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
-
- super(environmentService, fileService);
- }
-
- public toBackupResource(resource: Uri): Uri {
- return super.toBackupResource(resource);
- }
-}
-
-suite('BackupFileService', () => {
- let service: TestBackupFileService;
-
- setup(() => {
- service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
-
- // Delete any existing backups completely and then re-create it.
- return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => {
- return pfs.mkdirp(backupHome).then(() => {
- return pfs.writeFile(workspacesJsonPath, '');
- });
- });
- });
-
- teardown(() => {
- return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
- });
-
- suite('hashPath', () => {
- test('should correctly hash the path for untitled scheme URIs', () => {
- const uri = Uri.from({
- scheme: 'untitled',
- path: 'Untitled-1'
- });
- const actual = hashPath(uri);
- // If these hashes change people will lose their backed up files!
- assert.equal(actual, '13264068d108c6901b3592ea654fcd57');
- assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
- });
-
- test('should correctly hash the path for file scheme URIs', () => {
- const uri = Uri.file('/foo');
- const actual = hashPath(uri);
- // If these hashes change people will lose their backed up files!
- if (platform.isWindows) {
- assert.equal(actual, 'dec1a583f52468a020bd120c3f01d812');
- } else {
- assert.equal(actual, '1effb2475fcfba4f9e8b8a1dbc8f3caf');
- }
- assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
- });
- });
-
- suite('getBackupResource', () => {
- test('should get the correct backup path for text files', () => {
- // Format should be: ///
- const backupResource = fooFile;
- const workspaceHash = hashPath(workspaceResource);
- const filePathHash = hashPath(backupResource);
- const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
- assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
- });
-
- test('should get the correct backup path for untitled files', () => {
- // Format should be: ///
- const backupResource = Uri.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
- const workspaceHash = hashPath(workspaceResource);
- const filePathHash = hashPath(backupResource);
- const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
- assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
- });
- });
-
- suite('loadBackupResource', () => {
- test('should return whether a backup resource exists', () => {
- return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
- fs.writeFileSync(fooBackupPath, 'foo');
- service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
- return service.loadBackupResource(fooFile).then(resource => {
- assert.ok(resource);
- assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
- return service.hasBackups().then(hasBackups => {
- assert.ok(hasBackups);
- });
- });
- });
- });
- });
-
- suite('backupResource', () => {
- test('text file', function () {
- return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
- assert.equal(fs.existsSync(fooBackupPath), true);
- assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
- });
- });
-
- test('untitled file', function () {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
- assert.equal(fs.existsSync(untitledBackupPath), true);
- assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
- });
- });
-
- test('text file (ITextSnapshot)', function () {
- const model = TextModel.createFromString('test');
-
- return service.backupResource(fooFile, model.createSnapshot()).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
- assert.equal(fs.existsSync(fooBackupPath), true);
- assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
- model.dispose();
- });
- });
-
- test('untitled file (ITextSnapshot)', function () {
- const model = TextModel.createFromString('test');
-
- return service.backupResource(untitledFile, model.createSnapshot()).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
- assert.equal(fs.existsSync(untitledBackupPath), true);
- assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
- model.dispose();
- });
- });
-
- test('text file (large file, ITextSnapshot)', function () {
- const largeString = (new Array(10 * 1024)).join('Large String\n');
- const model = TextModel.createFromString(largeString);
-
- return service.backupResource(fooFile, model.createSnapshot()).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
- assert.equal(fs.existsSync(fooBackupPath), true);
- assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
- model.dispose();
- });
- });
-
- test('untitled file (large file, ITextSnapshot)', function () {
- const largeString = (new Array(10 * 1024)).join('Large String\n');
- const model = TextModel.createFromString(largeString);
-
- return service.backupResource(untitledFile, model.createSnapshot()).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
- assert.equal(fs.existsSync(untitledBackupPath), true);
- assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
- model.dispose();
- });
- });
- });
-
- suite('discardResourceBackup', () => {
- test('text file', function () {
- return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
- return service.discardResourceBackup(fooFile).then(() => {
- assert.equal(fs.existsSync(fooBackupPath), false);
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
- });
- });
- });
-
- test('untitled file', function () {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
- return service.discardResourceBackup(untitledFile).then(() => {
- assert.equal(fs.existsSync(untitledBackupPath), false);
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
- });
- });
- });
- });
-
- suite('discardAllWorkspaceBackups', () => {
- test('text file', function () {
- return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
- return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
- return service.discardAllWorkspaceBackups().then(() => {
- assert.equal(fs.existsSync(fooBackupPath), false);
- assert.equal(fs.existsSync(barBackupPath), false);
- assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
- });
- });
- });
- });
-
- test('untitled file', function () {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
- return service.discardAllWorkspaceBackups().then(() => {
- assert.equal(fs.existsSync(untitledBackupPath), false);
- assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
- });
- });
- });
-
- test('should disable further backups', function () {
- return service.discardAllWorkspaceBackups().then(() => {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- assert.equal(fs.existsSync(workspaceBackupPath), false);
- });
- });
- });
- });
-
- suite('getWorkspaceFileBackups', () => {
- test('("file") - text file', () => {
- return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- return service.getWorkspaceFileBackups().then(textFiles => {
- assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
- return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- return service.getWorkspaceFileBackups().then(textFiles => {
- assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
- });
- });
- });
- });
- });
-
- test('("file") - untitled file', () => {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- return service.getWorkspaceFileBackups().then(textFiles => {
- assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
- });
- });
- });
-
- test('("untitled") - untitled file', () => {
- return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- return service.getWorkspaceFileBackups().then(textFiles => {
- assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
- });
- });
- });
- });
-
- test('resolveBackupContent', () => {
- test('should restore the original contents (untitled file)', () => {
- const contents = 'test\nand more stuff';
- service.backupResource(untitledFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- service.resolveBackupContent(service.toBackupResource(untitledFile)).then(factory => {
- assert.equal(contents, snapshotToString(factory!.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
- });
- });
- });
-
- test('should restore the original contents (text file)', () => {
- const contents = [
- 'Lorem ipsum ',
- 'dolor öäü sit amet ',
- 'consectetur ',
- 'adipiscing ßß elit',
- ].join('');
-
- service.backupResource(fooFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
- service.resolveBackupContent(service.toBackupResource(untitledFile)).then(factory => {
- assert.equal(contents, snapshotToString(factory!.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
- });
- });
- });
- });
-});
-
-suite('BackupFilesModel', () => {
- test('simple', () => {
- const model = new BackupFilesModel();
-
- const resource1 = Uri.file('test.html');
-
- assert.equal(model.has(resource1), false);
-
- model.add(resource1);
-
- assert.equal(model.has(resource1), true);
- assert.equal(model.has(resource1, 0), true);
- assert.equal(model.has(resource1, 1), false);
-
- model.remove(resource1);
-
- assert.equal(model.has(resource1), false);
-
- model.add(resource1);
-
- assert.equal(model.has(resource1), true);
- assert.equal(model.has(resource1, 0), true);
- assert.equal(model.has(resource1, 1), false);
-
- model.clear();
-
- assert.equal(model.has(resource1), false);
-
- model.add(resource1, 1);
-
- assert.equal(model.has(resource1), true);
- assert.equal(model.has(resource1, 0), false);
- assert.equal(model.has(resource1, 1), true);
-
- const resource2 = Uri.file('test1.html');
- const resource3 = Uri.file('test2.html');
- const resource4 = Uri.file('test3.html');
-
- model.add(resource2);
- model.add(resource3);
- model.add(resource4);
-
- assert.equal(model.has(resource1), true);
- assert.equal(model.has(resource2), true);
- assert.equal(model.has(resource3), true);
- assert.equal(model.has(resource4), true);
- });
-
- test('resolve', () => {
- return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
- fs.writeFileSync(fooBackupPath, 'foo');
-
- const model = new BackupFilesModel();
-
- return model.resolve(workspaceBackupPath).then(model => {
- assert.equal(model.has(Uri.file(fooBackupPath)), true);
- });
- });
- });
-
- test('get', () => {
- const model = new BackupFilesModel();
-
- assert.deepEqual(model.get(), []);
-
- const file1 = Uri.file('/root/file/foo.html');
- const file2 = Uri.file('/root/file/bar.html');
- const untitled = Uri.file('/root/untitled/bar.html');
-
- model.add(file1);
- model.add(file2);
- model.add(untitled);
-
- assert.deepEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]);
- });
-});
diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts
new file mode 100644
index 0000000000000..52a5bc95d1ce1
--- /dev/null
+++ b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts
@@ -0,0 +1,579 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as assert from 'assert';
+import * as platform from 'vs/base/common/platform';
+import * as crypto from 'crypto';
+import * as os from 'os';
+import * as fs from 'fs';
+import * as path from 'vs/base/common/path';
+import * as pfs from 'vs/base/node/pfs';
+import { URI } from 'vs/base/common/uri';
+import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
+import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
+import { getRandomTestPath } from 'vs/base/test/node/testUtils';
+import { DefaultEndOfLine } from 'vs/editor/common/model';
+import { Schemas } from 'vs/base/common/network';
+import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
+import { FileService } from 'vs/workbench/services/files/common/fileService';
+import { NullLogService } from 'vs/platform/log/common/log';
+import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
+import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
+import { parseArgs } from 'vs/platform/environment/node/argv';
+import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
+import { IFileService } from 'vs/platform/files/common/files';
+
+const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
+const backupHome = path.join(parentDir, 'Backups');
+const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
+
+const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
+const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource));
+const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
+const customFile = URI.parse('customScheme://some/path');
+const customFileWithFragment = URI.parse('customScheme2://some/path#fragment');
+const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
+const fooBarFile = URI.file(platform.isWindows ? 'c:\\Foo Bar' : '/Foo Bar');
+const untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
+const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
+const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
+const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
+
+class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
+
+ private config: IWindowConfiguration;
+
+ constructor(workspaceBackupPath: string) {
+ super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
+
+ this.config = Object.create(null);
+ this.config.backupPath = workspaceBackupPath;
+ }
+
+ get configuration(): IWindowConfiguration {
+ return this.config;
+ }
+}
+
+class TestBackupFileService extends BackupFileService {
+
+ readonly fileService: IFileService;
+
+ constructor(workspace: URI, backupHome: string, workspacesJsonPath: string) {
+ const fileService = new FileService(new NullLogService());
+ fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
+ const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
+
+ super(environmentService, fileService);
+
+ this.fileService = fileService;
+ }
+
+ toBackupResource(resource: URI): URI {
+ return super.toBackupResource(resource);
+ }
+}
+
+suite('BackupFileService', () => {
+ let service: TestBackupFileService;
+
+ setup(async () => {
+ service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
+
+ // Delete any existing backups completely and then re-create it.
+ await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
+ await pfs.mkdirp(backupHome);
+
+ return pfs.writeFile(workspacesJsonPath, '');
+ });
+
+ teardown(() => {
+ return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
+ });
+
+ suite('hashPath', () => {
+ test('should correctly hash the path for untitled scheme URIs', () => {
+ const uri = URI.from({
+ scheme: 'untitled',
+ path: 'Untitled-1'
+ });
+ const actual = hashPath(uri);
+ // If these hashes change people will lose their backed up files!
+ assert.equal(actual, '13264068d108c6901b3592ea654fcd57');
+ assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
+ });
+
+ test('should correctly hash the path for file scheme URIs', () => {
+ const uri = URI.file('/foo');
+ const actual = hashPath(uri);
+ // If these hashes change people will lose their backed up files!
+ if (platform.isWindows) {
+ assert.equal(actual, 'dec1a583f52468a020bd120c3f01d812');
+ } else {
+ assert.equal(actual, '1effb2475fcfba4f9e8b8a1dbc8f3caf');
+ }
+ assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
+ });
+ });
+
+ suite('getBackupResource', () => {
+ test('should get the correct backup path for text files', () => {
+ // Format should be: ///
+ const backupResource = fooFile;
+ const workspaceHash = hashPath(workspaceResource);
+ const filePathHash = hashPath(backupResource);
+ const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
+ assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
+ });
+
+ test('should get the correct backup path for untitled files', () => {
+ // Format should be: ///
+ const backupResource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
+ const workspaceHash = hashPath(workspaceResource);
+ const filePathHash = hashPath(backupResource);
+ const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
+ assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
+ });
+ });
+
+ suite('loadBackupResource', () => {
+ test('should return whether a backup resource exists', async () => {
+ await pfs.mkdirp(path.dirname(fooBackupPath));
+ fs.writeFileSync(fooBackupPath, 'foo');
+ service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
+ const resource = await service.loadBackupResource(fooFile);
+ assert.ok(resource);
+ assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
+ const hasBackups = await service.hasBackups();
+ assert.ok(hasBackups);
+ });
+ });
+
+ suite('backupResource', () => {
+ test('text file', async () => {
+ await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ assert.equal(fs.existsSync(fooBackupPath), true);
+ assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
+ });
+
+ test('text file (with meta)', async () => {
+ await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false), undefined, { etag: '678', orphaned: true });
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ assert.equal(fs.existsSync(fooBackupPath), true);
+ assert.equal(fs.readFileSync(fooBackupPath).toString(), `${fooFile.toString()} {"etag":"678","orphaned":true}\ntest`);
+ });
+
+ test('untitled file', async () => {
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
+ assert.equal(fs.existsSync(untitledBackupPath), true);
+ assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
+ });
+
+ test('text file (ITextSnapshot)', async () => {
+ const model = TextModel.createFromString('test');
+
+ await service.backupResource(fooFile, model.createSnapshot());
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ assert.equal(fs.existsSync(fooBackupPath), true);
+ assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
+ model.dispose();
+ });
+
+ test('untitled file (ITextSnapshot)', async () => {
+ const model = TextModel.createFromString('test');
+
+ await service.backupResource(untitledFile, model.createSnapshot());
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
+ assert.equal(fs.existsSync(untitledBackupPath), true);
+ assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
+ model.dispose();
+ });
+
+ test('text file (large file, ITextSnapshot)', async () => {
+ const largeString = (new Array(10 * 1024)).join('Large String\n');
+ const model = TextModel.createFromString(largeString);
+
+ await service.backupResource(fooFile, model.createSnapshot());
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ assert.equal(fs.existsSync(fooBackupPath), true);
+ assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
+ model.dispose();
+ });
+
+ test('untitled file (large file, ITextSnapshot)', async () => {
+ const largeString = (new Array(10 * 1024)).join('Large String\n');
+ const model = TextModel.createFromString(largeString);
+
+ await service.backupResource(untitledFile, model.createSnapshot());
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
+ assert.equal(fs.existsSync(untitledBackupPath), true);
+ assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
+ model.dispose();
+ });
+ });
+
+ suite('discardResourceBackup', () => {
+ test('text file', async () => {
+ await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ await service.discardResourceBackup(fooFile);
+ assert.equal(fs.existsSync(fooBackupPath), false);
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
+ });
+
+ test('untitled file', async () => {
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
+ await service.discardResourceBackup(untitledFile);
+ assert.equal(fs.existsSync(untitledBackupPath), false);
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
+ });
+ });
+
+ suite('discardAllWorkspaceBackups', () => {
+ test('text file', async () => {
+ await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
+ await service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
+ await service.discardAllWorkspaceBackups();
+ assert.equal(fs.existsSync(fooBackupPath), false);
+ assert.equal(fs.existsSync(barBackupPath), false);
+ assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
+ });
+
+ test('untitled file', async () => {
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
+ await service.discardAllWorkspaceBackups();
+ assert.equal(fs.existsSync(untitledBackupPath), false);
+ assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
+ });
+
+ test('should disable further backups', async () => {
+ await service.discardAllWorkspaceBackups();
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ assert.equal(fs.existsSync(workspaceBackupPath), false);
+ });
+ });
+
+ suite('getWorkspaceFileBackups', () => {
+ test('("file") - text file', async () => {
+ await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ const textFiles = await service.getWorkspaceFileBackups();
+ assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
+ await service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ const textFiles_1 = await service.getWorkspaceFileBackups();
+ assert.deepEqual(textFiles_1.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
+ });
+
+ test('("file") - untitled file', async () => {
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ const textFiles = await service.getWorkspaceFileBackups();
+ assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
+ });
+
+ test('("untitled") - untitled file', async () => {
+ await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
+ const textFiles = await service.getWorkspaceFileBackups();
+ assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
+ });
+ });
+
+ suite('resolveBackupContent', () => {
+
+ interface IBackupTestMetaData {
+ mtime?: number;
+ size?: number;
+ etag?: string;
+ orphaned?: boolean;
+ }
+
+ test('should restore the original contents (untitled file)', async () => {
+ const contents = 'test\nand more stuff';
+
+ await testResolveBackup(untitledFile, contents);
+ });
+
+ test('should restore the original contents (untitled file with metadata)', async () => {
+ const contents = 'test\nand more stuff';
+
+ const meta = {
+ etag: 'the Etag',
+ size: 666,
+ mtime: Date.now(),
+ orphaned: true
+ };
+
+ await testResolveBackup(untitledFile, contents, meta);
+ });
+
+ test('should restore the original contents (text file)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'consectetur ',
+ 'adipiscing ßß elit'
+ ].join('');
+
+ await testResolveBackup(fooFile, contents);
+ });
+
+ test('should restore the original contents (text file - custom scheme)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'consectetur ',
+ 'adipiscing ßß elit'
+ ].join('');
+
+ await testResolveBackup(customFile, contents);
+ });
+
+ test('should restore the original contents (text file with metadata)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: 'theEtag',
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await testResolveBackup(fooFile, contents, meta);
+ });
+
+ test('should restore the original contents (text file with metadata changed once)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: 'theEtag',
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await testResolveBackup(fooFile, contents, meta);
+
+ // Change meta and test again
+ meta.size = 999;
+ await testResolveBackup(fooFile, contents, meta);
+ });
+
+ test('should restore the original contents (text file with broken metadata)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: 'theEtag',
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await service.backupResource(fooFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false), 1, meta);
+
+ assert.ok(await service.loadBackupResource(fooFile));
+
+ const fileContents = fs.readFileSync(fooBackupPath).toString();
+ assert.equal(fileContents.indexOf(fooFile.toString()), 0);
+
+ const metaIndex = fileContents.indexOf('{');
+ const newFileContents = fileContents.substring(0, metaIndex) + '{{' + fileContents.substr(metaIndex);
+ fs.writeFileSync(fooBackupPath, newFileContents);
+
+ const backup = await service.resolveBackupContent(service.toBackupResource(fooFile));
+ assert.equal(contents, snapshotToString(backup.value.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
+ assert.ok(!backup.meta);
+ });
+
+ test('should restore the original contents (text file with metadata and fragment URI)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: 'theEtag',
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await testResolveBackup(customFileWithFragment, contents, meta);
+ });
+
+ test('should restore the original contents (text file with space in name with metadata)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: 'theEtag',
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await testResolveBackup(fooBarFile, contents, meta);
+ });
+
+ test('should restore the original contents (text file with too large metadata to persist)', async () => {
+ const contents = [
+ 'Lorem ipsum ',
+ 'dolor öäü sit amet ',
+ 'adipiscing ßß elit',
+ 'consectetur '
+ ].join('');
+
+ const meta = {
+ etag: (new Array(100 * 1024)).join('Large String'),
+ size: 888,
+ mtime: Date.now(),
+ orphaned: false
+ };
+
+ await testResolveBackup(fooBarFile, contents, meta, null);
+ });
+
+ async function testResolveBackup(resource: URI, contents: string, meta?: IBackupTestMetaData, expectedMeta?: IBackupTestMetaData | null) {
+ if (typeof expectedMeta === 'undefined') {
+ expectedMeta = meta;
+ }
+
+ await service.backupResource(resource, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false), 1, meta);
+
+ assert.ok(await service.loadBackupResource(resource));
+
+ const backup = await service.resolveBackupContent(service.toBackupResource(resource));
+ assert.equal(contents, snapshotToString(backup.value.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
+
+ if (expectedMeta) {
+ assert.equal(backup.meta!.etag, expectedMeta.etag);
+ assert.equal(backup.meta!.size, expectedMeta.size);
+ assert.equal(backup.meta!.mtime, expectedMeta.mtime);
+ assert.equal(backup.meta!.orphaned, expectedMeta.orphaned);
+ } else {
+ assert.ok(!backup.meta);
+ }
+ }
+ });
+});
+
+suite('BackupFilesModel', () => {
+
+ let service: TestBackupFileService;
+
+ setup(async () => {
+ service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
+
+ // Delete any existing backups completely and then re-create it.
+ await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
+ await pfs.mkdirp(backupHome);
+
+ return pfs.writeFile(workspacesJsonPath, '');
+ });
+
+ teardown(() => {
+ return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
+ });
+
+ test('simple', () => {
+ const model = new BackupFilesModel(service.fileService);
+
+ const resource1 = URI.file('test.html');
+
+ assert.equal(model.has(resource1), false);
+
+ model.add(resource1);
+
+ assert.equal(model.has(resource1), true);
+ assert.equal(model.has(resource1, 0), true);
+ assert.equal(model.has(resource1, 1), false);
+ assert.equal(model.has(resource1, 1, { foo: 'bar' }), false);
+
+ model.remove(resource1);
+
+ assert.equal(model.has(resource1), false);
+
+ model.add(resource1);
+
+ assert.equal(model.has(resource1), true);
+ assert.equal(model.has(resource1, 0), true);
+ assert.equal(model.has(resource1, 1), false);
+
+ model.clear();
+
+ assert.equal(model.has(resource1), false);
+
+ model.add(resource1, 1);
+
+ assert.equal(model.has(resource1), true);
+ assert.equal(model.has(resource1, 0), false);
+ assert.equal(model.has(resource1, 1), true);
+
+ const resource2 = URI.file('test1.html');
+ const resource3 = URI.file('test2.html');
+ const resource4 = URI.file('test3.html');
+
+ model.add(resource2);
+ model.add(resource3);
+ model.add(resource4, undefined, { foo: 'bar' });
+
+ assert.equal(model.has(resource1), true);
+ assert.equal(model.has(resource2), true);
+ assert.equal(model.has(resource3), true);
+
+ assert.equal(model.has(resource4), true);
+ assert.equal(model.has(resource4, undefined, { foo: 'bar' }), true);
+ assert.equal(model.has(resource4, undefined, { bar: 'foo' }), false);
+ });
+
+ test('resolve', async () => {
+ await pfs.mkdirp(path.dirname(fooBackupPath));
+ fs.writeFileSync(fooBackupPath, 'foo');
+ const model = new BackupFilesModel(service.fileService);
+
+ const resolvedModel = await model.resolve(URI.file(workspaceBackupPath));
+ assert.equal(resolvedModel.has(URI.file(fooBackupPath)), true);
+ });
+
+ test('get', () => {
+ const model = new BackupFilesModel(service.fileService);
+
+ assert.deepEqual(model.get(), []);
+
+ const file1 = URI.file('/root/file/foo.html');
+ const file2 = URI.file('/root/file/bar.html');
+ const untitled = URI.file('/root/untitled/bar.html');
+
+ model.add(file1);
+ model.add(file2);
+ model.add(untitled);
+
+ assert.deepEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]);
+ });
+});
diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts
index 6969fe7595680..9b93b9b397510 100644
--- a/src/vs/workbench/services/editor/browser/editorService.ts
+++ b/src/vs/workbench/services/editor/browser/editorService.ts
@@ -528,7 +528,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Untitled file support
const untitledInput = input;
if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
- return this.untitledEditorService.createOrGet(untitledInput.resource, untitledInput.language, untitledInput.contents, untitledInput.encoding);
+ return this.untitledEditorService.createOrGet(untitledInput.resource, untitledInput.mode, untitledInput.contents, untitledInput.encoding);
}
// Resource Editor Support
@@ -539,13 +539,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
label = basename(resourceInput.resource); // derive the label from the path (but not for data URIs)
}
- return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.forceFile) as EditorInput;
+ return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.mode, resourceInput.forceFile) as EditorInput;
}
throw new Error('Unknown input type');
}
- private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, forceFile: boolean | undefined): ICachedEditorInput {
+ private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, mode: string | undefined, forceFile: boolean | undefined): ICachedEditorInput {
if (EditorService.CACHE.has(resource)) {
const input = EditorService.CACHE.get(resource)!;
if (input instanceof ResourceEditorInput) {
@@ -556,10 +556,18 @@ export class EditorService extends Disposable implements EditorServiceImpl {
if (description) {
input.setDescription(description);
}
+
+ if (mode) {
+ input.setPreferredMode(mode);
+ }
} else if (!(input instanceof DataUriEditorInput)) {
if (encoding) {
input.setPreferredEncoding(encoding);
}
+
+ if (mode) {
+ input.setPreferredMode(mode);
+ }
}
return input;
@@ -569,7 +577,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// File
if (forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resource)) {
- input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService);
+ input = this.fileInputFactory.createFileInput(resource, encoding, mode, instantiationService);
}
// Data URI
@@ -579,13 +587,12 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Resource
else {
- input = instantiationService.createInstance(ResourceEditorInput, label, description, resource);
+ input = instantiationService.createInstance(ResourceEditorInput, label, description, resource, mode);
}
+ // Add to cache and remove when input gets disposed
EditorService.CACHE.set(resource, input);
- Event.once(input.onDispose)(() => {
- EditorService.CACHE.delete(resource);
- });
+ Event.once(input.onDispose)(() => EditorService.CACHE.delete(resource));
return input;
}
diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts
index 0fe83803a0c0c..287202578400b 100644
--- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts
+++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts
@@ -24,10 +24,10 @@ export class TestEditorControl extends BaseEditor {
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyFileEditorForEditorGroupService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
- setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise {
+ async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise {
super.setInput(input, options, token);
- return input.resolve().then(() => undefined);
+ await input.resolve();
}
getId(): string { return 'MyFileEditorForEditorGroupService'; }
@@ -45,11 +45,13 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
setEncoding(encoding: string) { }
getEncoding(): string { return null!; }
setPreferredEncoding(encoding: string) { }
+ setMode(mode: string) { }
+ setPreferredMode(mode: string) { }
getResource(): URI { return this.resource; }
setForceOpenAsBinary(): void { }
}
-suite('Editor groups service', () => {
+suite('EditorGroupsService', () => {
function registerTestEditorInput(): void {
@@ -291,7 +293,7 @@ suite('Editor groups service', () => {
part.dispose();
});
- test('copy/merge groups', function () {
+ test('copy/merge groups', async () => {
const part = createPart();
let groupAddedCounter = 0;
@@ -312,40 +314,32 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
- return rootGroup.openEditor(input, EditorOptions.create({ pinned: true })).then(() => {
- const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT, { activate: true });
- const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN);
-
- assert.equal(groupAddedCounter, 2);
- assert.equal(downGroup.count, 1);
- assert.ok(downGroup.activeEditor instanceof TestEditorInput);
-
- part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.COPY_EDITORS });
- assert.equal(rightGroup.count, 1);
- assert.ok(rightGroup.activeEditor instanceof TestEditorInput);
-
- part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.MOVE_EDITORS });
- assert.equal(rootGroup.count, 0);
-
- part.mergeGroup(rootGroup, downGroup);
- assert.equal(groupRemovedCounter, 1);
- assert.equal(rootGroupDisposed, true);
-
- groupAddedListener.dispose();
- groupRemovedListener.dispose();
- disposeListener.dispose();
-
- part.dispose();
- });
+ await rootGroup.openEditor(input, EditorOptions.create({ pinned: true }));
+ const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT, { activate: true });
+ const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN);
+ assert.equal(groupAddedCounter, 2);
+ assert.equal(downGroup.count, 1);
+ assert.ok(downGroup.activeEditor instanceof TestEditorInput);
+ part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.COPY_EDITORS });
+ assert.equal(rightGroup.count, 1);
+ assert.ok(rightGroup.activeEditor instanceof TestEditorInput);
+ part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.MOVE_EDITORS });
+ assert.equal(rootGroup.count, 0);
+ part.mergeGroup(rootGroup, downGroup);
+ assert.equal(groupRemovedCounter, 1);
+ assert.equal(rootGroupDisposed, true);
+ groupAddedListener.dispose();
+ groupRemovedListener.dispose();
+ disposeListener.dispose();
+ part.dispose();
});
- test('whenRestored', () => {
+ test('whenRestored', async () => {
const part = createPart();
- return part.whenRestored.then(() => {
- assert.ok(true);
- part.dispose();
- });
+ await part.whenRestored;
+ assert.ok(true);
+ part.dispose();
});
test('options', () => {
@@ -467,7 +461,7 @@ suite('Editor groups service', () => {
part.dispose();
});
- test('openEditors / closeEditors', function () {
+ test('openEditors / closeEditors', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -475,20 +469,17 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive'));
- return group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- return group.closeEditors([input, inputInactive]).then(() => {
- assert.equal(group.isEmpty(), true);
+ await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
- part.dispose();
- });
- });
+ await group.closeEditors([input, inputInactive]);
+ assert.equal(group.isEmpty(), true);
+ part.dispose();
});
- test('closeEditors (except one)', function () {
+ test('closeEditors (except one)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -497,22 +488,19 @@ suite('Editor groups service', () => {
const input2 = new TestEditorInput(URI.file('foo/bar2'));
const input3 = new TestEditorInput(URI.file('foo/bar3'));
- return group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]).then(() => {
- assert.equal(group.count, 3);
- assert.equal(group.getEditor(0), input1);
- assert.equal(group.getEditor(1), input2);
- assert.equal(group.getEditor(2), input3);
+ await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
+ assert.equal(group.count, 3);
+ assert.equal(group.getEditor(0), input1);
+ assert.equal(group.getEditor(1), input2);
+ assert.equal(group.getEditor(2), input3);
- return group.closeEditors({ except: input2 }).then(() => {
- assert.equal(group.count, 1);
- assert.equal(group.getEditor(0), input2);
-
- part.dispose();
- });
- });
+ await group.closeEditors({ except: input2 });
+ assert.equal(group.count, 1);
+ assert.equal(group.getEditor(0), input2);
+ part.dispose();
});
- test('closeEditors (saved only)', function () {
+ test('closeEditors (saved only)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -521,21 +509,18 @@ suite('Editor groups service', () => {
const input2 = new TestEditorInput(URI.file('foo/bar2'));
const input3 = new TestEditorInput(URI.file('foo/bar3'));
- return group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]).then(() => {
- assert.equal(group.count, 3);
- assert.equal(group.getEditor(0), input1);
- assert.equal(group.getEditor(1), input2);
- assert.equal(group.getEditor(2), input3);
-
- return group.closeEditors({ savedOnly: true }).then(() => {
- assert.equal(group.count, 0);
+ await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
+ assert.equal(group.count, 3);
+ assert.equal(group.getEditor(0), input1);
+ assert.equal(group.getEditor(1), input2);
+ assert.equal(group.getEditor(2), input3);
- part.dispose();
- });
- });
+ await group.closeEditors({ savedOnly: true });
+ assert.equal(group.count, 0);
+ part.dispose();
});
- test('closeEditors (direction: right)', function () {
+ test('closeEditors (direction: right)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -544,23 +529,20 @@ suite('Editor groups service', () => {
const input2 = new TestEditorInput(URI.file('foo/bar2'));
const input3 = new TestEditorInput(URI.file('foo/bar3'));
- return group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]).then(() => {
- assert.equal(group.count, 3);
- assert.equal(group.getEditor(0), input1);
- assert.equal(group.getEditor(1), input2);
- assert.equal(group.getEditor(2), input3);
-
- return group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 }).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input1);
- assert.equal(group.getEditor(1), input2);
+ await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
+ assert.equal(group.count, 3);
+ assert.equal(group.getEditor(0), input1);
+ assert.equal(group.getEditor(1), input2);
+ assert.equal(group.getEditor(2), input3);
- part.dispose();
- });
- });
+ await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 });
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input1);
+ assert.equal(group.getEditor(1), input2);
+ part.dispose();
});
- test('closeEditors (direction: left)', function () {
+ test('closeEditors (direction: left)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -569,23 +551,20 @@ suite('Editor groups service', () => {
const input2 = new TestEditorInput(URI.file('foo/bar2'));
const input3 = new TestEditorInput(URI.file('foo/bar3'));
- return group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]).then(() => {
- assert.equal(group.count, 3);
- assert.equal(group.getEditor(0), input1);
- assert.equal(group.getEditor(1), input2);
- assert.equal(group.getEditor(2), input3);
+ await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
+ assert.equal(group.count, 3);
+ assert.equal(group.getEditor(0), input1);
+ assert.equal(group.getEditor(1), input2);
+ assert.equal(group.getEditor(2), input3);
- return group.closeEditors({ direction: CloseDirection.LEFT, except: input2 }).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input2);
- assert.equal(group.getEditor(1), input3);
-
- part.dispose();
- });
- });
+ await group.closeEditors({ direction: CloseDirection.LEFT, except: input2 });
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input2);
+ assert.equal(group.getEditor(1), input3);
+ part.dispose();
});
- test('closeAllEditors', () => {
+ test('closeAllEditors', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -593,20 +572,17 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive'));
- return group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- return group.closeAllEditors().then(() => {
- assert.equal(group.isEmpty(), true);
+ await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
- part.dispose();
- });
- });
+ await group.closeAllEditors();
+ assert.equal(group.isEmpty(), true);
+ part.dispose();
});
- test('moveEditor (same group)', function () {
+ test('moveEditor (same group)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -622,22 +598,19 @@ suite('Editor groups service', () => {
}
});
- return group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- group.moveEditor(inputInactive, group, { index: 0 });
- assert.equal(editorMoveCounter, 1);
- assert.equal(group.getEditor(0), inputInactive);
- assert.equal(group.getEditor(1), input);
-
- editorGroupChangeListener.dispose();
- part.dispose();
- });
+ await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
+ group.moveEditor(inputInactive, group, { index: 0 });
+ assert.equal(editorMoveCounter, 1);
+ assert.equal(group.getEditor(0), inputInactive);
+ assert.equal(group.getEditor(1), input);
+ editorGroupChangeListener.dispose();
+ part.dispose();
});
- test('moveEditor (across groups)', function () {
+ test('moveEditor (across groups)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -647,23 +620,19 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive'));
- return group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- group.moveEditor(inputInactive, rightGroup, { index: 0 });
- assert.equal(group.count, 1);
- assert.equal(group.getEditor(0), input);
-
- assert.equal(rightGroup.count, 1);
- assert.equal(rightGroup.getEditor(0), inputInactive);
-
- part.dispose();
- });
+ await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
+ group.moveEditor(inputInactive, rightGroup, { index: 0 });
+ assert.equal(group.count, 1);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(rightGroup.count, 1);
+ assert.equal(rightGroup.getEditor(0), inputInactive);
+ part.dispose();
});
- test('copyEditor (across groups)', function () {
+ test('copyEditor (across groups)', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -673,24 +642,20 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive'));
- return group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]).then(() => {
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- group.copyEditor(inputInactive, rightGroup, { index: 0 });
- assert.equal(group.count, 2);
- assert.equal(group.getEditor(0), input);
- assert.equal(group.getEditor(1), inputInactive);
-
- assert.equal(rightGroup.count, 1);
- assert.equal(rightGroup.getEditor(0), inputInactive);
-
- part.dispose();
- });
+ await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
+ group.copyEditor(inputInactive, rightGroup, { index: 0 });
+ assert.equal(group.count, 2);
+ assert.equal(group.getEditor(0), input);
+ assert.equal(group.getEditor(1), inputInactive);
+ assert.equal(rightGroup.count, 1);
+ assert.equal(rightGroup.getEditor(0), inputInactive);
+ part.dispose();
});
- test('replaceEditors', () => {
+ test('replaceEditors', async () => {
const part = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty(), true);
@@ -698,17 +663,14 @@ suite('Editor groups service', () => {
const input = new TestEditorInput(URI.file('foo/bar'));
const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive'));
- return group.openEditor(input).then(() => {
- assert.equal(group.count, 1);
- assert.equal(group.getEditor(0), input);
-
- return group.replaceEditors([{ editor: input, replacement: inputInactive }]).then(() => {
- assert.equal(group.count, 1);
- assert.equal(group.getEditor(0), inputInactive);
+ await group.openEditor(input);
+ assert.equal(group.count, 1);
+ assert.equal(group.getEditor(0), input);
- part.dispose();
- });
- });
+ await group.replaceEditors([{ editor: input, replacement: inputInactive }]);
+ assert.equal(group.count, 1);
+ assert.equal(group.getEditor(0), inputInactive);
+ part.dispose();
});
test('find neighbour group (left/right)', function () {
diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts
index a670ff43ae3ef..658a4a84e773a 100644
--- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts
+++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts
@@ -29,15 +29,17 @@ 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';
+import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
+import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
export class TestEditorControl extends BaseEditor {
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyTestEditorForEditorService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
- setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise {
+ async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise {
super.setInput(input, options, token);
- return input.resolve().then(() => undefined);
+ await input.resolve();
}
getId(): string { return 'MyTestEditorForEditorService'; }
@@ -56,6 +58,8 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
setEncoding(encoding: string) { }
getEncoding(): string { return null!; }
setPreferredEncoding(encoding: string) { }
+ setMode(mode: string) { }
+ setPreferredMode(mode: string) { }
getResource(): URI { return this.resource; }
setForceOpenAsBinary(): void { }
setFailToOpen(): void {
@@ -75,7 +79,7 @@ class FileServiceProvider extends Disposable {
}
}
-suite('Editor service', () => {
+suite('EditorService', () => {
function registerTestEditorInput(): void {
Registry.as(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput));
@@ -83,7 +87,7 @@ suite('Editor service', () => {
registerTestEditorInput();
- test('basics', function () {
+ test('basics', async () => {
const partInstantiator = workbenchInstantiationService();
const part = partInstantiator.createInstance(EditorPart);
@@ -112,51 +116,49 @@ suite('Editor service', () => {
didCloseEditorListenerCounter++;
});
- return part.whenRestored.then(() => {
-
- // Open input
- return service.openEditor(input, { pinned: true }).then(editor => {
- assert.ok(editor instanceof TestEditorControl);
- assert.equal(editor, service.activeControl);
- assert.equal(input, service.activeEditor);
- assert.equal(service.visibleControls.length, 1);
- assert.equal(service.visibleControls[0], editor);
- assert.ok(!service.activeTextEditorWidget);
- assert.equal(service.visibleTextEditorWidgets.length, 0);
- assert.equal(service.isOpen(input), true);
- assert.equal(service.getOpened({ resource: input.getResource() }), input);
- assert.equal(service.isOpen(input, part.activeGroup), true);
- assert.equal(activeEditorChangeEventCounter, 1);
- assert.equal(visibleEditorChangeEventCounter, 1);
-
- // Close input
- return editor!.group!.closeEditor(input).then(() => {
- assert.equal(didCloseEditorListenerCounter, 1);
- assert.equal(activeEditorChangeEventCounter, 2);
- assert.equal(visibleEditorChangeEventCounter, 2);
- assert.ok(input.gotDisposed);
-
- // Open again 2 inputs
- return service.openEditor(input, { pinned: true }).then(editor => {
- return service.openEditor(otherInput, { pinned: true }).then(editor => {
- assert.equal(service.visibleControls.length, 1);
- assert.equal(service.isOpen(input), true);
- assert.equal(service.isOpen(otherInput), true);
-
- assert.equal(activeEditorChangeEventCounter, 4);
- assert.equal(visibleEditorChangeEventCounter, 4);
-
- activeEditorChangeListener.dispose();
- visibleEditorChangeListener.dispose();
- didCloseEditorListener.dispose();
- });
- });
- });
- });
- });
+ await part.whenRestored;
+
+ // Open input
+ let editor = await service.openEditor(input, { pinned: true });
+
+ assert.ok(editor instanceof TestEditorControl);
+ assert.equal(editor, service.activeControl);
+ assert.equal(input, service.activeEditor);
+ assert.equal(service.visibleControls.length, 1);
+ assert.equal(service.visibleControls[0], editor);
+ assert.ok(!service.activeTextEditorWidget);
+ assert.equal(service.visibleTextEditorWidgets.length, 0);
+ assert.equal(service.isOpen(input), true);
+ assert.equal(service.getOpened({ resource: input.getResource() }), input);
+ assert.equal(service.isOpen(input, part.activeGroup), true);
+ assert.equal(activeEditorChangeEventCounter, 1);
+ assert.equal(visibleEditorChangeEventCounter, 1);
+
+ // Close input
+ await editor!.group!.closeEditor(input);
+
+ assert.equal(didCloseEditorListenerCounter, 1);
+ assert.equal(activeEditorChangeEventCounter, 2);
+ assert.equal(visibleEditorChangeEventCounter, 2);
+ assert.ok(input.gotDisposed);
+
+ // Open again 2 inputs
+ await service.openEditor(input, { pinned: true });
+ editor = await service.openEditor(otherInput, { pinned: true });
+
+ assert.equal(service.visibleControls.length, 1);
+ assert.equal(service.isOpen(input), true);
+ assert.equal(service.isOpen(otherInput), true);
+
+ assert.equal(activeEditorChangeEventCounter, 4);
+ assert.equal(visibleEditorChangeEventCounter, 4);
+
+ activeEditorChangeListener.dispose();
+ visibleEditorChangeListener.dispose();
+ didCloseEditorListener.dispose();
});
- test('openEditors() / replaceEditors()', function () {
+ test('openEditors() / replaceEditors()', async () => {
const partInstantiator = workbenchInstantiationService();
const part = partInstantiator.createInstance(EditorPart);
@@ -171,18 +173,16 @@ suite('Editor service', () => {
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors'));
const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-openEditors'));
- return part.whenRestored.then(() => {
+ await part.whenRestored;
- // Open editors
- return service.openEditors([{ editor: input }, { editor: otherInput }]).then(() => {
- assert.equal(part.activeGroup.count, 2);
+ // Open editors
+ await service.openEditors([{ editor: input }, { editor: otherInput }]);
+ assert.equal(part.activeGroup.count, 2);
- return service.replaceEditors([{ editor: input, replacement: replaceInput }], part.activeGroup).then(() => {
- assert.equal(part.activeGroup.count, 2);
- assert.equal(part.activeGroup.getIndexOfEditor(replaceInput), 0);
- });
- });
- });
+ // Replace editors
+ await service.replaceEditors([{ editor: input, replacement: replaceInput }], part.activeGroup);
+ assert.equal(part.activeGroup.count, 2);
+ assert.equal(part.activeGroup.getIndexOfEditor(replaceInput), 0);
});
test('caching', function () {
@@ -234,10 +234,15 @@ suite('Editor service', () => {
assert.ok(!input1AgainAndAgain!.isDisposed());
});
- test('createInput', function () {
+ test('createInput', async function () {
const instantiationService = workbenchInstantiationService();
const service: EditorService = instantiationService.createInstance(EditorService);
+ const mode = 'create-input-test';
+ ModesRegistry.registerLanguage({
+ id: mode,
+ });
+
// Untyped Input (file)
let input = service.createInput({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof FileEditorInput);
@@ -250,6 +255,18 @@ suite('Editor service', () => {
contentInput = input;
assert.equal(contentInput.getPreferredEncoding(), 'utf16le');
+ // Untyped Input (file, mode)
+ input = service.createInput({ resource: toResource.call(this, '/index.html'), mode });
+ assert(input instanceof FileEditorInput);
+ contentInput = input;
+ assert.equal(contentInput.getPreferredMode(), mode);
+
+ // Untyped Input (file, different mode)
+ input = service.createInput({ resource: toResource.call(this, '/index.html'), mode: 'text' });
+ assert(input instanceof FileEditorInput);
+ contentInput = input;
+ assert.equal(contentInput.getPreferredMode(), 'text');
+
// Untyped Input (untitled)
input = service.createInput({ options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
@@ -257,6 +274,14 @@ suite('Editor service', () => {
// Untyped Input (untitled with contents)
input = service.createInput({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
+ let model = await input.resolve() as UntitledEditorModel;
+ assert.equal(model.textEditorModel!.getValue(), 'Hello Untitled');
+
+ // Untyped Input (untitled with mode)
+ input = service.createInput({ mode, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
+ assert(input instanceof UntitledEditorInput);
+ model = await input.resolve() as UntitledEditorModel;
+ assert.equal(model.getMode(), mode);
// Untyped Input (untitled with file path)
input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
@@ -276,6 +301,10 @@ suite('Editor service', () => {
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
provider.dispose();
+
+ // Untyped Input (resource)
+ input = service.createInput({ resource: URI.parse('custom:resource') });
+ assert(input instanceof ResourceEditorInput);
});
test('delegate', function (done) {
@@ -298,7 +327,7 @@ suite('Editor service', () => {
const ed = instantiationService.createInstance(MyEditor, 'my.editor');
- const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'));
+ const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'), undefined);
const delegate = instantiationService.createInstance(DelegatingEditorService);
delegate.setEditorOpenHandler((group: IEditorGroup, input: IEditorInput, options?: EditorOptions) => {
assert.strictEqual(input, inp);
@@ -311,7 +340,7 @@ suite('Editor service', () => {
delegate.openEditor(inp);
});
- test('close editor does not dispose when editor opened in other group', function () {
+ test('close editor does not dispose when editor opened in other group', async () => {
const partInstantiator = workbenchInstantiationService();
const part = partInstantiator.createInstance(EditorPart);
@@ -327,30 +356,26 @@ suite('Editor service', () => {
const rootGroup = part.activeGroup;
const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
- return part.whenRestored.then(() => {
-
- // Open input
- return service.openEditor(input, { pinned: true }).then(editor => {
- return service.openEditor(input, { pinned: true }, rightGroup).then(editor => {
- const editors = service.editors;
- assert.equal(editors.length, 2);
- assert.equal(editors[0], input);
- assert.equal(editors[1], input);
-
- // Close input
- return rootGroup.closeEditor(input).then(() => {
- assert.equal(input.isDisposed(), false);
-
- return rightGroup.closeEditor(input).then(() => {
- assert.equal(input.isDisposed(), true);
- });
- });
- });
- });
- });
+ await part.whenRestored;
+
+ // Open input
+ await service.openEditor(input, { pinned: true });
+ await service.openEditor(input, { pinned: true }, rightGroup);
+
+ const editors = service.editors;
+ assert.equal(editors.length, 2);
+ assert.equal(editors[0], input);
+ assert.equal(editors[1], input);
+
+ // Close input
+ await rootGroup.closeEditor(input);
+ assert.equal(input.isDisposed(), false);
+
+ await rightGroup.closeEditor(input);
+ assert.equal(input.isDisposed(), true);
});
- test('open to the side', function () {
+ test('open to the side', async () => {
const partInstantiator = workbenchInstantiationService();
const part = partInstantiator.createInstance(EditorPart);
@@ -366,22 +391,20 @@ suite('Editor service', () => {
const rootGroup = part.activeGroup;
- return part.whenRestored.then(() => {
- return service.openEditor(input1, { pinned: true }, rootGroup).then(editor => {
- return service.openEditor(input1, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
- assert.equal(part.activeGroup, rootGroup);
- assert.equal(part.count, 2);
- assert.equal(editor!.group, part.groups[1]);
-
- // Open to the side uses existing neighbour group if any
- return service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
- assert.equal(part.activeGroup, rootGroup);
- assert.equal(part.count, 2);
- assert.equal(editor!.group, part.groups[1]);
- });
- });
- });
- });
+ await part.whenRestored;
+
+ await service.openEditor(input1, { pinned: true }, rootGroup);
+ let editor = await service.openEditor(input1, { pinned: true, preserveFocus: true }, SIDE_GROUP);
+
+ assert.equal(part.activeGroup, rootGroup);
+ assert.equal(part.count, 2);
+ assert.equal(editor!.group, part.groups[1]);
+
+ // Open to the side uses existing neighbour group if any
+ editor = await service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP);
+ assert.equal(part.activeGroup, rootGroup);
+ assert.equal(part.count, 2);
+ assert.equal(editor!.group, part.groups[1]);
});
test('active editor change / visible editor change events', async function () {
diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts
index 002a49d799c56..ee813e14fe500 100644
--- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts
+++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts
@@ -31,7 +31,7 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput {
constructor(defaultSettingsResource: URI,
@ITextModelService textModelResolverService: ITextModelService
) {
- super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService);
+ super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService);
}
getTypeId(): string {
diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts
index 612bd746dfbbb..a5c31f63549b5 100644
--- a/src/vs/workbench/services/search/node/fileSearch.ts
+++ b/src/vs/workbench/services/search/node/fileSearch.ts
@@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri';
import { readdir } from 'vs/base/node/pfs';
import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search';
import { spawnRipgrepCmd } from './ripgrepFileSearch';
+import { prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
interface IDirectoryEntry {
base: string;
@@ -76,7 +77,7 @@ export class FileWalker {
this.errors = [];
if (this.filePattern) {
- this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
+ this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).value;
}
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts
index 1d5ed2ca55e66..021aa7bb235f1 100644
--- a/src/vs/workbench/services/search/node/rawSearchService.ts
+++ b/src/vs/workbench/services/search/node/rawSearchService.ts
@@ -312,7 +312,7 @@ export class SearchService implements IRawSearchService {
// Pattern match on results
const results: IRawFileMatch[] = [];
- const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
+ const normalizedSearchValueLowercase = prepareQuery(searchValue).value;
for (const entry of cachedEntries) {
// Check if this entry is a match for the search value
diff --git a/src/vs/workbench/services/search/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts
index 34d9c2bad1c72..03fc9cde8601e 100644
--- a/src/vs/workbench/services/search/test/node/search.test.ts
+++ b/src/vs/workbench/services/search/test/node/search.test.ts
@@ -290,25 +290,25 @@ suite('FileSearchEngine', () => {
});
});
- test('Files: NPE (CamelCase)', function (done: () => void) {
- this.timeout(testTimeout);
- const engine = new FileSearchEngine({
- type: QueryType.File,
- folderQueries: ROOT_FOLDER_QUERY,
- filePattern: 'NullPE'
- });
-
- let count = 0;
- engine.search((result) => {
- if (result) {
- count++;
- }
- }, () => { }, (error) => {
- assert.ok(!error);
- assert.equal(count, 1);
- done();
- });
- });
+ // test('Files: NPE (CamelCase)', function (done: () => void) {
+ // this.timeout(testTimeout);
+ // const engine = new FileSearchEngine({
+ // type: QueryType.File,
+ // folderQueries: ROOT_FOLDER_QUERY,
+ // filePattern: 'NullPE'
+ // });
+
+ // let count = 0;
+ // engine.search((result) => {
+ // if (result) {
+ // count++;
+ // }
+ // }, () => { }, (error) => {
+ // assert.ok(!error);
+ // assert.equal(count, 1);
+ // done();
+ // });
+ // });
test('Files: *.*', function (done: () => void) {
this.timeout(testTimeout);
diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
index ecb2e2bab4a18..199e8c65bb692 100644
--- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
+++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
@@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { URI } from 'vs/base/common/uri';
-import { isUndefinedOrNull, withUndefinedAsNull } from 'vs/base/common/types';
+import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
@@ -18,13 +18,12 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
-import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
+import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { hash } from 'vs/base/common/hash';
-import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isLinux } from 'vs/base/common/platform';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -33,6 +32,13 @@ import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/reso
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
+export interface IBackupMetaData {
+ mtime: number;
+ size: number;
+ etag: string;
+ orphaned: boolean;
+}
+
/**
* 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.
*/
@@ -57,15 +63,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private resource: URI;
- private contentEncoding: string; // encoding as reported from disk
- private preferredEncoding: string; // encoding as chosen by the user
+ private contentEncoding: string; // encoding as reported from disk
+ private preferredEncoding: string; // encoding as chosen by the user
+
+ private preferredMode: string; // mode as chosen by the user
private versionId: number;
private bufferSavedVersionId: number;
private blockModelContentChange: boolean;
- private createTextEditorModelPromise: Promise | null;
-
private lastResolvedDiskStat: IFileStatWithMetadata;
private autoSaveAfterMillies?: number;
@@ -88,6 +94,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
constructor(
resource: URI,
preferredEncoding: string,
+ preferredMode: string,
@INotificationService private readonly notificationService: INotificationService,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@@ -104,6 +111,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.resource = resource;
this.preferredEncoding = preferredEncoding;
+ this.preferredMode = preferredMode;
this.inOrphanMode = false;
this.dirty = false;
this.versionId = 0;
@@ -199,18 +207,40 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private onFilesAssociationChange(): void {
- if (!this.textEditorModel) {
+ if (!this.isResolved()) {
return;
}
const firstLineText = this.getFirstLineText(this.textEditorModel);
- const languageSelection = this.getOrCreateMode(this.modeService, undefined, firstLineText);
+ const languageSelection = this.getOrCreateMode(this.resource, this.modeService, this.preferredMode, firstLineText);
this.modelService.setMode(this.textEditorModel, languageSelection);
}
- getVersionId(): number {
- return this.versionId;
+ setMode(mode: string): void {
+ super.setMode(mode);
+
+ this.preferredMode = mode;
+ }
+
+ async backup(target = this.resource): Promise {
+ if (this.isResolved()) {
+
+ // Only fill in model metadata if resource matches
+ let meta: IBackupMetaData | undefined = undefined;
+ if (isEqual(target, this.resource) && this.lastResolvedDiskStat) {
+ meta = {
+ mtime: this.lastResolvedDiskStat.mtime,
+ size: this.lastResolvedDiskStat.size,
+ etag: this.lastResolvedDiskStat.etag,
+ orphaned: this.inOrphanMode
+ };
+ }
+
+ return this.backupFileService.backupResource(target, this.createSnapshot(), this.versionId, meta);
+ }
+
+ return Promise.resolve();
}
async revert(soft?: boolean): Promise {
@@ -245,7 +275,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
- load(options?: ILoadOptions): Promise {
+ async load(options?: ILoadOptions): Promise {
this.logService.trace('load() - enter', this.resource);
// It is very important to not reload the model when the model is dirty.
@@ -254,44 +284,57 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (this.dirty || this.saveSequentializer.hasPendingSave()) {
this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
- return Promise.resolve(this);
+ return this;
}
// Only for new models we support to load from backup
- if (!this.textEditorModel && !this.createTextEditorModelPromise) {
- return this.loadFromBackup(options);
+ if (!this.isResolved()) {
+ const backup = await this.backupFileService.loadBackupResource(this.resource);
+
+ if (this.isResolved()) {
+ return this; // Make sure meanwhile someone else did not suceed in loading
+ }
+
+ if (backup) {
+ try {
+ return await this.loadFromBackup(backup, options);
+ } catch (error) {
+ // ignore error and continue to load as file below
+ }
+ }
}
// Otherwise load from file resource
return this.loadFromFile(options);
}
- private async loadFromBackup(options?: ILoadOptions): Promise {
- const backup = await this.backupFileService.loadBackupResource(this.resource);
+ private async loadFromBackup(backup: URI, options?: ILoadOptions): Promise {
- // Make sure meanwhile someone else did not suceed or start loading
- if (this.createTextEditorModelPromise || this.textEditorModel) {
- return this.createTextEditorModelPromise || this;
- }
+ // Resolve actual backup contents
+ const resolvedBackup = await this.backupFileService.resolveBackupContent(backup);
- // If we have a backup, continue loading with it
- if (!!backup) {
- const content: ITextFileStreamContent = {
- resource: this.resource,
- name: basename(this.resource),
- mtime: Date.now(),
- size: 0,
- etag: ETAG_DISABLED, // always allow to save content restored from a backup (see https://github.com/Microsoft/vscode/issues/72343)
- value: createTextBufferFactory(''), // will be filled later from backup
- encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
- isReadonly: false
- };
+ if (this.isResolved()) {
+ return this; // Make sure meanwhile someone else did not suceed in loading
+ }
- return this.loadWithContent(content, options, backup);
+ // Load with backup
+ this.loadFromContent({
+ resource: this.resource,
+ name: basename(this.resource),
+ mtime: resolvedBackup.meta ? resolvedBackup.meta.mtime : Date.now(),
+ size: resolvedBackup.meta ? resolvedBackup.meta.size : 0,
+ etag: resolvedBackup.meta ? resolvedBackup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
+ value: resolvedBackup.value,
+ encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
+ isReadonly: false
+ }, options, true /* from backup */);
+
+ // Restore orphaned flag based on state
+ if (resolvedBackup.meta && resolvedBackup.meta.orphaned) {
+ this.setOrphaned(true);
}
- // Otherwise load from file
- return this.loadFromFile(options);
+ return this;
}
private async loadFromFile(options?: ILoadOptions): Promise {
@@ -321,12 +364,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Clear orphaned state when loading was successful
this.setOrphaned(false);
- // Guard against the model having changed in the meantime
- if (currentVersionId === this.versionId) {
- return this.loadWithContent(content, options);
+ if (currentVersionId !== this.versionId) {
+ return this; // Make sure meanwhile someone else did not suceed loading
}
- return this;
+ return this.loadFromContent(content, options);
} catch (error) {
const result = error.fileOperationResult;
@@ -356,33 +398,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
- private async loadWithContent(content: ITextFileStreamContent, options?: ILoadOptions, backup?: URI): Promise {
- const model = await this.doLoadWithContent(content, backup);
-
- // 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
- } else {
- /* __GDPR__
- "fileGet" : {
- "${include}": [
- "${FileTelemetryData}"
- ]
- }
- */
- this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
- }
-
- return model;
- }
-
- private doLoadWithContent(content: ITextFileStreamContent, backup?: URI): Promise {
+ private loadFromContent(content: ITextFileStreamContent, options?: ILoadOptions, fromBackup?: boolean): TextFileEditorModel {
this.logService.trace('load() - resolved content', this.resource);
// Update our resolved disk stat model
@@ -409,21 +425,61 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
// Update Existing Model
- if (this.textEditorModel) {
+ if (this.isResolved()) {
this.doUpdateTextModel(content.value);
+ }
+
+ // Create New Model
+ else {
+ this.doCreateTextModel(content.resource, content.value, !!fromBackup);
+ }
- return Promise.resolve(this);
+ // 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
+ } else {
+ /* __GDPR__
+ "fileGet" : {
+ "${include}": [
+ "${FileTelemetryData}"
+ ]
+ }
+ */
+ this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
}
- // Join an existing request to create the editor model to avoid race conditions
- else if (this.createTextEditorModelPromise) {
- this.logService.trace('load() - join existing text editor model promise', this.resource);
+ return this;
+ }
+
+ private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void {
+ this.logService.trace('load() - created text editor model', this.resource);
+
+ // Create model
+ this.createTextEditorModel(value, resource, this.preferredMode);
- return this.createTextEditorModelPromise;
+ // We restored a backup so we have to set the model as being dirty
+ // We also want to trigger auto save if it is enabled to simulate the exact same behaviour
+ // you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
+ if (fromBackup) {
+ this.makeDirty();
+ if (this.autoSaveAfterMilliesEnabled) {
+ this.doAutoSave(this.versionId);
+ }
}
- // Create New Model
- return this.doCreateTextModel(content.resource, content.value, backup);
+ // Ensure we are not tracking a stale state
+ else {
+ this.setDirty(false);
+ }
+
+ // Model Listeners
+ this.installModelListeners();
}
private doUpdateTextModel(value: ITextBufferFactory): void {
@@ -435,7 +491,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Update model value in a block that ignores model content change events
this.blockModelContentChange = true;
try {
- this.updateTextEditorModel(value);
+ this.updateTextEditorModel(value, this.preferredMode);
} finally {
this.blockModelContentChange = false;
}
@@ -444,44 +500,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.updateSavedVersionId();
}
- private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI | undefined): Promise {
- this.logService.trace('load() - created text editor model', this.resource);
-
- this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
- this.createTextEditorModelPromise = null;
-
- // Create model
- const hasBackupContent = !!backupContent;
- this.createTextEditorModel(backupContent ? backupContent : value, resource);
-
- // We restored a backup so we have to set the model as being dirty
- // We also want to trigger auto save if it is enabled to simulate the exact same behaviour
- // you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
- if (hasBackupContent) {
- this.makeDirty();
- if (this.autoSaveAfterMilliesEnabled) {
- this.doAutoSave(this.versionId);
- }
- }
-
- // Ensure we are not tracking a stale state
- else {
- this.setDirty(false);
- }
-
- // Model Listeners
- this.installModelListeners();
-
- return this;
- }, error => {
- this.createTextEditorModelPromise = null;
-
- return Promise.reject(error);
- });
-
- return this.createTextEditorModelPromise;
- }
-
private installModelListeners(): void {
// See https://github.com/Microsoft/vscode/issues/30189
@@ -489,27 +507,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// where `value` was captured in the content change listener closure scope.
// Content Change
- if (this.textEditorModel) {
+ if (this.isResolved()) {
this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
}
}
- private async doLoadBackup(backup: URI | undefined): Promise {
- if (!backup) {
- return null;
- }
-
- try {
- return withUndefinedAsNull(await this.backupFileService.resolveBackupContent(backup));
- } catch (error) {
- return null; // ignore errors
- }
- }
-
- protected getOrCreateMode(modeService: IModeService, preferredModeIds: string | undefined, firstLineText?: string): ILanguageSelection {
- return modeService.createByFilepathOrFirstLine(this.resource.fsPath, firstLineText);
- }
-
private onModelContentChanged(): void {
this.logService.trace(`onModelContentChanged() - enter`, this.resource);
@@ -526,7 +528,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
// Note: we currently only do this check when auto-save is turned off because there you see
// a dirty indicator that you want to get rid of when undoing to the saved version.
- if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
+ if (!this.autoSaveAfterMilliesEnabled && this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
// Clear flags
@@ -657,7 +659,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Push all edit operations to the undo stack so that the user has a chance to
// Ctrl+Z back to the saved version. We only do this when auto-save is turned off
- if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel) {
+ if (!this.autoSaveAfterMilliesEnabled && this.isResolved()) {
this.textEditorModel.pushStackElement();
}
@@ -687,7 +689,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942).
// To fix this issue, we will not store the contents to disk when we got disposed.
if (this.disposed) {
- return undefined;
+ return;
+ }
+
+ // We require a resolved model from this point on, since we are about to write data to disk.
+ if (!this.isResolved()) {
+ return;
}
// Under certain conditions we do a short-cut of flushing contents to disk when we can assume that
@@ -713,11 +720,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Save to Disk
// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
this.logService.trace(`doSave(${versionId}) - before write()`, this.resource);
- const snapshot = this.createSnapshot();
- if (!snapshot) {
- throw new Error('Invalid snapshot');
- }
- return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
+ return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
overwriteReadonly: options.overwriteReadonly,
overwriteEncoding: options.overwriteEncoding,
mtime: this.lastResolvedDiskStat.mtime,
@@ -850,12 +853,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private doTouch(versionId: number): Promise {
- const snapshot = this.createSnapshot();
- if (!snapshot) {
- throw new Error('invalid snapshot');
+ if (!this.isResolved()) {
+ return Promise.resolve();
}
- return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
+ return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
mtime: this.lastResolvedDiskStat.mtime,
encoding: this.getEncoding(),
etag: this.lastResolvedDiskStat.etag
@@ -863,6 +865,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Updated resolved stat with updated stat since touching it might have changed mtime
this.updateLastResolvedDiskStat(stat);
+
+ // Emit File Saved Event
+ this._onDidStateChange.fire(StateChange.SAVED);
+
}, error => onUnexpectedError(error) /* just log any error but do not notify the user since the file was not dirty */));
}
@@ -896,7 +902,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// in order to find out if the model changed back to a saved version (e.g.
// when undoing long enough to reach to a version that is saved and then to
// clear the dirty flag)
- if (this.textEditorModel) {
+ if (this.isResolved()) {
this.bufferSavedVersionId = this.textEditorModel.getAlternativeVersionId();
}
}
@@ -935,10 +941,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.lastSaveAttemptTime;
}
- getETag(): string | null {
- return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag || null : null;
- }
-
hasState(state: ModelState): boolean {
switch (state) {
case ModelState.CONFLICT:
@@ -1020,8 +1022,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return true;
}
- isResolved(): boolean {
- return !isUndefinedOrNull(this.lastResolvedDiskStat);
+ isResolved(): this is IResolvedTextFileEditorModel {
+ return !!this.textEditorModel;
}
isReadonly(): boolean {
@@ -1046,8 +1048,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.inOrphanMode = false;
this.inErrorMode = false;
- this.createTextEditorModelPromise = null;
-
this.cancelPendingAutoSave();
super.dispose();
diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
index 1baca93526c51..af67898ef698b 100644
--- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
+++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
@@ -153,7 +153,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
// Model does not exist
else {
- const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined);
+ const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined, options ? options.mode : undefined);
modelPromise = model.load(options);
// Install state change listener
@@ -204,6 +204,11 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
// Remove from pending loads
this.mapResourceToPendingModelLoaders.delete(resource);
+ // Apply mode if provided
+ if (options && options.mode) {
+ resolvedModel.setMode(options.mode);
+ }
+
return resolvedModel;
} catch (error) {
diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts
index 1c513e1bca5ec..f7884a3e7b2f8 100644
--- a/src/vs/workbench/services/textfile/common/textFileService.ts
+++ b/src/vs/workbench/services/textfile/common/textFileService.ts
@@ -40,6 +40,7 @@ import { trim } from 'vs/base/common/strings';
import { VSBuffer } from 'vs/base/common/buffer';
import { ITextSnapshot } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
+import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
@@ -238,59 +239,44 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise {
// Handle file resources first
- await Promise.all(dirtyFileModels.map(async model => {
- const snapshot = model.createSnapshot();
- if (snapshot) {
- await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
- }
- }));
+ await Promise.all(dirtyFileModels.map(model => model.backup()));
// Handle untitled resources
- const untitledModelPromises = untitledResources
+ await Promise.all(untitledResources
.filter(untitled => this.untitledEditorService.exists(untitled))
- .map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
-
- const untitledModels = await Promise.all(untitledModelPromises);
-
- await Promise.all(untitledModels.map(async model => {
- const snapshot = model.createSnapshot();
- if (snapshot) {
- await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
- }
- }));
+ .map(async untitled => (await this.untitledEditorService.loadOrCreate({ resource: untitled })).backup()));
}
- private confirmBeforeShutdown(): boolean | Promise {
- return this.confirmSave().then(confirm => {
+ private async confirmBeforeShutdown(): Promise {
+ const confirm = await this.confirmSave();
- // Save
- if (confirm === ConfirmResult.SAVE) {
- return this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true }).then(result => {
- if (result.results.some(r => !r.success)) {
- return true; // veto if some saves failed
- }
+ // Save
+ if (confirm === ConfirmResult.SAVE) {
+ const result = await this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true });
- return this.noVeto({ cleanUpBackups: true });
- });
+ if (result.results.some(r => !r.success)) {
+ return true; // veto if some saves failed
}
- // Don't Save
- else if (confirm === ConfirmResult.DONT_SAVE) {
+ return this.noVeto({ cleanUpBackups: true });
+ }
+
+ // Don't Save
+ else if (confirm === ConfirmResult.DONT_SAVE) {
- // Make sure to revert untitled so that they do not restore
- // see https://github.com/Microsoft/vscode/issues/29572
- this.untitledEditorService.revertAll();
+ // Make sure to revert untitled so that they do not restore
+ // see https://github.com/Microsoft/vscode/issues/29572
+ this.untitledEditorService.revertAll();
- return this.noVeto({ cleanUpBackups: true });
- }
+ return this.noVeto({ cleanUpBackups: true });
+ }
- // Cancel
- else if (confirm === ConfirmResult.CANCEL) {
- return true; // veto
- }
+ // Cancel
+ else if (confirm === ConfirmResult.CANCEL) {
+ return true; // veto
+ }
- return false;
- });
+ return false;
}
private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise {
@@ -503,10 +489,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
dirtyTargetModelUris.push(targetModelResource);
// Backup dirty source model to the target resource it will become later
- const snapshot = sourceModel.createSnapshot();
- if (snapshot) {
- await this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId());
- }
+ await sourceModel.backup(targetModelResource);
}));
}
@@ -872,17 +855,12 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// 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.isResolved() && targetModel.isResolved()) {
+ this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
- if (sourceModel.textEditorModel) {
- const language = sourceModel.textEditorModel.getLanguageIdentifier();
- if (language.id > 1) {
- targetModel.textEditorModel.setMode(language); // only use if more specific than plain/text
- }
+ const mode = sourceModel.textEditorModel.getLanguageIdentifier();
+ if (mode.language !== PLAINTEXT_MODE_ID) {
+ targetModel.textEditorModel.setMode(mode); // only use if more specific than plain/text
}
}
diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts
index 422ea39499b6a..f3db50a0396d0 100644
--- a/src/vs/workbench/services/textfile/common/textfiles.ts
+++ b/src/vs/workbench/services/textfile/common/textfiles.ts
@@ -6,7 +6,7 @@
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
-import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
+import { IEncodingSupport, ConfirmResult, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor';
import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
@@ -367,6 +367,11 @@ export interface IModelLoadOrCreateOptions {
*/
reason?: LoadReason;
+ /**
+ * The language mode to use for the model text content.
+ */
+ mode?: string;
+
/**
* The encoding to use when resolving the model text content.
*/
@@ -443,19 +448,15 @@ export interface ILoadOptions {
reason?: LoadReason;
}
-export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
+export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport {
readonly onDidContentChange: Event;
readonly onDidStateChange: Event;
- getVersionId(): number;
-
getResource(): URI;
hasState(state: ModelState): boolean;
- getETag(): string | null;
-
updatePreferredEncoding(encoding: string): void;
save(options?: ISaveOptions): Promise;
@@ -464,16 +465,17 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
revert(soft?: boolean): Promise;
- createSnapshot(): ITextSnapshot | null;
+ backup(target?: URI): Promise;
isDirty(): boolean;
- isResolved(): boolean;
+ isResolved(): this is IResolvedTextFileEditorModel;
isDisposed(): boolean;
}
export interface IResolvedTextFileEditorModel extends ITextFileEditorModel {
+
readonly textEditorModel: ITextModel;
createSnapshot(): ITextSnapshot;
diff --git a/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts b/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts
index e9a71315beac1..ce097d6fdf562 100644
--- a/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts
+++ b/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts
@@ -17,7 +17,7 @@ import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiatio
export class TextResourcePropertiesService implements ITextResourcePropertiesService {
- _serviceBrand: ServiceIdentifier;
+ _serviceBrand: ServiceIdentifier;
private remoteEnvironment: IRemoteAgentEnvironment | null = null;
diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts
index ac6f037067292..e22cbe034dbb2 100644
--- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts
+++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts
@@ -14,6 +14,7 @@ import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/commo
import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
+import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
class ServiceAccessor {
constructor(@ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService, @IFileService public fileService: TestFileService) {
@@ -44,25 +45,53 @@ suite('Files - TextFileEditorModel', () => {
accessor.fileService.setContent(content);
});
- test('Save', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ test('save', async function () {
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
model.textEditorModel!.setValue('bar');
assert.ok(getLastModifiedTime(model) <= Date.now());
+ let savedEvent = false;
+ model.onDidStateChange(e => {
+ if (e === StateChange.SAVED) {
+ savedEvent = true;
+ }
+ });
+
await model.save();
assert.ok(model.getLastSaveAttemptTime() <= Date.now());
assert.ok(!model.isDirty());
+ assert.ok(savedEvent);
+
+ model.dispose();
+ assert.ok(!accessor.modelService.getModel(model.getResource()));
+ });
+
+ test('save - touching also emits saved event', async function () {
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
+
+ await model.load();
+
+ let savedEvent = false;
+ model.onDidStateChange(e => {
+ if (e === StateChange.SAVED) {
+ savedEvent = true;
+ }
+ });
+
+ await model.save({ force: true });
+
+ assert.ok(savedEvent);
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
});
test('setEncoding - encode', function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.setEncoding('utf8', EncodingMode.Encode); // no-op
assert.equal(getLastModifiedTime(model), -1);
@@ -75,7 +104,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('setEncoding - decode', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.setEncoding('utf16', EncodingMode.Decode);
@@ -84,8 +113,24 @@ suite('Files - TextFileEditorModel', () => {
model.dispose();
});
+ test('create with mode', async function () {
+ const mode = 'text-file-model-test';
+ ModesRegistry.registerLanguage({
+ id: mode,
+ });
+
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', mode);
+
+ await model.load();
+
+ assert.equal(model.textEditorModel!.getModeId(), mode);
+
+ model.dispose();
+ assert.ok(!accessor.modelService.getModel(model.getResource()));
+ });
+
test('disposes when underlying model is destroyed', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
@@ -94,7 +139,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('Load does not trigger save', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8');
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined);
assert.ok(model.hasState(ModelState.SAVED));
model.onDidStateChange(e => {
@@ -108,7 +153,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('Load returns dirty model as long as model is dirty', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
model.textEditorModel!.setValue('foo');
@@ -123,7 +168,7 @@ suite('Files - TextFileEditorModel', () => {
test('Revert', async function () {
let eventCounter = 0;
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.onDidStateChange(e => {
if (e === StateChange.REVERTED) {
@@ -145,7 +190,7 @@ suite('Files - TextFileEditorModel', () => {
test('Revert (soft)', async function () {
let eventCounter = 0;
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.onDidStateChange(e => {
if (e === StateChange.REVERTED) {
@@ -165,7 +210,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('Load and undo turns model dirty', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
accessor.fileService.setContent('Hello Change');
@@ -175,7 +220,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('File not modified error is handled gracefully', async function () {
- let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
@@ -190,7 +235,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('Load error is handled gracefully if model already exists', async function () {
- let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND));
@@ -236,7 +281,7 @@ suite('Files - TextFileEditorModel', () => {
test('Save Participant', async function () {
let eventCounter = 0;
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
model.onDidStateChange(e => {
if (e === StateChange.SAVED) {
@@ -266,7 +311,7 @@ suite('Files - TextFileEditorModel', () => {
test('Save Participant, async participant', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
TextFileEditorModel.setSaveParticipant({
participate: (model) => {
@@ -284,7 +329,7 @@ suite('Files - TextFileEditorModel', () => {
});
test('Save Participant, bad participant', async function () {
- const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
+ const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
TextFileEditorModel.setSaveParticipant({
participate: (model) => {
diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts
index 15be3b8533f0b..1b5f490f490e3 100644
--- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts
+++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts
@@ -13,6 +13,7 @@ import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/file
import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
import { toResource } from 'vs/base/test/common/utils';
+import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
@@ -42,9 +43,9 @@ suite('Files - TextFileEditorModelManager', () => {
test('add, remove, clear, get, getAll', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
- const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
- const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
- const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
+ const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
+ const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
+ const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined);
manager.add(URI.file('/test.html'), model1);
manager.add(URI.file('/some/other.html'), model2);
@@ -117,9 +118,9 @@ suite('Files - TextFileEditorModelManager', () => {
test('removed from cache when model disposed', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
- const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
- const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
- const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
+ const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
+ const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
+ const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined);
manager.add(URI.file('/test.html'), model1);
manager.add(URI.file('/some/other.html'), model2);
@@ -290,4 +291,24 @@ suite('Files - TextFileEditorModelManager', () => {
assert.ok(model.isDisposed());
manager.dispose();
});
+
+ test('mode', async function () {
+ const mode = 'text-file-model-manager-test';
+ ModesRegistry.registerLanguage({
+ id: mode,
+ });
+
+ const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
+
+ const resource = toResource.call(this, '/path/index_something.txt');
+
+ let model = await manager.loadOrCreate(resource, { mode });
+ assert.equal(model.textEditorModel!.getModeId(), mode);
+
+ model = await manager.loadOrCreate(resource, { mode: 'text' });
+ assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID);
+
+ manager.disposeModel((model as TextFileEditorModel));
+ manager.dispose();
+ });
});
\ No newline at end of file
diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts
index 72049db9f118a..ce5dd5dd7727e 100644
--- a/src/vs/workbench/services/textfile/test/textFileService.test.ts
+++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts
@@ -65,8 +65,8 @@ suite('Files - TextFileService', () => {
accessor.untitledEditorService.revertAll();
});
- test('confirm onWillShutdown - no veto', function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ test('confirm onWillShutdown - no veto', async function () {
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const event = new BeforeShutdownEventImpl();
@@ -76,14 +76,12 @@ suite('Files - TextFileService', () => {
if (typeof veto === 'boolean') {
assert.ok(!veto);
} else {
- veto.then(veto => {
- assert.ok(!veto);
- });
+ assert.ok(!(await veto));
}
});
test('confirm onWillShutdown - veto if user cancels', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -99,7 +97,7 @@ suite('Files - TextFileService', () => {
});
test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -125,7 +123,7 @@ suite('Files - TextFileService', () => {
});
test('confirm onWillShutdown - save (hot.exit: off)', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -144,7 +142,7 @@ suite('Files - TextFileService', () => {
});
test('isDirty/getDirty - files and untitled', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -171,7 +169,7 @@ suite('Files - TextFileService', () => {
});
test('save - file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -187,11 +185,11 @@ suite('Files - TextFileService', () => {
test('save - UNC path', async function () {
const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' });
- model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const mockedFileUri = untitledUncUri.with({ scheme: Schemas.file });
- const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8');
+ const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8', undefined);
const loadOrCreateStub = sinon.stub(accessor.textFileService.models, 'loadOrCreate', () => Promise.resolve(mockedEditorInput));
sinon.stub(accessor.untitledEditorService, 'exists', () => true);
@@ -211,7 +209,7 @@ suite('Files - TextFileService', () => {
});
test('saveAll - file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -228,7 +226,7 @@ suite('Files - TextFileService', () => {
});
test('saveAs - file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -244,7 +242,7 @@ suite('Files - TextFileService', () => {
});
test('revert - file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -260,7 +258,7 @@ suite('Files - TextFileService', () => {
});
test('delete - dirty file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
@@ -274,8 +272,8 @@ suite('Files - TextFileService', () => {
});
test('move - dirty file', async function () {
- let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
- let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8');
+ let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
+ let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(sourceModel.getResource(), sourceModel);
(accessor.textFileService.models).add(targetModel.getResource(), targetModel);
@@ -395,7 +393,7 @@ suite('Files - TextFileService', () => {
});
async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
+ model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
(accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts
index 10e4b34c2fb7c..b924c55963df8 100644
--- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts
+++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts
@@ -53,7 +53,7 @@ suite('Workbench - TextModelResolverService', () => {
accessor.untitledEditorService.revertAll();
});
- test('resolve resource', function () {
+ test('resolve resource', async () => {
const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', {
provideTextContent: function (resource: URI): Promise {
if (resource.scheme === 'test') {
@@ -67,67 +67,60 @@ suite('Workbench - TextModelResolverService', () => {
});
let resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' });
- let input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource);
-
- return input.resolve().then(async model => {
- assert.ok(model);
- assert.equal(snapshotToString((model as ResourceEditorModel).createSnapshot()!), 'Hello Test');
-
- let disposed = false;
- let disposedPromise = new Promise(resolve => {
- Event.once(model.onDispose)(() => {
- disposed = true;
- resolve();
- });
+ let input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource, undefined);
+
+ const model = await input.resolve();
+ assert.ok(model);
+ assert.equal(snapshotToString(((model as ResourceEditorModel).createSnapshot()!)), 'Hello Test');
+ let disposed = false;
+ let disposedPromise = new Promise(resolve => {
+ Event.once(model.onDispose)(() => {
+ disposed = true;
+ resolve();
});
- input.dispose();
- await disposedPromise;
- assert.equal(disposed, true);
-
- dispose.dispose();
});
+ input.dispose();
+
+ await disposedPromise;
+ assert.equal(disposed, true);
+ dispose.dispose();
});
- test('resolve file', function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8');
- (accessor.textFileService.models).add(model.getResource(), model);
+ test('resolve file', async function () {
+ const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined);
+ (accessor.textFileService.models).add(textModel.getResource(), textModel);
- return model.load().then(() => {
- return accessor.textModelResolverService.createModelReference(model.getResource()).then(ref => {
- const model = ref.object;
- const editorModel = model.textEditorModel;
+ await textModel.load();
- assert.ok(editorModel);
- assert.equal(editorModel.getValue(), 'Hello Html');
+ const ref = await accessor.textModelResolverService.createModelReference(textModel.getResource());
- let disposed = false;
- Event.once(model.onDispose)(() => {
- disposed = true;
- });
+ const model = ref.object;
+ const editorModel = model.textEditorModel;
- ref.dispose();
- return timeout(0).then(() => { // due to the reference resolving the model first which is async
- assert.equal(disposed, true);
- });
- });
+ assert.ok(editorModel);
+ assert.equal(editorModel.getValue(), 'Hello Html');
+
+ let disposed = false;
+ Event.once(model.onDispose)(() => {
+ disposed = true;
});
+
+ ref.dispose();
+ await timeout(0); // due to the reference resolving the model first which is async
+ assert.equal(disposed, true);
});
- test('resolve untitled', function () {
+ test('resolve untitled', async () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
- return input.resolve().then(() => {
- return accessor.textModelResolverService.createModelReference(input.getResource()).then(ref => {
- const model = ref.object;
- const editorModel = model.textEditorModel;
-
- assert.ok(editorModel);
- ref.dispose();
-
- input.dispose();
- });
- });
+ await input.resolve();
+ const ref = await accessor.textModelResolverService.createModelReference(input.getResource());
+ const model = ref.object;
+ const editorModel = model.textEditorModel;
+ assert.ok(editorModel);
+ ref.dispose();
+ input.dispose();
});
test('even loading documents should be refcounted', async () => {
@@ -135,12 +128,12 @@ suite('Workbench - TextModelResolverService', () => {
let waitForIt = new Promise(c => resolveModel = c);
const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', {
- provideTextContent: (resource: URI): Promise => {
- return waitForIt.then(_ => {
- let modelContent = 'Hello Test';
- let languageSelection = accessor.modeService.create('json');
- return accessor.modelService.createModel(modelContent, languageSelection, resource);
- });
+ provideTextContent: async (resource: URI): Promise => {
+ await waitForIt;
+
+ let modelContent = 'Hello Test';
+ let languageSelection = accessor.modeService.create('json');
+ return accessor.modelService.createModel(modelContent, languageSelection, resource);
}
});
diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts
index 6b343944f7c63..35b571ec52be6 100644
--- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts
+++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts
@@ -21,7 +21,7 @@ export const IUntitledEditorService = createDecorator('u
export interface IModelLoadOrCreateOptions {
resource?: URI;
- modeId?: string;
+ mode?: string;
initialValue?: string;
encoding?: string;
useResourcePath?: boolean;
@@ -29,7 +29,7 @@ export interface IModelLoadOrCreateOptions {
export interface IUntitledEditorService {
- _serviceBrand: any;
+ _serviceBrand: ServiceIdentifier;
/**
* Events for when untitled editors content changes (e.g. any keystroke).
@@ -78,7 +78,7 @@ export interface IUntitledEditorService {
* It is valid to pass in a file resource. In that case the path will be used as identifier.
* The use case is to be able to create a new file with a specific path with VSCode.
*/
- createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string): UntitledEditorInput;
+ createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string): UntitledEditorInput;
/**
* Creates a new untitled model with the optional resource URI or returns an existing one
@@ -184,10 +184,10 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
}
loadOrCreate(options: IModelLoadOrCreateOptions = Object.create(null)): Promise {
- return this.createOrGet(options.resource, options.modeId, options.initialValue, options.encoding, options.useResourcePath).resolve();
+ return this.createOrGet(options.resource, options.mode, options.initialValue, options.encoding, options.useResourcePath).resolve();
}
- createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput {
+ createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput {
if (resource) {
// Massage resource if it comes with known file based resource
@@ -207,44 +207,47 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
}
// Create new otherwise
- return this.doCreate(resource, hasAssociatedFilePath, modeId, initialValue, encoding);
+ return this.doCreate(resource, hasAssociatedFilePath, mode, initialValue, encoding);
}
- private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, modeId?: string, initialValue?: string, encoding?: string): UntitledEditorInput {
- if (!resource) {
+ private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, mode?: string, initialValue?: string, encoding?: string): UntitledEditorInput {
+ let untitledResource: URI;
+ if (resource) {
+ untitledResource = resource;
+ } else {
// Create new taking a resource URI that is not already taken
let counter = this.mapResourceToInput.size + 1;
do {
- resource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` });
+ untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` });
counter++;
- } while (this.mapResourceToInput.has(resource));
+ } while (this.mapResourceToInput.has(untitledResource));
}
// Look up default language from settings if any
- if (!modeId && !hasAssociatedFilePath) {
+ if (!mode && !hasAssociatedFilePath) {
const configuration = this.configurationService.getValue();
if (configuration.files && configuration.files.defaultLanguage) {
- modeId = configuration.files.defaultLanguage;
+ mode = configuration.files.defaultLanguage;
}
}
- const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId, initialValue, encoding);
+ const input = this.instantiationService.createInstance(UntitledEditorInput, untitledResource, hasAssociatedFilePath, mode, initialValue, encoding);
const contentListener = input.onDidModelChangeContent(() => {
- this._onDidChangeContent.fire(resource!);
+ this._onDidChangeContent.fire(untitledResource);
});
const dirtyListener = input.onDidChangeDirty(() => {
- this._onDidChangeDirty.fire(resource!);
+ this._onDidChangeDirty.fire(untitledResource);
});
const encodingListener = input.onDidModelChangeEncoding(() => {
- this._onDidChangeEncoding.fire(resource!);
+ this._onDidChangeEncoding.fire(untitledResource);
});
const disposeListener = input.onDispose(() => {
- this._onDidDisposeModel.fire(resource!);
+ this._onDidDisposeModel.fire(untitledResource);
});
// Remove from cache on dispose
@@ -259,7 +262,7 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
});
// Add to cache
- this.mapResourceToInput.set(resource, input);
+ this.mapResourceToInput.set(untitledResource, input);
return input;
}
diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts
index 4f7400dbc916c..c1ab20f25737e 100644
--- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts
+++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts
@@ -86,7 +86,7 @@ class MyResourceInput extends ResourceEditorInput { }
suite('Workbench base editor', () => {
- test('BaseEditor API', function () {
+ test('BaseEditor API', async () => {
let e = new MyEditor(NullTelemetryService);
let input = new MyOtherInput();
let options = new EditorOptions();
@@ -94,25 +94,24 @@ suite('Workbench base editor', () => {
assert(!e.isVisible());
assert(!e.input);
assert(!e.options);
- return e.setInput(input, options, CancellationToken.None).then(() => {
- assert.strictEqual(input, e.input);
- assert.strictEqual(options, e.options);
-
- const group = new TestEditorGroup(1);
- e.setVisible(true, group);
- assert(e.isVisible());
- assert.equal(e.group, group);
- input.onDispose(() => {
- assert(false);
- });
- e.dispose();
- e.clearInput();
- e.setVisible(false, group);
- assert(!e.isVisible());
- assert(!e.input);
- assert(!e.options);
- assert(!e.getControl());
+
+ await e.setInput(input, options, CancellationToken.None);
+ assert.strictEqual(input, e.input);
+ assert.strictEqual(options, e.options);
+ const group = new TestEditorGroup(1);
+ e.setVisible(true, group);
+ assert(e.isVisible());
+ assert.equal(e.group, group);
+ input.onDispose(() => {
+ assert(false);
});
+ e.dispose();
+ e.clearInput();
+ e.setVisible(false, group);
+ assert(!e.isVisible());
+ assert(!e.input);
+ assert(!e.options);
+ assert(!e.getControl());
});
test('EditorDescriptor', () => {
@@ -154,10 +153,10 @@ suite('Workbench base editor', () => {
let inst = new TestInstantiationService();
- const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake')))!.instantiate(inst);
+ const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst);
assert.strictEqual(editor.getId(), 'myEditor');
- const otherEditor = EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, 'fake', '', URI.file('/fake')))!.instantiate(inst);
+ const otherEditor = EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst);
assert.strictEqual(otherEditor.getId(), 'myOtherEditor');
(EditorRegistry).setEditors(oldEditors);
@@ -173,7 +172,7 @@ suite('Workbench base editor', () => {
let inst = new TestInstantiationService();
- const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake')))!.instantiate(inst);
+ const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst);
assert.strictEqual('myOtherEditor', editor.getId());
(EditorRegistry).setEditors(oldEditors);
diff --git a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts
index 3023add68cab8..662bd56d2242d 100644
--- a/src/vs/workbench/test/common/editor/editorDiffModel.test.ts
+++ b/src/vs/workbench/test/common/editor/editorDiffModel.test.ts
@@ -35,7 +35,7 @@ suite('Workbench editor model', () => {
accessor = instantiationService.createInstance(ServiceAccessor);
});
- test('TextDiffEditorModel', () => {
+ test('TextDiffEditorModel', async () => {
const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', {
provideTextContent: function (resource: URI): Promise {
if (resource.scheme === 'test') {
@@ -48,27 +48,26 @@ suite('Workbench editor model', () => {
}
});
- let input = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.from({ scheme: 'test', authority: null!, path: 'thePath' }));
- let otherInput = instantiationService.createInstance(ResourceEditorInput, 'name2', 'description', URI.from({ scheme: 'test', authority: null!, path: 'thePath' }));
+ let input = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), undefined);
+ let otherInput = instantiationService.createInstance(ResourceEditorInput, 'name2', 'description', URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), undefined);
let diffInput = new DiffEditorInput('name', 'description', input, otherInput);
- return diffInput.resolve().then((model: any) => {
- assert(model);
- assert(model instanceof TextDiffEditorModel);
+ let model = await diffInput.resolve() as TextDiffEditorModel;
- let diffEditorModel = model.textDiffEditorModel;
- assert(diffEditorModel.original);
- assert(diffEditorModel.modified);
+ assert(model);
+ assert(model instanceof TextDiffEditorModel);
- return diffInput.resolve().then((model: any) => {
- assert(model.isResolved());
+ let diffEditorModel = model.textDiffEditorModel!;
+ assert(diffEditorModel.original);
+ assert(diffEditorModel.modified);
- assert(diffEditorModel !== model.textDiffEditorModel);
- diffInput.dispose();
- assert(!model.textDiffEditorModel);
+ model = await diffInput.resolve() as TextDiffEditorModel;
+ assert(model.isResolved());
- dispose.dispose();
- });
- });
+ assert(diffEditorModel !== model.textDiffEditorModel);
+ diffInput.dispose();
+ assert(!model.textDiffEditorModel);
+
+ dispose.dispose();
});
});
diff --git a/src/vs/workbench/test/common/editor/editorGroups.test.ts b/src/vs/workbench/test/common/editor/editorGroups.test.ts
index fde39c097bee4..18d862a91ebc8 100644
--- a/src/vs/workbench/test/common/editor/editorGroups.test.ts
+++ b/src/vs/workbench/test/common/editor/editorGroups.test.ts
@@ -111,27 +111,17 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput {
}
getTypeId() { return 'testFileEditorInputForGroups'; }
resolve(): Promise { return Promise.resolve(null!); }
+ setEncoding(encoding: string) { }
+ getEncoding(): string { return null!; }
+ setPreferredEncoding(encoding: string) { }
+ getResource(): URI { return this.resource; }
+ setForceOpenAsBinary(): void { }
+ setMode(mode: string) { }
+ setPreferredMode(mode: string) { }
matches(other: TestFileEditorInput): boolean {
return other && this.id === other.id && other instanceof TestFileEditorInput;
}
-
- setEncoding(encoding: string) {
- }
-
- getEncoding(): string {
- return null!;
- }
-
- setPreferredEncoding(encoding: string) {
- }
-
- getResource(): URI {
- return this.resource;
- }
-
- setForceOpenAsBinary(): void {
- }
}
function input(id = String(index++), nonSerializable?: boolean, resource?: URI): EditorInput {
diff --git a/src/vs/workbench/test/common/editor/editorModel.test.ts b/src/vs/workbench/test/common/editor/editorModel.test.ts
index 513d2783fa1f5..663f926850135 100644
--- a/src/vs/workbench/test/common/editor/editorModel.test.ts
+++ b/src/vs/workbench/test/common/editor/editorModel.test.ts
@@ -21,8 +21,8 @@ import { TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTe
class MyEditorModel extends EditorModel { }
class MyTextEditorModel extends BaseTextEditorModel {
- public createTextEditorModel(value: ITextBufferFactory, resource?: URI, modeId?: string) {
- return super.createTextEditorModel(value, resource, modeId);
+ public createTextEditorModel(value: ITextBufferFactory, resource?: URI, preferredMode?: string) {
+ return super.createTextEditorModel(value, resource, preferredMode);
}
isReadonly(): boolean {
@@ -40,7 +40,7 @@ suite('Workbench editor model', () => {
modeService = instantiationService.stub(IModeService, ModeServiceImpl);
});
- test('EditorModel', () => {
+ test('EditorModel', async () => {
let counter = 0;
let m = new MyEditorModel();
@@ -50,25 +50,23 @@ suite('Workbench editor model', () => {
counter++;
});
- return m.load().then(model => {
- assert(model === m);
- assert.strictEqual(m.isResolved(), true);
- m.dispose();
- assert.equal(counter, 1);
- });
+ const model = await m.load();
+ assert(model === m);
+ assert.strictEqual(m.isResolved(), true);
+ m.dispose();
+ assert.equal(counter, 1);
});
- test('BaseTextEditorModel', () => {
+ test('BaseTextEditorModel', async () => {
let modelService = stubModelService(instantiationService);
let m = new MyTextEditorModel(modelService, modeService);
- return m.load().then((model: MyTextEditorModel) => {
- assert(model === m);
- model.createTextEditorModel(createTextBufferFactory('foo'), null!, 'text/plain');
- assert.strictEqual(m.isResolved(), true);
- }).then(() => {
- m.dispose();
- });
+ const model = await m.load() as MyTextEditorModel;
+
+ assert(model === m);
+ model.createTextEditorModel(createTextBufferFactory('foo'), null!, 'text/plain');
+ assert.strictEqual(m.isResolved(), true);
+ m.dispose();
});
function stubModelService(instantiationService: TestInstantiationService): IModelService {
diff --git a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts
index 954b83d938431..d1e788e80f49c 100644
--- a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts
+++ b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts
@@ -12,17 +12,16 @@ import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestSe
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
+import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
class ServiceAccessor {
constructor(
@IModelService public modelService: IModelService,
@IModeService public modeService: IModeService
- ) {
- }
+ ) { }
}
suite('Workbench resource editor input', () => {
-
let instantiationService: IInstantiationService;
let accessor: ServiceAccessor;
@@ -31,14 +30,33 @@ suite('Workbench resource editor input', () => {
accessor = instantiationService.createInstance(ServiceAccessor);
});
- test('simple', () => {
- let resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' });
+ test('basics', async () => {
+ const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' });
accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource);
- let input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource);
- return input.resolve().then(model => {
- assert.ok(model);
- assert.equal(snapshotToString((model as ResourceEditorModel).createSnapshot()!), 'function test() {}');
+ const input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource, undefined);
+
+ const model = await input.resolve();
+
+ assert.ok(model);
+ assert.equal(snapshotToString(((model as ResourceEditorModel).createSnapshot()!)), 'function test() {}');
+ });
+
+ test('custom mode', async () => {
+ ModesRegistry.registerLanguage({
+ id: 'resource-input-test',
});
+
+ const resource = URI.from({ scheme: 'inmemory', authority: null!, path: 'thePath' });
+ accessor.modelService.createModel('function test() {}', accessor.modeService.create('text'), resource);
+
+ const input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource, 'resource-input-test');
+
+ const model = await input.resolve();
+ assert.ok(model);
+ assert.equal(model.textEditorModel.getModeId(), 'resource-input-test');
+
+ input.setMode('text');
+ assert.equal(model.textEditorModel.getModeId(), PLAINTEXT_MODE_ID);
});
});
\ No newline at end of file
diff --git a/src/vs/workbench/test/common/editor/untitledEditor.test.ts b/src/vs/workbench/test/common/editor/untitledEditor.test.ts
index 3dc06265b44bb..001ad7a6a2bc3 100644
--- a/src/vs/workbench/test/common/editor/untitledEditor.test.ts
+++ b/src/vs/workbench/test/common/editor/untitledEditor.test.ts
@@ -16,6 +16,7 @@ import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { timeout } from 'vs/base/common/async';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
+import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
export class TestUntitledEditorService extends UntitledEditorService {
get(resource: URI) { return super.get(resource); }
@@ -45,7 +46,7 @@ suite('Workbench untitled editors', () => {
accessor.untitledEditorService.dispose();
});
- test('Untitled Editor Service', function (done) {
+ test('Untitled Editor Service', async (done) => {
const service = accessor.untitledEditorService;
assert.equal(service.getAll().length, 0);
@@ -68,36 +69,35 @@ suite('Workbench untitled editors', () => {
assert.equal(service.getAll().length, 1);
// dirty
- input2.resolve().then(model => {
- assert.ok(!service.isDirty(input2.getResource()));
+ const model = await input2.resolve();
- const listener = service.onDidChangeDirty(resource => {
- listener.dispose();
+ assert.ok(!service.isDirty(input2.getResource()));
- assert.equal(resource.toString(), input2.getResource().toString());
+ const listener = service.onDidChangeDirty(resource => {
+ listener.dispose();
- assert.ok(service.isDirty(input2.getResource()));
- assert.equal(service.getDirty()[0].toString(), input2.getResource().toString());
- assert.equal(service.getDirty([input2.getResource()])[0].toString(), input2.getResource().toString());
- assert.equal(service.getDirty([input1.getResource()]).length, 0);
+ assert.equal(resource.toString(), input2.getResource().toString());
- service.revertAll();
- assert.equal(service.getAll().length, 0);
- assert.ok(!input2.isDirty());
- assert.ok(!model.isDirty());
+ assert.ok(service.isDirty(input2.getResource()));
+ assert.equal(service.getDirty()[0].toString(), input2.getResource().toString());
+ assert.equal(service.getDirty([input2.getResource()])[0].toString(), input2.getResource().toString());
+ assert.equal(service.getDirty([input1.getResource()]).length, 0);
- input2.dispose();
+ service.revertAll();
+ assert.equal(service.getAll().length, 0);
+ assert.ok(!input2.isDirty());
+ assert.ok(!model.isDirty());
- assert.ok(!service.exists(input2.getResource()));
+ input2.dispose();
- done();
- });
+ assert.ok(!service.exists(input2.getResource()));
+ done();
+ });
- model.textEditorModel.setValue('foo bar');
- }, err => done(err));
+ model.textEditorModel.setValue('foo bar');
});
- test('Untitled with associated resource', function () {
+ test('Untitled with associated resource', () => {
const service = accessor.untitledEditorService;
const file = URI.file(join('C:\\', '/foo/file.txt'));
const untitled = service.createOrGet(file);
@@ -107,53 +107,49 @@ suite('Workbench untitled editors', () => {
untitled.dispose();
});
- test('Untitled no longer dirty when content gets empty', function () {
+ test('Untitled no longer dirty when content gets empty', async () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
// dirty
- return input.resolve().then(model => {
- model.textEditorModel.setValue('foo bar');
- assert.ok(model.isDirty());
-
- model.textEditorModel.setValue('');
- assert.ok(!model.isDirty());
-
- input.dispose();
- });
+ const model = await input.resolve();
+ model.textEditorModel.setValue('foo bar');
+ assert.ok(model.isDirty());
+ model.textEditorModel.setValue('');
+ assert.ok(!model.isDirty());
+ input.dispose();
});
- test('Untitled via loadOrCreate', function () {
+ test('Untitled via loadOrCreate', async () => {
const service = accessor.untitledEditorService;
- service.loadOrCreate().then(model1 => {
- model1.textEditorModel!.setValue('foo bar');
- assert.ok(model1.isDirty());
-
- model1.textEditorModel!.setValue('');
- assert.ok(!model1.isDirty());
-
- return service.loadOrCreate({ initialValue: 'Hello World' }).then(model2 => {
- assert.equal(snapshotToString(model2.createSnapshot()!), 'Hello World');
-
- const input = service.createOrGet();
-
- return service.loadOrCreate({ resource: input.getResource() }).then(model3 => {
- assert.equal(model3.getResource().toString(), input.getResource().toString());
-
- const file = URI.file(join('C:\\', '/foo/file44.txt'));
- return service.loadOrCreate({ resource: file }).then(model4 => {
- assert.ok(service.hasAssociatedFilePath(model4.getResource()));
- assert.ok(model4.isDirty());
-
- model1.dispose();
- model2.dispose();
- model3.dispose();
- model4.dispose();
- input.dispose();
- });
- });
- });
- });
+
+ const model1 = await service.loadOrCreate();
+
+ model1.textEditorModel!.setValue('foo bar');
+ assert.ok(model1.isDirty());
+
+ model1.textEditorModel!.setValue('');
+ assert.ok(!model1.isDirty());
+
+ const model2 = await service.loadOrCreate({ initialValue: 'Hello World' });
+ assert.equal(snapshotToString(model2.createSnapshot()!), 'Hello World');
+
+ const input = service.createOrGet();
+
+ const model3 = await service.loadOrCreate({ resource: input.getResource() });
+
+ assert.equal(model3.getResource().toString(), input.getResource().toString());
+
+ const file = URI.file(join('C:\\', '/foo/file44.txt'));
+ const model4 = await service.loadOrCreate({ resource: file });
+ assert.ok(service.hasAssociatedFilePath(model4.getResource()));
+ assert.ok(model4.isDirty());
+
+ model1.dispose();
+ model2.dispose();
+ model3.dispose();
+ model4.dispose();
+ input.dispose();
});
test('Untitled suggest name', function () {
@@ -163,24 +159,31 @@ suite('Workbench untitled editors', () => {
assert.ok(service.suggestFileName(input.getResource()));
});
- test('Untitled with associated path remains dirty when content gets empty', function () {
+ test('Untitled with associated path remains dirty when content gets empty', async () => {
const service = accessor.untitledEditorService;
const file = URI.file(join('C:\\', '/foo/file.txt'));
const input = service.createOrGet(file);
// dirty
- return input.resolve().then(model => {
- model.textEditorModel.setValue('foo bar');
- assert.ok(model.isDirty());
+ const model = await input.resolve();
+ model.textEditorModel.setValue('foo bar');
+ assert.ok(model.isDirty());
+ model.textEditorModel.setValue('');
+ assert.ok(model.isDirty());
+ input.dispose();
+ });
- model.textEditorModel.setValue('');
- assert.ok(model.isDirty());
+ test('Untitled with initial content is dirty', async () => {
+ const service = accessor.untitledEditorService;
+ const input = service.createOrGet(undefined, undefined, 'Hello World');
- input.dispose();
- });
+ // dirty
+ const model = await input.resolve();
+ assert.ok(model.isDirty());
+ input.dispose();
});
- test('Untitled created with files.defaultLanguage setting', function () {
+ test('Untitled created with files.defaultLanguage setting', () => {
const defaultLanguage = 'javascript';
const config = accessor.testConfigurationService;
config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage });
@@ -188,30 +191,52 @@ suite('Workbench untitled editors', () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
- assert.equal(input.getModeId(), defaultLanguage);
+ assert.equal(input.getMode(), defaultLanguage);
config.setUserConfiguration('files', { 'defaultLanguage': undefined });
input.dispose();
});
- test('Untitled created with modeId overrides files.defaultLanguage setting', function () {
- const modeId = 'typescript';
+ test('Untitled created with mode overrides files.defaultLanguage setting', () => {
+ const mode = 'typescript';
const defaultLanguage = 'javascript';
const config = accessor.testConfigurationService;
config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage });
const service = accessor.untitledEditorService;
- const input = service.createOrGet(null!, modeId);
+ const input = service.createOrGet(null!, mode);
- assert.equal(input.getModeId(), modeId);
+ assert.equal(input.getMode(), mode);
config.setUserConfiguration('files', { 'defaultLanguage': undefined });
input.dispose();
});
- test('encoding change event', function () {
+ test('Untitled can change mode afterwards', async () => {
+ const mode = 'untitled-input-test';
+
+ ModesRegistry.registerLanguage({
+ id: mode,
+ });
+
+ const service = accessor.untitledEditorService;
+ const input = service.createOrGet(null!, mode);
+
+ assert.equal(input.getMode(), mode);
+
+ const model = await input.resolve();
+ assert.equal(model.getMode(), mode);
+
+ input.setMode('text');
+
+ assert.equal(input.getMode(), PLAINTEXT_MODE_ID);
+
+ input.dispose();
+ });
+
+ test('encoding change event', async () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
@@ -223,16 +248,13 @@ suite('Workbench untitled editors', () => {
});
// dirty
- return input.resolve().then(model => {
- model.setEncoding('utf16');
-
- assert.equal(counter, 1);
-
- input.dispose();
- });
+ const model = await input.resolve();
+ model.setEncoding('utf16');
+ assert.equal(counter, 1);
+ input.dispose();
});
- test('onDidChangeContent event', () => {
+ test('onDidChangeContent event', async () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
@@ -245,39 +267,32 @@ suite('Workbench untitled editors', () => {
assert.equal(r.toString(), input.getResource().toString());
});
- return input.resolve().then(model => {
- model.textEditorModel.setValue('foo');
- assert.equal(counter, 0, 'Dirty model should not trigger event immediately');
+ const model = await input.resolve();
+ model.textEditorModel.setValue('foo');
+ assert.equal(counter, 0, 'Dirty model should not trigger event immediately');
- return timeout(3).then(() => {
- assert.equal(counter, 1, 'Dirty model should trigger event');
+ await timeout(3);
+ assert.equal(counter, 1, 'Dirty model should trigger event');
+ model.textEditorModel.setValue('bar');
- model.textEditorModel.setValue('bar');
- return timeout(3).then(() => {
- assert.equal(counter, 2, 'Content change when dirty should trigger event');
+ await timeout(3);
+ assert.equal(counter, 2, 'Content change when dirty should trigger event');
+ model.textEditorModel.setValue('');
- model.textEditorModel.setValue('');
- return timeout(3).then(() => {
- assert.equal(counter, 3, 'Manual revert should trigger event');
+ await timeout(3);
+ assert.equal(counter, 3, 'Manual revert should trigger event');
+ model.textEditorModel.setValue('foo');
- model.textEditorModel.setValue('foo');
- return timeout(3).then(() => {
- assert.equal(counter, 4, 'Dirty model should trigger event');
+ await timeout(3);
+ assert.equal(counter, 4, 'Dirty model should trigger event');
+ model.revert();
- model.revert();
- return timeout(3).then(() => {
- assert.equal(counter, 5, 'Revert should trigger event');
-
- input.dispose();
- });
- });
- });
- });
- });
- });
+ await timeout(3);
+ assert.equal(counter, 5, 'Revert should trigger event');
+ input.dispose();
});
- test('onDidDisposeModel event', () => {
+ test('onDidDisposeModel event', async () => {
const service = accessor.untitledEditorService;
const input = service.createOrGet();
@@ -288,10 +303,9 @@ suite('Workbench untitled editors', () => {
assert.equal(r.toString(), input.getResource().toString());
});
- return input.resolve().then(model => {
- assert.equal(counter, 0);
- input.dispose();
- assert.equal(counter, 1);
- });
+ await input.resolve();
+ assert.equal(counter, 0);
+ input.dispose();
+ assert.equal(counter, 1);
});
});
\ No newline at end of file
diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts
index 54cced7af7024..502d3fae8e908 100644
--- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts
+++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts
@@ -37,7 +37,7 @@ suite('MainThreadSaveParticipant', function () {
});
test('insert final new line', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/final_new_line.txt'), 'utf8') as IResolvedTextFileEditorModel;
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel;
await model.load();
const configService = new TestConfigurationService();
@@ -70,7 +70,7 @@ suite('MainThreadSaveParticipant', function () {
});
test('trim final new lines', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8') as IResolvedTextFileEditorModel;
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel;
await model.load();
const configService = new TestConfigurationService();
@@ -105,7 +105,7 @@ suite('MainThreadSaveParticipant', function () {
});
test('trim final new lines bug#39750', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8') as IResolvedTextFileEditorModel;
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel;
await model.load();
const configService = new TestConfigurationService();
@@ -132,7 +132,7 @@ suite('MainThreadSaveParticipant', function () {
});
test('trim final new lines bug#46075', async function () {
- const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8') as IResolvedTextFileEditorModel;
+ const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel;
await model.load();
const configService = new TestConfigurationService();
diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts
index 1e00736484cfd..b917b7ac10af0 100644
--- a/src/vs/workbench/test/workbenchTestServices.ts
+++ b/src/vs/workbench/test/workbenchTestServices.ts
@@ -15,7 +15,7 @@ import { ConfirmResult, IEditorInputWithOptions, CloseDirection, IEditorIdentifi
import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
-import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
+import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
@@ -85,7 +85,7 @@ import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer';
import { BrowserTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
- return instantiationService.createInstance(FileEditorInput, resource, undefined);
+ return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined);
}
export const TestEnvironmentService = new WorkbenchEnvironmentService(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
@@ -1093,7 +1093,7 @@ export class TestBackupFileService implements IBackupFileService {
throw new Error('not implemented');
}
- public backupResource(_resource: URI, _content: ITextSnapshot): Promise {
+ public backupResource(_resource: URI, _content: ITextSnapshot, versionId?: number, meta?: T): Promise {
return Promise.resolve();
}
@@ -1108,7 +1108,7 @@ export class TestBackupFileService implements IBackupFileService {
return textBuffer.getValueInRange(range, EndOfLinePreference.TextDefined);
}
- public resolveBackupContent(_backup: URI): Promise {
+ public resolveBackupContent(_backup: URI): Promise> {
throw new Error('not implemented');
}