diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index b6697d55a47..ba914be9bf5 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -369,15 +369,15 @@ export function always(promise: TPromise, f: Function): TPromise { * Runs the provided list of promise factories in sequential order. The returned * promise will complete to an array of results from each promise. */ -export function sequence(promiseFactory: ITask>[]): TPromise { +export function sequence(promiseFactories: ITask>[]): TPromise { const results: T[] = []; // reverse since we start with last element using pop() - promiseFactory = promiseFactory.reverse(); + promiseFactories = promiseFactories.reverse(); function next(): Promise { - if (promiseFactory.length) { - return promiseFactory.pop()(); + if (promiseFactories.length) { + return promiseFactories.pop()(); } return null; @@ -399,6 +399,29 @@ export function sequence(promiseFactory: ITask>[]): TPromise return TPromise.as(null).then(thenHandler); } +export function first(promiseFactories: ITask>[], shouldStop: (t: T) => boolean = t => !!t): TPromise { + promiseFactories = [...promiseFactories.reverse()]; + + const loop = () => { + if (promiseFactories.length === 0) { + return TPromise.as(null); + } + + const factory = promiseFactories.pop(); + const promise = factory(); + + return promise.then(result => { + if (shouldStop(result)) { + return TPromise.as(result); + } + + return loop(); + }); + }; + + return loop(); +} + export function once(fn: T): T { const _this = this; let didCall = false; diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index ae0f900fb1c..7f24172f50c 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -67,3 +67,42 @@ export class Disposables extends Disposable { } } } + +export interface IReference extends IDisposable { + readonly object: T; +} + +export abstract class ReferenceCollection { + + private references: { [key: string]: { readonly object: T; counter: number; } } = Object.create(null); + + constructor() { } + + acquire(key: string): IReference { + let reference = this.references[key]; + + if (!reference) { + reference = this.references[key] = { counter: 0, object: this.createReferencedObject(key) }; + } + + const { object } = reference; + const dispose = () => { + if (--reference.counter === 0) { + this.destroyReferencedObject(reference.object); + delete this.references[key]; + } + }; + + reference.counter++; + + return { object, dispose }; + } + + protected abstract createReferencedObject(key: string): T; + protected abstract destroyReferencedObject(object: T): void; +} + +export class ImmortalReference implements IReference { + constructor(public object: T) { } + dispose(): void { /* noop */ } +} \ No newline at end of file diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 6a815e02b54..95e368eaa26 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle'; class Disposable implements IDisposable { isDisposed = false; @@ -49,4 +49,42 @@ suite('Lifecycle', () => { assert(disposable.isDisposed); assert(disposable2.isDisposed); }); +}); + +suite('Reference Collection', () => { + class Collection extends ReferenceCollection { + private _count = 0; + get count() { return this._count; } + protected createReferencedObject(key: string): number { this._count++; return key.length; } + protected destroyReferencedObject(object: number): void { this._count--; } + } + + test('simple', () => { + const collection = new Collection(); + + const ref1 = collection.acquire('test'); + assert(ref1); + assert.equal(ref1.object, 4); + assert.equal(collection.count, 1); + ref1.dispose(); + assert.equal(collection.count, 0); + + const ref2 = collection.acquire('test'); + const ref3 = collection.acquire('test'); + assert.equal(ref2.object, ref3.object); + assert.equal(collection.count, 1); + + const ref4 = collection.acquire('monkey'); + assert.equal(ref4.object, 6); + assert.equal(collection.count, 2); + + ref2.dispose(); + assert.equal(collection.count, 2); + + ref3.dispose(); + assert.equal(collection.count, 1); + + ref4.dispose(); + assert.equal(collection.count, 0); + }); }); \ No newline at end of file diff --git a/src/vs/editor/browser/standalone/simpleServices.ts b/src/vs/editor/browser/standalone/simpleServices.ts index 601388feff8..710c1145cb4 100644 --- a/src/vs/editor/browser/standalone/simpleServices.ts +++ b/src/vs/editor/browser/standalone/simpleServices.ts @@ -9,7 +9,7 @@ import Severity from 'vs/base/common/severity'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IConfigurationService, IConfigurationServiceEvent, IConfigurationValue, getConfigurationValue, IConfigurationKeys } from 'vs/platform/configuration/common/configuration'; -import { IEditor, IEditorInput, IEditorOptions, IEditorService, IResourceInput, ITextEditorModel, Position } from 'vs/platform/editor/common/editor'; +import { IEditor, IEditorInput, IEditorOptions, IEditorService, IResourceInput, Position } from 'vs/platform/editor/common/editor'; import { AbstractExtensionService, ActivatedExtension } from 'vs/platform/extensions/common/abstractExtensionService'; import { IExtensionDescription, IExtensionService } from 'vs/platform/extensions/common/extensions'; import { ICommandService, ICommand, ICommandHandler } from 'vs/platform/commands/common/commands'; @@ -26,8 +26,8 @@ import { getDefaultValues as getDefaultConfiguration } from 'vs/platform/configu import { CommandService } from 'vs/platform/commands/common/commandService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; -import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITextModelResolverService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IDisposable, IReference, ImmortalReference } from 'vs/base/common/lifecycle'; export class SimpleEditor implements IEditor { @@ -173,7 +173,7 @@ export class SimpleEditorModelResolverService implements ITextModelResolverServi this.editor = new SimpleEditor(editor); } - public resolve(resource: URI): TPromise { + public createModelReference(resource: URI): TPromise> { let model: editorCommon.IModel; model = this.editor.withTypedEditor( @@ -182,10 +182,10 @@ export class SimpleEditorModelResolverService implements ITextModelResolverServi ); if (!model) { - return TPromise.as(null); + return TPromise.as(new ImmortalReference(null)); } - return TPromise.as(new SimpleModel(model)); + return TPromise.as(new ImmortalReference(new SimpleModel(model))); } public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { diff --git a/src/vs/editor/common/services/bulkEdit.ts b/src/vs/editor/common/services/bulkEdit.ts index 0143ef864ab..84a65ce432f 100644 --- a/src/vs/editor/common/services/bulkEdit.ts +++ b/src/vs/editor/common/services/bulkEdit.ts @@ -7,9 +7,10 @@ import * as nls from 'vs/nls'; import { merge } from 'vs/base/common/arrays'; import { IStringDictionary, forEach, values } from 'vs/base/common/collections'; +import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; +import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IEventService } from 'vs/platform/event/common/event'; import { EventType as FileEventType, FileChangesEvent, IFileChange } from 'vs/platform/files/common/files'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -64,16 +65,17 @@ class ChangeRecorder { } } -class EditTask { +class EditTask implements IDisposable { private _initialSelections: Selection[]; private _endCursorSelection: Selection; - private _model: IModel; + private get _model(): IModel { return this._modelReference.object.textEditorModel; } + private _modelReference: IReference; private _edits: IIdentifiedSingleEditOperation[]; - constructor(model: IModel) { + constructor(modelReference: IReference) { this._endCursorSelection = null; - this._model = model; + this._modelReference = modelReference; this._edits = []; } @@ -138,14 +140,21 @@ class EditTask { private static _editCompare(a: IIdentifiedSingleEditOperation, b: IIdentifiedSingleEditOperation): number { return Range.compareRangesUsingStarts(a.range, b.range); } + + dispose() { + if (this._model) { + this._modelReference.dispose(); + this._modelReference = null; + } + } } class SourceModelEditTask extends EditTask { private _knownInitialSelections: Selection[]; - constructor(model: IModel, initialSelections: Selection[]) { - super(model); + constructor(modelReference: IReference, initialSelections: Selection[]) { + super(modelReference); this._knownInitialSelections = initialSelections; } @@ -154,7 +163,7 @@ class SourceModelEditTask extends EditTask { } } -class BulkEditModel { +class BulkEditModel implements IDisposable { private _textModelResolverService: ITextModelResolverService; private _numberOfResourcesToModify: number = 0; @@ -208,7 +217,9 @@ class BulkEditModel { } forEach(this._edits, entry => { - const promise = this._textModelResolverService.resolve(URI.parse(entry.key)).then(model => { + const promise = this._textModelResolverService.createModelReference(URI.parse(entry.key)).then(ref => { + const model = ref.object; + if (!model || !model.textEditorModel) { throw new Error(`Cannot load file ${entry.key}`); } @@ -217,10 +228,10 @@ class BulkEditModel { let task: EditTask; if (this._sourceModel && textEditorModel.uri.toString() === this._sourceModel.toString()) { - this._sourceModelTask = new SourceModelEditTask(textEditorModel, this._sourceSelections); + this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections); task = this._sourceModelTask; } else { - task = new EditTask(textEditorModel); + task = new EditTask(ref); } entry.value.forEach(edit => task.addEdit(edit)); @@ -251,6 +262,10 @@ class BulkEditModel { this.progress.worked(1); } } + + dispose(): void { + this._tasks = dispose(this._tasks); + } } export interface BulkEdit { @@ -314,7 +329,7 @@ export function createBulkEdit(eventService: IEventService, textModelResolverSer selections = editor.getSelections(); } - let model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner); + const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner); return model.prepare().then(_ => { @@ -324,7 +339,10 @@ export function createBulkEdit(eventService: IEventService, textModelResolverSer } recording.stop(); - return model.apply(); + + const result = model.apply(); + model.dispose(); + return result; }); } diff --git a/src/vs/editor/common/services/resolverService.ts b/src/vs/editor/common/services/resolverService.ts index e3116160fc5..aacd94b9622 100644 --- a/src/vs/editor/common/services/resolverService.ts +++ b/src/vs/editor/common/services/resolverService.ts @@ -8,8 +8,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IModel } from 'vs/editor/common/editorCommon'; -import { ITextEditorModel as IBaseTextEditorModel } from 'vs/platform/editor/common/editor'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { IDisposable, IReference } from 'vs/base/common/lifecycle'; export const ITextModelResolverService = createDecorator('textModelResolverService'); @@ -17,14 +17,13 @@ export interface ITextModelResolverService { _serviceBrand: any; /** - * Given a resource, tries to resolve a ITextEditorModel out of it. Will support many schemes like file://, untitled://, - * inMemory:// and for anything else fall back to the model content provider registry. + * Provided a resource URI, it will return a model reference + * which should be disposed once not needed anymore. */ - resolve(resource: URI): TPromise; + createModelReference(resource: URI): TPromise>; /** - * For unknown resources, allows to register a content provider such as this service is able to resolve arbritrary - * resources to ITextEditorModels. + * Registers a specific `scheme` content provider. */ registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable; } @@ -37,7 +36,7 @@ export interface ITextModelContentProvider { provideTextContent(resource: URI): TPromise; } -export interface ITextEditorModel extends IBaseTextEditorModel { +export interface ITextEditorModel extends IEditorModel { /** * Provides access to the underlying IModel. diff --git a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts index 3df5bc33b45..7fbd1dbe432 100644 --- a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts +++ b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts @@ -112,6 +112,8 @@ export class DefinitionAction extends EditorAction { this._openReference(editorService, next, this._configuration.openToSide).then(editor => { if (model.references.length > 1) { this._openInPeek(editorService, editor, model); + } else { + model.dispose(); } }); } @@ -142,6 +144,8 @@ export class DefinitionAction extends EditorAction { return this._openReference(editorService, reference, false); } }); + } else { + model.dispose(); } } } @@ -319,7 +323,9 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC // Single result else { let result = results[0]; - this.textModelResolverService.resolve(result.uri).then(model => { + + this.textModelResolverService.createModelReference(result.uri).then(ref => { + const model = ref.object; let hoverMessage: MarkedString; if (model && model.textEditorModel) { const editorModel = model.textEditorModel; @@ -369,6 +375,8 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC }; } + ref.dispose(); + this.addDecoration({ startLineNumber: position.lineNumber, startColumn: word.startColumn, diff --git a/src/vs/editor/contrib/quickFix/browser/quickFix.ts b/src/vs/editor/contrib/quickFix/browser/quickFix.ts index 124e1fa30fa..e6ba08288a4 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFix.ts @@ -97,7 +97,7 @@ export class QuickFixController implements IEditorContribution { if (kb) { title = nls.localize('quickFixWithKb', "Show Fixes ({0})", this._keybindingService.getLabelFor(kb)); } else { - title = nls.localize('quickFix', "Show Fixes", this._keybindingService.getLabelFor(kb)); + title = nls.localize('quickFix', "Show Fixes"); } this._lightBulbWidget.getDomNode().title = title; } diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesController.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesController.ts index 50e9720009c..a779cc24b7d 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesController.ts @@ -147,6 +147,11 @@ export class ReferencesController implements editorCommon.IEditorContribution { if (requestId !== this._requestIdPool || !this._widget) { return; } + + if (this._model) { + this._model.dispose(); + } + this._model = model; // measure time it stays open @@ -194,7 +199,10 @@ export class ReferencesController implements editorCommon.IEditorContribution { } this._referenceSearchVisible.reset(); this._disposables = dispose(this._disposables); - this._model = null; + if (this._model) { + this._model.dispose(); + this._model = null; + } this._editor.focus(); this._requestIdPool += 1; // Cancel pending requests } diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesModel.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesModel.ts index 9d5e770208c..e9b7289215b 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesModel.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesModel.ts @@ -7,14 +7,15 @@ import { EventEmitter } from 'vs/base/common/eventEmitter'; import Event, { fromEventEmitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/paths'; +import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; -import { IModel, IPosition, IRange } from 'vs/editor/common/editorCommon'; +import { IPosition, IRange } from 'vs/editor/common/editorCommon'; import { Location } from 'vs/editor/common/modes'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; +import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; export class OneReference { @@ -62,30 +63,39 @@ export class OneReference { } } -export class FilePreview { +export class FilePreview implements IDisposable { - constructor(private _value: IModel) { + constructor(private _modelReference: IReference) { } + private get _model() { return this._modelReference.object.textEditorModel; } + public preview(range: IRange, n: number = 8): { before: string; inside: string; after: string } { const {startLineNumber, startColumn, endColumn} = range; - const word = this._value.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n }); + const word = this._model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n }); const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn); const afterRange = new Range(startLineNumber, endColumn, startLineNumber, Number.MAX_VALUE); const ret = { - before: this._value.getValueInRange(beforeRange).replace(/^\s+/, strings.empty), - inside: this._value.getValueInRange(range), - after: this._value.getValueInRange(afterRange).replace(/\s+$/, strings.empty) + before: this._model.getValueInRange(beforeRange).replace(/^\s+/, strings.empty), + inside: this._model.getValueInRange(range), + after: this._model.getValueInRange(afterRange).replace(/\s+$/, strings.empty) }; return ret; } + + dispose(): void { + if (this._modelReference) { + this._modelReference.dispose(); + this._modelReference = null; + } + } } -export class FileReferences { +export class FileReferences implements IDisposable { private _children: OneReference[]; private _preview: FilePreview; @@ -134,11 +144,15 @@ export class FileReferences { return TPromise.as(this); } - return textModelResolverService.resolve(this._uri).then(model => { + return textModelResolverService.createModelReference(this._uri).then(modelReference => { + const model = modelReference.object; + if (!model) { + modelReference.dispose(); throw new Error(); } - this._preview = new FilePreview(model.textEditorModel); + + this._preview = new FilePreview(modelReference); this._resolved = true; return this; @@ -150,9 +164,16 @@ export class FileReferences { return this; }); } + + dispose(): void { + if (this._preview) { + this._preview.dispose(); + this._preview = null; + } + } } -export class ReferencesModel { +export class ReferencesModel implements IDisposable { private _groups: FileReferences[] = []; private _references: OneReference[] = []; @@ -239,6 +260,10 @@ export class ReferencesModel { } } + dispose(): void { + this._groups = dispose(this._groups); + } + private static _compareReferences(a: Location, b: Location): number { if (a.uri.toString() < b.uri.toString()) { return -1; diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts index da509cfb6c3..9d0d3e7e8f8 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts @@ -10,7 +10,7 @@ import * as collections from 'vs/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; import { getPathLabel } from 'vs/base/common/labels'; import Event, { Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, Disposables } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposables, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -494,6 +494,7 @@ export class ReferenceWidget extends PeekViewWidget { private _treeContainer: Builder; private _sash: VSash; private _preview: ICodeEditor; + private _previewModelReference: IDisposable = EmptyDisposable; private _previewNotAvailableMessage: Model; private _previewContainer: Builder; private _messageContainer: Builder; @@ -715,24 +716,31 @@ export class ReferenceWidget extends PeekViewWidget { this.setTitle(nls.localize('peekView.alternateTitle', "References")); } - return TPromise.join([ - this._textModelResolverService.resolve(reference.uri), - this._tree.reveal(reference) - ]).then(values => { + const promise = this._textModelResolverService.createModelReference(reference.uri); + + return TPromise.join([promise, this._tree.reveal(reference)]).then(values => { + const ref = values[0]; + if (!this._model) { + ref.dispose(); // disposed return; } + this._previewModelReference.dispose(); + this._previewModelReference = EmptyDisposable; + // show in editor - let [model] = values; + const model = ref.object; if (model) { + this._previewModelReference = ref; this._preview.setModel(model.textEditorModel); var sel = Range.lift(reference.range).collapseToStart(); this._preview.setSelection(sel); this._preview.revealRangeInCenter(sel); } else { this._preview.setModel(this._previewNotAvailableMessage); + ref.dispose(); } // show in tree diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index fd2de5827bf..9807700305f 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -37,10 +37,6 @@ export interface IEditorModel { dispose(): void; } -export interface ITextEditorModel extends IEditorModel { - textEditorModel: any; -} - export interface IResourceInput { /** diff --git a/src/vs/workbench/api/node/mainThreadDocuments.ts b/src/vs/workbench/api/node/mainThreadDocuments.ts index 8a575e652df..fd123e00b57 100644 --- a/src/vs/workbench/api/node/mainThreadDocuments.ts +++ b/src/vs/workbench/api/node/mainThreadDocuments.ts @@ -187,8 +187,11 @@ export class MainThreadDocuments extends MainThreadDocumentsShape { } private _handleAsResourceInput(uri: URI): TPromise { - return this._textModelResolverService.resolve(uri).then(model => { - return !!model; + return this._textModelResolverService.createModelReference(uri).then(ref => { + const result = !!ref.object; + ref.dispose(); + + return result; }); } diff --git a/src/vs/workbench/browser/parts/editor/stringEditor.ts b/src/vs/workbench/browser/parts/editor/stringEditor.ts index 1654fc95136..dbe016509b4 100644 --- a/src/vs/workbench/browser/parts/editor/stringEditor.ts +++ b/src/vs/workbench/browser/parts/editor/stringEditor.ts @@ -107,7 +107,7 @@ export class StringEditor extends BaseTextEditor { // Set Editor Model const textEditor = this.getControl(); - const textEditorModel = (resolvedModel).textEditorModel; + const textEditorModel = resolvedModel.textEditorModel; textEditor.setModel(textEditorModel); // Apply Options from TextOptions diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 5569b6c888a..e12b560ef53 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -12,8 +12,8 @@ import { EditorModel } from 'vs/workbench/common/editor'; * and the modified version. */ export class DiffEditorModel extends EditorModel { - private _originalModel: EditorModel; - private _modifiedModel: EditorModel; + protected _originalModel: EditorModel; + protected _modifiedModel: EditorModel; constructor(originalModel: EditorModel, modifiedModel: EditorModel) { super(); diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 5464bb982f9..6aa853fed0a 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -7,6 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { EditorInput, ITextEditorModel } from 'vs/workbench/common/editor'; import URI from 'vs/base/common/uri'; +import { IReference } from 'vs/base/common/lifecycle'; import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetry'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; @@ -17,9 +18,9 @@ import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorMo */ export class ResourceEditorInput extends EditorInput { - public static ID: string = 'workbench.editors.resourceEditorInput'; + static ID: string = 'workbench.editors.resourceEditorInput'; - protected cachedModel: ResourceEditorModel; + protected promise: TPromise>; protected resource: URI; private name: string; @@ -38,64 +39,60 @@ export class ResourceEditorInput extends EditorInput { this.resource = resource; } - public getTypeId(): string { + getTypeId(): string { return ResourceEditorInput.ID; } - public getName(): string { + getName(): string { return this.name; } - public setName(name: string): void { + setName(name: string): void { if (this.name !== name) { this.name = name; this._onDidChangeLabel.fire(); } } - public getDescription(): string { + getDescription(): string { return this.description; } - public setDescription(description: string): void { + setDescription(description: string): void { if (this.description !== description) { this.description = description; this._onDidChangeLabel.fire(); } } - public getTelemetryDescriptor(): { [key: string]: any; } { + getTelemetryDescriptor(): { [key: string]: any; } { const descriptor = super.getTelemetryDescriptor(); descriptor['resource'] = telemetryURIDescriptor(this.resource); return descriptor; } - public resolve(refresh?: boolean): TPromise { - - // Use Cached Model - if (this.cachedModel) { - return TPromise.as(this.cachedModel); + resolve(refresh?: boolean): TPromise { + if (!this.promise) { + this.promise = this.textModelResolverService.createModelReference(this.resource); } - // Otherwise Create Model and handle dispose event - return this.textModelResolverService.resolve(this.resource).then(model => { + return this.promise.then(ref => { + const model = ref.object; + if (!(model instanceof ResourceEditorModel)) { + ref.dispose(); + this.promise = null; return TPromise.wrapError(`Unexpected model for ResourceInput: ${this.resource}`); // TODO@Ben eventually also files should be supported, but we guard due to the dangerous dispose of the model in dispose() } - this.cachedModel = model; + // TODO@Joao this should never happen + model.onDispose(() => this.dispose()); - const unbind = model.onDispose(() => { - this.cachedModel = null; // make sure we do not dispose model again - unbind.dispose(); - this.dispose(); - }); - - return this.cachedModel; + return model; }); } - public matches(otherInput: any): boolean { + matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } @@ -110,10 +107,10 @@ export class ResourceEditorInput extends EditorInput { return false; } - public dispose(): void { - if (this.cachedModel) { - this.cachedModel.dispose(); - this.cachedModel = null; + dispose(): void { + if (this.promise) { + this.promise.done(ref => ref.dispose()); + this.promise = null; } super.dispose(); diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index 10738e61a61..dc199593c22 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -23,6 +23,14 @@ export class TextDiffEditorModel extends DiffEditorModel { this.updateTextDiffEditorModel(); } + get originalModel(): BaseTextEditorModel { + return this._originalModel as BaseTextEditorModel; + } + + get modifiedModel(): BaseTextEditorModel { + return this._modifiedModel as BaseTextEditorModel; + } + public load(): TPromise { return super.load().then(() => { this.updateTextDiffEditorModel(); @@ -37,15 +45,15 @@ export class TextDiffEditorModel extends DiffEditorModel { // Create new if (!this._textDiffEditorModel) { this._textDiffEditorModel = { - original: (this.originalModel).textEditorModel, - modified: (this.modifiedModel).textEditorModel + original: this.originalModel.textEditorModel, + modified: this.modifiedModel.textEditorModel }; } // Update existing else { - this._textDiffEditorModel.original = (this.originalModel).textEditorModel; - this._textDiffEditorModel.modified = (this.modifiedModel).textEditorModel; + this._textDiffEditorModel.original = this.originalModel.textEditorModel; + this._textDiffEditorModel.modified = this.modifiedModel.textEditorModel; } } } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index cdc13d587d9..7327ba2fefd 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -9,7 +9,7 @@ import { EndOfLinePreference, IModel, IRawText } from 'vs/editor/common/editorCo import { IMode } from 'vs/editor/common/modes'; import { EditorModel } from 'vs/workbench/common/editor'; import URI from 'vs/base/common/uri'; -import { ITextEditorModel } from 'vs/platform/editor/common/editor'; +import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { RawText } from 'vs/editor/common/model/textModel'; diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index bf1d9eb870c..3c243d2c02c 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -213,6 +213,8 @@ export abstract class BaseZoomAction extends Action { target = ConfigurationTarget.WORKSPACE; } + level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels + const applyZoom = () => { webFrame.setZoomLevel(level); browser.setZoomFactor(webFrame.getZoomFactor()); diff --git a/src/vs/workbench/electron-browser/extensionHost.ts b/src/vs/workbench/electron-browser/extensionHost.ts index d40c4e369b2..73eb60ec53a 100644 --- a/src/vs/workbench/electron-browser/extensionHost.ts +++ b/src/vs/workbench/electron-browser/extensionHost.ts @@ -123,7 +123,7 @@ export class ExtensionHostProcessWorker { this.extHostWatchDog.start(); this.extHostWatchDog.onAlert(() => { - this.extHostWatchDog.stop(); + this.extHostWatchDog.reset(); // log the identifiers of those extensions that // have code and are loaded in the extension host @@ -134,7 +134,7 @@ export class ExtensionHostProcessWorker { ids.push(ext.id); } } - this.telemetryService.publicLog('extHostUnresponsive', { extensionIds: ids }); + this.telemetryService.publicLog('extHostUnresponsive2', { extensionIds: ids }); }); }); }); diff --git a/src/vs/workbench/electron-browser/workbench.main.ts b/src/vs/workbench/electron-browser/workbench.main.ts index 8c714f75c97..73a43894ae5 100644 --- a/src/vs/workbench/electron-browser/workbench.main.ts +++ b/src/vs/workbench/electron-browser/workbench.main.ts @@ -43,6 +43,8 @@ import 'vs/workbench/parts/search/browser/search.contribution'; import 'vs/workbench/parts/search/browser/searchViewlet'; // can be packaged separately import 'vs/workbench/parts/search/browser/openAnythingHandler'; // can be packaged separately +import 'vs/workbench/parts/scm/browser/scm.contribution'; + import 'vs/workbench/parts/git/electron-browser/git.contribution'; import 'vs/workbench/parts/git/browser/gitQuickOpen'; import 'vs/workbench/parts/git/browser/gitActions.contribution'; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 9af4802801e..5fac76c8111 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -75,6 +75,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TextFileService } from 'vs/workbench/services/textfile/electron-browser/textFileService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ISCMService } from 'vs/workbench/services/scm/common/scm'; +import { SCMService } from 'vs/workbench/services/scm/common/scmService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -465,6 +467,9 @@ export class Workbench implements IPartService { // Text File Service serviceCollection.set(ITextFileService, this.instantiationService.createInstance(TextFileService)); + // SCM Service + serviceCollection.set(ISCMService, this.instantiationService.createInstance(SCMService)); + // Text Model Resolver Service serviceCollection.set(ITextModelResolverService, this.instantiationService.createInstance(TextModelResolverService)); diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index b2a38cce499..752ce17beb1 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -7,7 +7,6 @@ import * as nls from 'vs/nls'; import uri from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; -import severity from 'vs/base/common/severity'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IModel as EditorIModel, IEditorContribution, IRange } from 'vs/editor/common/editorCommon'; @@ -423,16 +422,6 @@ export interface IDebugService { */ removeReplExpressions(): void; - /** - * Adds a new log to the repl. Either a string value or a dictionary (used to inspect complex objects printed to the repl). - */ - logToRepl(value: string | { [key: string]: any }, severity?: severity): void; - - /** - * Appends new output to the repl. - */ - appendReplOutput(value: string, severity?: severity): void; - /** * Adds a new watch expression and evaluates it against the debug adapter. */ diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index ec359b44f3e..8de493c8544 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -842,9 +842,9 @@ export class Model implements debug.IModel { public appendReplOutput(value: string, severity?: severity): void { const elements: OutputElement[] = []; - let previousOutput = this.replElements.length && (this.replElements[this.replElements.length - 1]); - let lines = value.split('\n'); - let groupTogether = !!previousOutput && (previousOutput.category === 'output' && severity === previousOutput.severity); + const previousOutput = this.replElements.length && (this.replElements[this.replElements.length - 1]); + const lines = value.split('\n'); + const groupTogether = !!previousOutput && (previousOutput.category === 'output' && severity === previousOutput.severity); if (groupTogether) { // append to previous line if same group diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 53772e45d32..7bf8ada792d 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -195,12 +195,12 @@ export class DebugService implements debug.IDebugService { // flush any existing simple values logged if (simpleVals.length) { - this.logToRepl(simpleVals.join(' '), sev); + this.model.logToRepl(simpleVals.join(' '), sev); simpleVals = []; } // show object - this.logToRepl(a, sev); + this.model.logToRepl(a, sev); } // string: watch out for % replacement directive @@ -229,7 +229,7 @@ export class DebugService implements debug.IDebugService { // flush simple values if (simpleVals.length) { - this.logToRepl(simpleVals.join(' '), sev); + this.model.logToRepl(simpleVals.join(' '), sev); } } } @@ -368,7 +368,7 @@ export class DebugService implements debug.IDebugService { private onOutput(event: DebugProtocol.OutputEvent): void { const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; - this.appendReplOutput(event.body.output, outputSeverity); + this.model.appendReplOutput(event.body.output, outputSeverity); } private loadBreakpoints(): Breakpoint[] { @@ -515,14 +515,6 @@ export class DebugService implements debug.IDebugService { .then(() => this.focusStackFrameAndEvaluate(this.viewModel.focusedStackFrame)); } - public logToRepl(value: string | { [key: string]: any }, severity?: severity): void { - this.model.logToRepl(value, severity); - } - - public appendReplOutput(value: string, severity?: severity): void { - this.model.appendReplOutput(value, severity); - } - public removeReplExpressions(): void { this.model.removeReplExpressions(); } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index ead1b3f53e6..188341e19c8 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -385,7 +385,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { }); } else if (request.command === 'handshake') { try { - const vsda = (require.__$__nodeRequire('vsda')); + const vsda = require.__$__nodeRequire('vsda'); const obj = new vsda.signer(); const sig = obj.sign(request.arguments.value); response.body = { diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index c589e362310..9c887f4f3ec 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -5,7 +5,6 @@ import uri from 'vs/base/common/uri'; import Event from 'vs/base/common/event'; -import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import debug = require('vs/workbench/parts/debug/common/debug'); import { Source } from 'vs/workbench/parts/debug/common/debugSource'; @@ -63,10 +62,6 @@ export class MockDebugService implements debug.IDebugService { public removeReplExpressions(): void { } - public logToRepl(value: string | { [key: string]: any }, severity?: severity): void { } - - public appendReplOutput(value: string, severity?: severity): void { } - public addWatchExpression(name?: string): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts index 32d184db3d6..f0b0644c178 100644 --- a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts +++ b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts @@ -8,14 +8,9 @@ import 'vs/css!./media/git.contribution'; import nls = require('vs/nls'); import async = require('vs/base/common/async'); -import errors = require('vs/base/common/errors'); -import paths = require('vs/base/common/paths'); import lifecycle = require('vs/base/common/lifecycle'); -import winjs = require('vs/base/common/winjs.base'); import ext = require('vs/workbench/common/contributions'); import git = require('vs/workbench/parts/git/common/git'); -import common = require('vs/editor/common/editorCommon'); -import widget = require('vs/editor/browser/codeEditor'); import viewlet = require('vs/workbench/browser/viewlet'); import statusbar = require('vs/workbench/browser/parts/statusbar/statusbar'); import platform = require('vs/platform/platform'); @@ -30,18 +25,10 @@ import quickopen = require('vs/workbench/browser/quickopen'); import 'vs/workbench/parts/git/browser/gitEditorContributions'; import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activityService'; import { IEventService } from 'vs/platform/event/common/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService } from 'vs/platform/message/common/message'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { RawText } from 'vs/editor/common/model/textModel'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import URI from 'vs/base/common/uri'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { Schemas } from 'vs/base/common/network'; import IGitService = git.IGitService; @@ -120,330 +107,6 @@ export class StatusUpdater implements ext.IWorkbenchContribution { } } -class DirtyDiffModelDecorator { - static ID = 'vs.git.editor.dirtyDiffDecorator'; - static MODIFIED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-modified-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - static ADDED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-added-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - static DELETED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-deleted-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - - private modelService: IModelService; - private editorWorkerService: IEditorWorkerService; - private editorService: IWorkbenchEditorService; - private contextService: IWorkspaceContextService; - private gitService: IGitService; - - private model: common.IModel; - private _originalContentsURI: URI; - private path: string; - private decorations: string[]; - - private delayer: async.ThrottledDelayer; - private diffDelayer: async.ThrottledDelayer; - private toDispose: lifecycle.IDisposable[]; - - constructor(model: common.IModel, path: string, - @IModelService modelService: IModelService, - @IEditorWorkerService editorWorkerService: IEditorWorkerService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IGitService gitService: IGitService - ) { - this.modelService = modelService; - this.editorWorkerService = editorWorkerService; - this.editorService = editorService; - this.contextService = contextService; - this.gitService = gitService; - - this.model = model; - this._originalContentsURI = model.uri.with({ scheme: Schemas.internal }); - this.path = path; - this.decorations = []; - - this.delayer = new async.ThrottledDelayer(500); - this.diffDelayer = new async.ThrottledDelayer(200); - - this.toDispose = []; - this.toDispose.push(model.onDidChangeContent(() => this.triggerDiff())); - this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.STATE_CHANGED, () => this.onChanges())); - this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.OPERATION_END, e => { - if (e.operation.id !== git.ServiceOperations.BACKGROUND_FETCH) { - this.onChanges(); - } - })); - - this.onChanges(); - } - - private onChanges(): void { - if (!this.gitService) { - return; - } - - if (this.gitService.getState() !== git.ServiceState.OK) { - return; - } - - // go through all interesting models - this.trigger(); - } - - private trigger(): void { - this.delayer - .trigger(() => this.diffOriginalContents()) - .done(null, errors.onUnexpectedError); - } - - private diffOriginalContents(): winjs.TPromise { - return this.getOriginalContents() - .then(contents => { - if (!this.model || this.model.isDisposed()) { - return; // disposed - } - - if (!contents) { - // untracked file - this.modelService.destroyModel(this._originalContentsURI); - return this.triggerDiff(); - } - - let originalModel = this.modelService.getModel(this._originalContentsURI); - if (originalModel) { - let contentsRawText = RawText.fromStringWithModelOptions(contents, originalModel); - - // return early if nothing has changed - if (originalModel.equals(contentsRawText)) { - return winjs.TPromise.as(null); - } - - // we already have the original contents - originalModel.setValueFromRawText(contentsRawText); - } else { - // this is the first time we load the original contents - this.modelService.createModel(contents, null, this._originalContentsURI); - } - - return this.triggerDiff(); - }); - } - - private getOriginalContents(): winjs.TPromise { - var gitModel = this.gitService.getModel(); - var treeish = gitModel.getStatus().find(this.path, git.StatusType.INDEX) ? '~' : 'HEAD'; - - return this.gitService.buffer(this.path, treeish); - } - - private triggerDiff(): winjs.Promise { - if (!this.diffDelayer) { - return winjs.TPromise.as(null); - } - - return this.diffDelayer.trigger(() => { - if (!this.model || this.model.isDisposed()) { - return winjs.TPromise.as([]); // disposed - } - - return this.editorWorkerService.computeDirtyDiff(this._originalContentsURI, this.model.uri, true); - }).then((diff: common.IChange[]) => { - if (!this.model || this.model.isDisposed()) { - return; // disposed - } - - return this.decorations = this.model.deltaDecorations(this.decorations, DirtyDiffModelDecorator.changesToDecorations(diff || [])); - }); - } - - private static changesToDecorations(diff: common.IChange[]): common.IModelDeltaDecoration[] { - return diff.map((change) => { - var startLineNumber = change.modifiedStartLineNumber; - var endLineNumber = change.modifiedEndLineNumber || startLineNumber; - - // Added - if (change.originalEndLineNumber === 0) { - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.ADDED_DECORATION_OPTIONS - }; - } - - // Removed - if (change.modifiedEndLineNumber === 0) { - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: startLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.DELETED_DECORATION_OPTIONS - }; - } - - // Modified - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.MODIFIED_DECORATION_OPTIONS - }; - }); - } - - public dispose(): void { - this.modelService.destroyModel(this._originalContentsURI); - this.toDispose = lifecycle.dispose(this.toDispose); - if (this.model && !this.model.isDisposed()) { - this.model.deltaDecorations(this.decorations, []); - } - this.model = null; - this.decorations = null; - if (this.delayer) { - this.delayer.cancel(); - this.delayer = null; - } - if (this.diffDelayer) { - this.diffDelayer.cancel(); - this.diffDelayer = null; - } - } -} - -export class DirtyDiffDecorator implements ext.IWorkbenchContribution { - - private gitService: IGitService; - private messageService: IMessageService; - private editorService: IWorkbenchEditorService; - private eventService: IEventService; - private contextService: IWorkspaceContextService; - private instantiationService: IInstantiationService; - private models: common.IModel[]; - private decorators: { [modelId: string]: DirtyDiffModelDecorator }; - private toDispose: lifecycle.IDisposable[]; - - constructor( - @IGitService gitService: IGitService, - @IMessageService messageService: IMessageService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEditorGroupService editorGroupService: IEditorGroupService, - @IEventService eventService: IEventService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IInstantiationService instantiationService: IInstantiationService - ) { - this.gitService = gitService; - this.messageService = messageService; - this.editorService = editorService; - this.eventService = eventService; - this.contextService = contextService; - this.instantiationService = instantiationService; - - this.models = []; - this.decorators = Object.create(null); - this.toDispose = []; - this.toDispose.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); - this.toDispose.push(gitService.addListener2(git.ServiceEvents.DISPOSE, () => this.dispose())); - } - - public getId(): string { - return 'git.DirtyDiffModelDecorator'; - } - - private onEditorsChanged(): void { - // HACK: This is the best current way of figuring out whether to draw these decorations - // or not. Needs context from the editor, to know whether it is a diff editor, in place editor - // etc. - - const repositoryRoot = this.gitService.getModel().getRepositoryRoot(); - - // If there is no repository root, just wait until that changes - if (typeof repositoryRoot !== 'string') { - this.gitService.addOneTimeDisposableListener(git.ServiceEvents.STATE_CHANGED, () => this.onEditorsChanged()); - - this.models.forEach(m => this.onModelInvisible(m)); - this.models = []; - return; - } - - const models = this.editorService.getVisibleEditors() - - // map to the editor controls - .map(e => e.getControl()) - - // only interested in code editor widgets - .filter(c => c instanceof widget.CodeEditor) - - // map to models - .map(e => (e).getModel()) - - // remove nulls and duplicates - .filter((m, i, a) => !!m && a.indexOf(m, i + 1) === -1) - - // get the associated resource - .map(m => ({ model: m, resource: m.uri })) - - // remove nulls - .filter(p => !!p.resource && - // and invalid resources - (p.resource.scheme === 'file' && paths.isEqualOrParent(p.resource.fsPath, repositoryRoot)) - ) - - // get paths - .map(p => ({ model: p.model, path: paths.normalize(paths.relative(repositoryRoot, p.resource.fsPath)) })) - - // remove nulls and inside .git files - .filter(p => !!p.path && p.path.indexOf('.git/') === -1); - - var newModels = models.filter(p => this.models.every(m => p.model !== m)); - var oldModels = this.models.filter(m => models.every(p => p.model !== m)); - - newModels.forEach(p => this.onModelVisible(p.model, p.path)); - oldModels.forEach(m => this.onModelInvisible(m)); - - this.models = models.map(p => p.model); - } - - private onModelVisible(model: common.IModel, path: string): void { - this.decorators[model.id] = this.instantiationService.createInstance(DirtyDiffModelDecorator, model, path); - } - - private onModelInvisible(model: common.IModel): void { - this.decorators[model.id].dispose(); - delete this.decorators[model.id]; - } - - public dispose(): void { - this.toDispose = lifecycle.dispose(this.toDispose); - this.models.forEach(m => this.decorators[m.id].dispose()); - this.models = null; - this.decorators = null; - } -} - export const VIEWLET_ID = 'workbench.view.git'; class OpenGitViewletAction extends viewlet.ToggleViewletAction { @@ -500,11 +163,6 @@ export function registerContributions(): void { StatusUpdater ); - // Register DirtyDiffDecorator - (platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution( - DirtyDiffDecorator - ); - // Register Quick Open for git (platform.Registry.as(quickopen.Extensions.Quickopen)).registerQuickOpenHandler( new quickopen.QuickOpenHandlerDescriptor( diff --git a/src/vs/workbench/parts/git/browser/media/git.contribution.css b/src/vs/workbench/parts/git/browser/media/git.contribution.css index 4e23b10ae50..48a983ffd30 100644 --- a/src/vs/workbench/parts/git/browser/media/git.contribution.css +++ b/src/vs/workbench/parts/git/browser/media/git.contribution.css @@ -46,37 +46,6 @@ background-color: rgba(235, 59, 0, 0.3); } -/* Git dirty diff editor decorations */ -.monaco-editor .git-dirty-modified-diff-glyph { - border-left: 3px solid rgba(0, 122, 204, 0.6); - margin-left: 5px; -} -.monaco-editor.vs-dark .git-dirty-modified-diff-glyph { - border-left: 3px solid rgba(0, 188, 242, 0.6); - margin-left: 5px; -} -.monaco-editor .git-dirty-added-diff-glyph { - border-left: 3px solid rgba(45, 136, 62, 0.6); - margin-left: 5px; -} -.monaco-editor.vs-dark .git-dirty-added-diff-glyph { - border-left: 3px solid rgba(127, 186, 0, 0.6); - margin-left: 5px; -} -.monaco-editor .git-dirty-deleted-diff-glyph:after { - content: ''; - position: absolute; - bottom: -4px; - margin-left: 5px; - box-sizing: border-box; - border-left: 4px solid rgba(185, 19, 26, 0.76); - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - width: 4px; - height: 0; - z-index: 9; -} - .monaco-shell .git-branch-dropdown-menu .action-label.git-action.checkout.HEAD { font-weight: bold; } diff --git a/src/vs/workbench/parts/git/common/gitContentProvider.ts b/src/vs/workbench/parts/git/common/gitContentProvider.ts new file mode 100644 index 00000000000..8dad7f3d5ec --- /dev/null +++ b/src/vs/workbench/parts/git/common/gitContentProvider.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IModelService } from 'vs/editor/common/services/modelService'; +import URI from 'vs/base/common/uri'; +import { dispose } from 'vs/base/common/lifecycle'; +import { Throttler } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IModel } from 'vs/editor/common/editorCommon'; +import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; +import { IBaselineResourceProvider, ISCMService } from 'vs/workbench/services/scm/common/scm'; +import { IGitService, StatusType, ServiceEvents, ServiceOperations, ServiceState } from 'vs/workbench/parts/git/common/git'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +export class GitContentProvider implements IWorkbenchContribution, ITextModelContentProvider, IBaselineResourceProvider { + + private throttler = new Throttler(); + + constructor( + @ITextModelResolverService textModelResolverService: ITextModelResolverService, + @IModelService private modelService: IModelService, + @ISCMService private scmService: ISCMService, + @IGitService private gitService: IGitService + ) { + this.scmService.registerBaselineResourceProvider(this); + textModelResolverService.registerTextModelContentProvider('git-index', this); + } + + getBaselineResource(resource: URI): TPromise { + return TPromise.as(resource.with({ scheme: 'git-index' })); + } + + provideTextContent(uri: URI): TPromise { + if (uri.scheme !== 'git-index') { + return null; + } + + const gitModel = this.gitService.getModel(); + const path = uri.fsPath; + const treeish = gitModel.getStatus().find(path, StatusType.INDEX) ? '~' : 'HEAD'; + + return this.gitService.buffer(path, treeish) + .then(contents => this.modelService.createModel(contents || '', null, uri)) + .then(model => { + const trigger = () => { + this.throttler.queue(() => { + return this.gitService.buffer(path, treeish) + .then(contents => { + // TODO@Joao who owns the model? this is confusing + // if (!contents) { + // model.destroy(); + // return; + // } + + model.setValue(contents); + }); + }).done(null, onUnexpectedError); + }; + + const onChanges = () => { + if (this.gitService.getState() !== ServiceState.OK) { + return; + } + + trigger(); + }; + + const disposables = [ + this.gitService.addListener2(ServiceEvents.STATE_CHANGED, onChanges), + this.gitService.addListener2(ServiceEvents.OPERATION_END, e => { + if (e.operation.id !== ServiceOperations.BACKGROUND_FETCH) { + onChanges(); + } + }) + ]; + + model.onWillDispose(() => { + dispose(disposables); + }); + + return model; + }); + } + + getId(): string { + return 'git.contentprovider'; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts index 0516fbf8f54..615963248c5 100644 --- a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts +++ b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts @@ -10,15 +10,20 @@ import { IGitService } from 'vs/workbench/parts/git/common/git'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actionRegistry'; import { CloneAction } from './gitActions'; +import { GitContentProvider } from '../common/gitContentProvider'; +import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actionRegistry'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; registerContributions(); // Register Service registerSingleton(IGitService, ElectronGitService); -const workbenchActionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); const category = localize('git', "Git"); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); \ No newline at end of file +Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(GitContentProvider); \ No newline at end of file diff --git a/src/vs/workbench/parts/git/node/rawGitService.ts b/src/vs/workbench/parts/git/node/rawGitService.ts index 85efc9c8c66..48361f0a4ed 100644 --- a/src/vs/workbench/parts/git/node/rawGitService.ts +++ b/src/vs/workbench/parts/git/node/rawGitService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { join } from 'path'; +import { join, isAbsolute, relative } from 'path'; import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { detectMimesFromFile, detectMimesFromStream } from 'vs/base/node/mime'; import { realpath, exists } from 'vs/base/node/pfs'; @@ -187,6 +187,11 @@ export class RawGitService implements IRawGitService { // careful, this buffers the whole object into memory show(filePath: string, treeish?: string): TPromise { treeish = (!treeish || treeish === '~') ? '' : treeish; + + if (isAbsolute(filePath)) { + filePath = relative(this.repo.path, filePath); + } + return this.repo.buffer(treeish + ':' + filePath).then(null, e => { if (e instanceof GitError) { return ''; // mostly untracked files end up in a git error diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index ec74f6ad9c9..7c3fb2cdc9a 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -60,8 +60,13 @@ CommandsRegistry.registerCommand('_workbench.htmlZone', function (accessor: Serv return; } - return accessor.get(ITextModelResolverService).resolve(params.resource).then(model => { + const textModelResolverService = accessor.get(ITextModelResolverService); + + return textModelResolverService.createModelReference(params.resource).then(ref => { + const model = ref.object; const contents = model.textEditorModel.getValue(); + ref.dispose(); + HtmlZoneController.getInstance(codeEditor).addZone(params.lineNumber, contents); }); diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index 2b0cafc32be..03e4e08d058 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -10,7 +10,7 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModel } from 'vs/editor/common/editorCommon'; import { Dimension, Builder } from 'vs/base/browser/builder'; -import { empty as EmptyDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { EditorOptions, EditorInput } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { Position } from 'vs/platform/editor/common/editor'; @@ -20,7 +20,7 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel' import { HtmlInput } from 'vs/workbench/parts/html/common/htmlInput'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; +import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import Webview from './webview'; /** @@ -39,7 +39,8 @@ export class HtmlPreviewPart extends BaseEditor { private _baseUrl: URI; - private _model: IModel; + private _modelRef: IReference; + private get _model(): IModel { return this._modelRef.object.textEditorModel; } private _modelChangeSubscription = EmptyDisposable; private _themeChangeSubscription = EmptyDisposable; @@ -65,7 +66,9 @@ export class HtmlPreviewPart extends BaseEditor { // unhook listeners this._themeChangeSubscription.dispose(); this._modelChangeSubscription.dispose(); - this._model = undefined; + if (this._modelRef) { + this._modelRef.dispose(); + } super.dispose(); } @@ -121,7 +124,7 @@ export class HtmlPreviewPart extends BaseEditor { } private _hasValidModel(): boolean { - return this._model && !this._model.isDisposed(); + return this._modelRef && this._model && !this._model.isDisposed(); } public layout(dimension: Dimension): void { @@ -141,7 +144,9 @@ export class HtmlPreviewPart extends BaseEditor { return TPromise.as(undefined); } - this._model = undefined; + if (this._modelRef) { + this._modelRef.dispose(); + } this._modelChangeSubscription.dispose(); if (!(input instanceof HtmlInput)) { @@ -149,14 +154,18 @@ export class HtmlPreviewPart extends BaseEditor { } return super.setInput(input, options).then(() => { - let resourceUri = (input).getResource(); - return this._textModelResolverService.resolve(resourceUri).then(model => { + const resourceUri = (input).getResource(); + return this._textModelResolverService.createModelReference(resourceUri).then(ref => { + const model = ref.object; + if (model instanceof BaseTextEditorModel) { - this._model = model.textEditorModel; + this._modelRef = ref; } + if (!this._model) { return TPromise.wrapError(localize('html.voidInput', "Invalid editor input.")); } + this._modelChangeSubscription = this._model.onDidChangeContent(() => this.webview.contents = this._model.getLinesContent()); this.webview.baseUrl = resourceUri.toString(true); this.webview.contents = this._model.getLinesContent(); diff --git a/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts new file mode 100644 index 00000000000..119f5834ca9 --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/dirtydiffDecorator'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import * as winjs from 'vs/base/common/winjs.base'; +import { memoize } from 'vs/base/common/decorators'; +import * as ext from 'vs/workbench/common/contributions'; +import * as common from 'vs/editor/common/editorCommon'; +import * as widget from 'vs/editor/browser/codeEditor'; +import { IEventService } from 'vs/platform/event/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IMessageService } from 'vs/platform/message/common/message'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import URI from 'vs/base/common/uri'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { ISCMService } from 'vs/workbench/services/scm/common/scm'; + +class DirtyDiffModelDecorator { + + static MODIFIED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'dirty-diff-modified-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + static ADDED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'dirty-diff-added-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + static DELETED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'dirty-diff-deleted-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + private decorations: string[]; + private baselineModel: common.IModel; + private diffDelayer: ThrottledDelayer; + private toDispose: IDisposable[]; + + constructor( + private model: common.IModel, + private uri: URI, + @ISCMService private scmService: ISCMService, + @IModelService private modelService: IModelService, + @IEditorWorkerService private editorWorkerService: IEditorWorkerService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService + ) { + this.decorations = []; + this.diffDelayer = new ThrottledDelayer(200); + this.toDispose = []; + this.triggerDiff(); + this.toDispose.push(model.onDidChangeContent(() => this.triggerDiff())); + } + + @memoize + private get originalURIPromise(): winjs.TPromise { + return this.scmService.getBaselineResource(this.uri) + .then(originalUri => this.textModelResolverService.createModelReference(originalUri) + .then(ref => { + this.baselineModel = ref.object.textEditorModel; + + this.toDispose.push(ref); + this.toDispose.push(ref.object.textEditorModel.onDidChangeContent(() => this.triggerDiff())); + + return originalUri; + })); + } + + private triggerDiff(): winjs.Promise { + if (!this.diffDelayer) { + return winjs.TPromise.as(null); + } + + return this.diffDelayer + .trigger(() => this.diff()) + .then((diff: common.IChange[]) => { + if (!this.model || this.model.isDisposed() || !this.baselineModel || this.baselineModel.isDisposed()) { + return; // disposed + } + + if (this.baselineModel.getValueLength() === 0) { + diff = []; + } + + return this.decorations = this.model.deltaDecorations(this.decorations, DirtyDiffModelDecorator.changesToDecorations(diff || [])); + }); + } + + private diff(): winjs.Promise { + return this.originalURIPromise.then(originalURI => { + if (!this.model || this.model.isDisposed()) { + return winjs.TPromise.as([]); // disposed + } + + this.editorWorkerService.computeDirtyDiff(originalURI, this.model.uri, true); + }); + } + + private static changesToDecorations(diff: common.IChange[]): common.IModelDeltaDecoration[] { + return diff.map((change) => { + const startLineNumber = change.modifiedStartLineNumber; + const endLineNumber = change.modifiedEndLineNumber || startLineNumber; + + // Added + if (change.originalEndLineNumber === 0) { + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.ADDED_DECORATION_OPTIONS + }; + } + + // Removed + if (change.modifiedEndLineNumber === 0) { + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: startLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.DELETED_DECORATION_OPTIONS + }; + } + + // Modified + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.MODIFIED_DECORATION_OPTIONS + }; + }); + } + + dispose(): void { + this.toDispose = dispose(this.toDispose); + + if (this.model && !this.model.isDisposed()) { + this.model.deltaDecorations(this.decorations, []); + } + + this.model = null; + this.baselineModel = null; + this.decorations = null; + + if (this.diffDelayer) { + this.diffDelayer.cancel(); + this.diffDelayer = null; + } + } +} + +export class DirtyDiffDecorator implements ext.IWorkbenchContribution { + + private models: common.IModel[] = []; + private decorators: { [modelId: string]: DirtyDiffModelDecorator } = Object.create(null); + private toDispose: IDisposable[] = []; + + constructor( + @IMessageService private messageService: IMessageService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @IEventService private eventService: IEventService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + this.toDispose.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); + } + + getId(): string { + return 'git.DirtyDiffModelDecorator'; + } + + private onEditorsChanged(): void { + // HACK: This is the best current way of figuring out whether to draw these decorations + // or not. Needs context from the editor, to know whether it is a diff editor, in place editor + // etc. + + const models = this.editorService.getVisibleEditors() + + // map to the editor controls + .map(e => e.getControl()) + + // only interested in code editor widgets + .filter(c => c instanceof widget.CodeEditor) + + // map to models + .map(e => (e).getModel()) + + // remove nulls and duplicates + .filter((m, i, a) => !!m && !!m.uri && a.indexOf(m, i + 1) === -1) + + // get the associated resource + .map(m => ({ model: m, uri: m.uri })); + + const newModels = models.filter(p => this.models.every(m => p.model !== m)); + const oldModels = this.models.filter(m => models.every(p => p.model !== m)); + + newModels.forEach(({ model, uri }) => this.onModelVisible(model, uri)); + oldModels.forEach(m => this.onModelInvisible(m)); + + this.models = models.map(p => p.model); + } + + private onModelVisible(model: common.IModel, uri: URI): void { + this.decorators[model.id] = this.instantiationService.createInstance(DirtyDiffModelDecorator, model, uri); + } + + private onModelInvisible(model: common.IModel): void { + this.decorators[model.id].dispose(); + delete this.decorators[model.id]; + } + + dispose(): void { + this.toDispose = dispose(this.toDispose); + this.models.forEach(m => this.decorators[m.id].dispose()); + this.models = null; + this.decorators = null; + } +} diff --git a/src/vs/workbench/parts/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/parts/scm/browser/media/dirtydiffDecorator.css new file mode 100644 index 00000000000..31023d7ffa9 --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/media/dirtydiffDecorator.css @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .dirty-diff-modified-glyph { + border-left: 3px solid rgba(0, 122, 204, 0.6); + margin-left: 5px; +} + +.monaco-editor.vs-dark .dirty-diff-modified-glyph { + border-left: 3px solid rgba(0, 188, 242, 0.6); + margin-left: 5px; +} + +.monaco-editor .dirty-diff-added-glyph { + border-left: 3px solid rgba(45, 136, 62, 0.6); + margin-left: 5px; +} + +.monaco-editor.vs-dark .dirty-diff-added-glyph { + border-left: 3px solid rgba(127, 186, 0, 0.6); + margin-left: 5px; +} + +.monaco-editor .dirty-diff-deleted-glyph:after { + content: ''; + position: absolute; + bottom: -4px; + margin-left: 5px; + box-sizing: border-box; + border-left: 4px solid rgba(185, 19, 26, 0.76); + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + width: 4px; + height: 0; + z-index: 9; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/browser/scm.contribution.ts b/src/vs/workbench/parts/scm/browser/scm.contribution.ts new file mode 100644 index 00000000000..40d189f227c --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/scm.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Registry } from 'vs/platform/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { DirtyDiffDecorator } from './dirtydiffDecorator'; + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(DirtyDiffDecorator); \ No newline at end of file diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index 518ad4d0453..cffb8d16073 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -57,11 +57,13 @@ class EditorInputCache { if (editorInputPromise) { editorInputPromise.done(() => { if (reloadFromSource) { - this.textModelResolverService.resolve(fileMatch.resource()).then(editorModel => { - if (editorModel.textEditorModel) { + this.textModelResolverService.createModelReference(fileMatch.resource()).done(ref => { + const model = ref.object; + if (model.textEditorModel) { let replaceResource = this.getReplaceResource(fileMatch.resource()); - this.modelService.getModel(replaceResource).setValue(editorModel.textEditorModel.getValue()); + this.modelService.getModel(replaceResource).setValue(model.textEditorModel.getValue()); this.replaceService.replace(fileMatch, null, replaceResource); + ref.dispose(); } }); } else { @@ -107,13 +109,14 @@ class EditorInputCache { } private createRightInput(element: FileMatch): TPromise { - return new TPromise((c, e, p) => { - this.textModelResolverService.resolve(element.resource()).then(value => { - let model = value.textEditorModel; - let replaceResource = this.getReplaceResource(element.resource()); - this.modelService.createModel(model.getValue(), model.getMode(), replaceResource); - c(this.editorService.createInput({ resource: replaceResource })); - }); + return this.textModelResolverService.createModelReference(element.resource()).then(ref => { + const model = ref.object; + let textEditorModel = model.textEditorModel; + let replaceResource = this.getReplaceResource(element.resource()); + this.modelService.createModel(textEditorModel.getValue(), textEditorModel.getMode(), replaceResource); + ref.dispose(); + + return this.editorService.createInput({ resource: replaceResource }); }); } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 06cf03b2ce7..0f4c79ef8aa 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -9,9 +9,6 @@ import URI from 'vs/base/common/uri'; import network = require('vs/base/common/network'); import { Registry } from 'vs/platform/platform'; import { basename, dirname } from 'vs/base/common/paths'; -import types = require('vs/base/common/types'); -import { IDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ICommonCodeEditor, IModel, EditorType, IEditor as ICommonEditor } from 'vs/editor/common/editorCommon'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorOptions, IFileEditorInput, TextEditorOptions, IEditorRegistry, Extensions } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -206,46 +203,9 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { // Base Text Editor Support for inmemory resources const resourceInput = input; - if (resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.inMemory) { - - // For in-memory resources we only support to resolve the input from the current active editor - // because the workbench does not track editor models by in memory URL. This concept is only - // being used in the code editor. - const activeEditor = this.getActiveEditor(); - if (activeEditor) { - const control = activeEditor.getControl(); - if (types.isFunction(control.getEditorType)) { - - // Single Editor: If code editor model matches, return input from editor - if (control.getEditorType() === EditorType.ICodeEditor) { - const codeEditor = control; - const model = this.findModel(codeEditor, input); - if (model) { - return TPromise.as(activeEditor.input); - } - } - - // Diff Editor: If left or right code editor model matches, return associated input - else if (control.getEditorType() === EditorType.IDiffEditor) { - const diffInput = activeEditor.input; - const diffCodeEditor = control; - - const originalModel = this.findModel(diffCodeEditor.getOriginalEditor(), input); - if (originalModel) { - return TPromise.as(diffInput.originalInput); - } - - const modifiedModel = this.findModel(diffCodeEditor.getModifiedEditor(), input); - if (modifiedModel) { - return TPromise.as(diffInput.modifiedInput); - } - } - } - } - } // Untitled file support - else if (resourceInput.resource instanceof URI && (resourceInput.resource.scheme === UntitledEditorInput.SCHEMA)) { + if (resourceInput.resource instanceof URI && (resourceInput.resource.scheme === UntitledEditorInput.SCHEMA)) { return TPromise.as(this.untitledEditorService.createOrGet(resourceInput.resource)); } @@ -273,15 +233,6 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { return typedFileInput; }); } - - private findModel(editor: ICommonCodeEditor, input: IResourceInput): IModel { - const model = editor.getModel(); - if (!model) { - return null; - } - - return model.uri.toString() === input.resource.toString() ? model : null; - } } export interface IDelegatingWorkbenchEditorServiceHandler { diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts new file mode 100644 index 00000000000..d5be391cdfc --- /dev/null +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import URI from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export interface IBaselineResourceProvider { + getBaselineResource(resource: URI): TPromise; +} + +export const ISCMService = createDecorator('scm'); + +export interface ISCMService { + + _serviceBrand: any; + + getBaselineResource(resource: URI): TPromise; + registerBaselineResourceProvider(provider: IBaselineResourceProvider): IDisposable; +} \ No newline at end of file diff --git a/src/vs/workbench/services/scm/common/scmService.ts b/src/vs/workbench/services/scm/common/scmService.ts new file mode 100644 index 00000000000..6249f985f21 --- /dev/null +++ b/src/vs/workbench/services/scm/common/scmService.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import URI from 'vs/base/common/uri'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ISCMService, IBaselineResourceProvider } from './scm'; + +export class SCMService implements ISCMService { + + _serviceBrand; + + private providers: IBaselineResourceProvider[] = []; + + getBaselineResource(resource: URI): TPromise { + const promises = this.providers + .map(p => p.getBaselineResource(resource)); + + return TPromise.join(promises).then(originalResources => { + // TODO@Joao: just take the first + return originalResources.filter(uri => !!uri)[0]; + }); + } + + registerBaselineResourceProvider(provider: IBaselineResourceProvider): IDisposable { + this.providers = [provider, ...this.providers]; + + return toDisposable(() => { + const index = this.providers.indexOf(provider); + + if (index < 0) { + return; + } + + this.providers.splice(index, 1); + }); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 779f8b9f2b5..6e308646021 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -12,7 +12,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor'; import { IFileStat, IBaseStat, IResolveContentOptions } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITextEditorModel } from 'vs/platform/editor/common/editor'; +import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { Event as BaseEvent, PropertyChangeEvent } from 'vs/base/common/events'; diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index aad206e758d..4a033541e84 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -6,142 +6,137 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; +import { first, always } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModel } from 'vs/editor/common/editorCommon'; -import { ITextEditorModel } from 'vs/platform/editor/common/editor'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { sequence } from 'vs/base/common/async'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import network = require('vs/base/common/network'); -import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; +import { ITextModelResolverService, ITextModelContentProvider, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorInput } from 'vs/workbench/common/editor'; + +class ResourceModelCollection extends ReferenceCollection> { + + private providers: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null); + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @IModelService private modelService: IModelService + ) { + super(); + } + + createReferencedObject(key: string): TPromise { + const resource = URI.parse(key); + + return this.resolveTextModelContent(this.modelService, key) + .then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); + } + + destroyReferencedObject(modelPromise: TPromise): void { + modelPromise.done(model => model.dispose()); + } + + registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { + const registry = this.providers; + const providers = registry[scheme] || (registry[scheme] = []); + + providers.unshift(provider); + + return toDisposable(() => { + const array = registry[scheme]; + + if (!array) { + return; + } + + const index = array.indexOf(provider); + + if (index === -1) { + return; + } + + array.splice(index, 1); + + if (array.length === 0) { + delete registry[scheme]; + } + }); + } + + private resolveTextModelContent(modelService: IModelService, key: string): TPromise { + const resource = URI.parse(key); + const model = modelService.getModel(resource); + + if (model) { + // TODO@Joao this should never happen + return TPromise.as(model); + } + + const providers = this.providers[resource.scheme] || []; + const factories = providers.map(p => () => p.provideTextContent(resource)); + + return first(factories).then(uri => { + if (!uri) { + return TPromise.wrapError(`Could not resolve any model with uri '${resource}'.`); + } + + return uri; + }); + } +} export class TextModelResolverService implements ITextModelResolverService { - public _serviceBrand: any; + _serviceBrand: any; - private loadingTextModels: { [uri: string]: TPromise } = Object.create(null); - private contentProviderRegistry: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null); + private promiseCache: { [uri: string]: TPromise> } = Object.create(null); + private resourceModelCollection: ResourceModelCollection; constructor( @ITextFileService private textFileService: ITextFileService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IInstantiationService private instantiationService: IInstantiationService, - @IModelService private modelService: IModelService + @IInstantiationService private instantiationService: IInstantiationService ) { + this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection); } - public resolve(resource: URI): TPromise { + createModelReference(resource: URI): TPromise> { + const uri = resource.toString(); + let promise = this.promiseCache[uri]; + if (promise) { + return promise; + } + + promise = this.promiseCache[uri] = this._createModelReference(resource); + + return always(promise, () => delete this.promiseCache[uri]); + } + + private _createModelReference(resource: URI): TPromise> { // File Schema: use text file service + // TODO ImmortalReference is a hack if (resource.scheme === network.Schemas.file) { - return this.textFileService.models.loadOrCreate(resource); + return this.textFileService.models.loadOrCreate(resource) + .then(model => new ImmortalReference(model)); } // Untitled Schema: go through cached input + // TODO ImmortalReference is a hack if (resource.scheme === UntitledEditorInput.SCHEMA) { - return this.untitledEditorService.createOrGet(resource).resolve(); + return this.untitledEditorService.createOrGet(resource).resolve() + .then(model => new ImmortalReference(model)); } - // In Memory: only works on the active editor - if (resource.scheme === network.Schemas.inMemory) { - return this.editorService.createInput({ resource }).then(input => { - if (input instanceof EditorInput) { - return input.resolve(); - } - - return null; - }); - } - - // Any other resource: use content provider registry - return this.resolveTextModelContent(this.modelService, resource).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource)); + const ref = this.resourceModelCollection.acquire(resource.toString()); + return ref.object.then(model => ({ object: model, dispose: () => ref.dispose() })); } - public registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { - let array = this.contentProviderRegistry[scheme]; - if (!array) { - array = [provider]; - this.contentProviderRegistry[scheme] = array; - } else { - array.unshift(provider); - } - - const registry = this.contentProviderRegistry; - return { - dispose() { - const array = registry[scheme]; - const idx = array.indexOf(provider); - if (idx >= 0) { - array.splice(idx, 1); - if (array.length === 0) { - delete registry[scheme]; - } - } - } - }; - } - - private resolveTextModelContent(modelService: IModelService, resource: URI): TPromise { - const model = modelService.getModel(resource); - if (model) { - return TPromise.as(model); - } - - let loadingTextModel = this.loadingTextModels[resource.toString()]; - if (!loadingTextModel) { - - // make sure we have a provider this scheme - // the resource uses - const contentProviders = this.contentProviderRegistry[resource.scheme]; - if (!contentProviders) { - return TPromise.wrapError(`No model with uri '${resource}' nor a resolver for the scheme '${resource.scheme}'.`); - } - - // load the model-content from the provider and cache - // the loading such that we don't create the same model - // twice - this.loadingTextModels[resource.toString()] = loadingTextModel = new TPromise((resolve, reject) => { - let result: IModel; - let lastError: any; - - sequence(contentProviders.map(provider => { - return () => { - if (!result) { - const contentPromise = provider.provideTextContent(resource); - if (!contentPromise) { - return TPromise.wrapError(`No resolver for the scheme '${resource.scheme}' found.`); - } - - return contentPromise.then(value => { - result = value; - }, err => { - lastError = err; - }); - } - }; - })).then(() => { - if (!result && lastError) { - reject(lastError); - } else { - resolve(result); - } - }, reject); - - }, function () { - // no cancellation when caching promises - }); - - // remove the cached promise 'cos the model is now known to the model service (see above) - loadingTextModel.then(() => delete this.loadingTextModels[resource.toString()], () => delete this.loadingTextModels[resource.toString()]); - } - - return loadingTextModel; + registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { + return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider); } } \ No newline at end of file diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index 5f09abac3ae..7562c7c1b62 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -78,35 +78,38 @@ suite('Workbench - TextModelResolverService', () => { }); }); - test('resolve file', function (done) { + test('resolve file', function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8'); (accessor.textFileService.models).add(model.getResource(), model); - model.load().then(() => { - accessor.textModelResolverServie.resolve(model.getResource()).then(model => { - const editorModel = model.textEditorModel as IModel; + + return model.load().then(() => { + return accessor.textModelResolverServie.createModelReference(model.getResource()).then(ref => { + const model = ref.object; + const editorModel = model.textEditorModel; assert.ok(editorModel); assert.equal(editorModel.getValue(), 'Hello Html'); - - done(); + ref.dispose(); }); }); }); - test('resolve untitled', function (done) { + test('resolve untitled', function () { const service = accessor.untitledEditorService; const input = service.createOrGet(); - input.resolve().then(() => { - accessor.textModelResolverServie.resolve(input.getResource()).then(model => { - const editorModel = model.textEditorModel as IModel; + return input.resolve().then(() => { + return accessor.textModelResolverServie.createModelReference(input.getResource()).then(ref => { + const model = ref.object; + const editorModel = model.textEditorModel; assert.ok(editorModel); + ref.dispose(); input.dispose(); - - done(); }); }); }); + + // TODO: add reference tests! }); \ No newline at end of file diff --git a/src/vs/workbench/test/node/api/extHostConfiguration.test.ts b/src/vs/workbench/test/node/api/extHostConfiguration.test.ts index 2548e4d3c84..8caacae668b 100644 --- a/src/vs/workbench/test/node/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/node/api/extHostConfiguration.test.ts @@ -122,7 +122,7 @@ suite('ExtHostConfiguration', function () { assert.throws(() => config['get'] = 'get-prop'); }); - test('udate/section to key', function () { + test('update/section to key', function () { const shape = new RecordingShape(); const allConfig = createExtHostConfiguration({ @@ -144,6 +144,17 @@ suite('ExtHostConfiguration', function () { assert.equal(shape.lastArgs[1], 'foo.bar'); }); + test('update, what is #15834', function () { + const shape = new RecordingShape(); + const allConfig = createExtHostConfiguration({ + ['editor.formatOnSave']: createConfigurationValue(true) + }, shape); + + allConfig.getConfiguration('editor').update('formatOnSave', { extensions: ['ts'] }); + assert.equal(shape.lastArgs[1], 'editor.formatOnSave'); + assert.deepEqual(shape.lastArgs[2], { extensions: ['ts'] }); + }); + test('update/error-state not OK', function () { const shape = new class extends MainThreadConfigurationShape {