diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts new file mode 100644 index 00000000000..2377db5d7bb --- /dev/null +++ b/src/vs/base/common/hash.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +/** + * Return a hash value for an object. + */ +export function hash(obj: any, hashVal = 0) : number { + switch (typeof obj) { + case 'object': + if (obj === null) { + return numberHash(349, hashVal); + } else if (Array.isArray(obj)) { + return arrayHash(obj, hashVal); + } + return objectHash(obj, hashVal); + case 'string': + return stringHash(obj, hashVal); + case 'boolean': + return booleanHash(obj, hashVal); + case 'number': + return numberHash(obj, hashVal); + case 'undefined': + return numberHash(obj, 937); + default: + return numberHash(obj, 617); + } +} + +function numberHash(val: number, initialHashVal: number) : number { + return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32 +} + +function booleanHash(b: boolean, initialHashVal: number) : number { + return numberHash(b ? 433 : 863, initialHashVal); +} + +function stringHash(s: string, hashVal: number) { + hashVal = numberHash(149417, hashVal); + for (let i = 0, length = s.length; i < length; i++) { + hashVal = numberHash(s.charCodeAt(i), hashVal); + } + return hashVal; +} + +function arrayHash(arr: any[], initialHashVal: number) : number { + initialHashVal = numberHash(104579, initialHashVal); + return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal); +} + +function objectHash(obj: any, initialHashVal: number) : number { + initialHashVal = numberHash(181387, initialHashVal); + return Object.keys(obj).sort().reduce((hashVal, key) => { + hashVal = stringHash(key, hashVal); + return hash(obj[key], hashVal); + }, initialHashVal); +} diff --git a/src/vs/base/test/common/hash.test.ts b/src/vs/base/test/common/hash.test.ts new file mode 100644 index 00000000000..7595ea8da5c --- /dev/null +++ b/src/vs/base/test/common/hash.test.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import {hash} from 'vs/base/common/hash'; + +suite('Hash', () => { + test('string', () => { + assert.equal(hash('hello'), hash('hello')); + assert.notEqual(hash('hello'), hash('world')); + assert.notEqual(hash('hello'), hash('olleh')); + assert.notEqual(hash('hello'), hash('Hello')); + assert.notEqual(hash('hello'), hash('Hello ')); + assert.notEqual(hash('h'), hash('H')); + assert.notEqual(hash('-'), hash('_')); + }); + + test('number', () => { + assert.equal(hash(1), hash(1.0)); + assert.notEqual(hash(0), hash(1)); + assert.notEqual(hash(1), hash(-1)); + assert.notEqual(hash(0x12345678), hash(0x123456789)); + }); + + test('boolean', () => { + assert.equal(hash(true), hash(true)); + assert.notEqual(hash(true), hash(false)); + }); + + test('array', () => { + assert.equal(hash([1, 2, 3]), hash([1, 2, 3])); + assert.equal(hash(['foo', 'bar']), hash(['foo', 'bar'])); + assert.equal(hash([]), hash([])); + assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo'])); + assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null])); + }); + + test('object', () => { + assert.equal(hash({}), hash({})); + assert.equal(hash({'foo': 'bar'}), hash({'foo': 'bar'})); + assert.equal(hash({'foo': 'bar', 'foo2': void 0}), hash({'foo2': void 0, 'foo': 'bar'})); + assert.notEqual(hash({'foo': 'bar'}), hash({'foo': 'bar2'})); + assert.notEqual(hash({}), hash([])); + }); +}); \ No newline at end of file diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 3e35030ea3f..88ee526ebbd 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -8,86 +8,196 @@ import * as objects from 'vs/base/common/objects'; import * as strings from 'vs/base/common/strings'; import URI from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; -import {IDecorationRenderOptions, IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IThemeDecorationRenderOptions, OverviewRulerLane} from 'vs/editor/common/editorCommon'; +import {IDecorationRenderOptions, IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IThemeDecorationRenderOptions, + IContentDecorationRenderOptions, OverviewRulerLane, TrackedRangeStickiness, IDecorationInstanceRenderOptions} from 'vs/editor/common/editorCommon'; import {AbstractCodeEditorService} from 'vs/editor/common/services/abstractCodeEditorService'; +import {IDisposable, toDisposable} from 'vs/base/common/lifecycle'; export class CodeEditorServiceImpl extends AbstractCodeEditorService { private _styleSheet: HTMLStyleElement; - private _decorationRenderOptions: {[key:string]:DecorationRenderOptions}; + private _decorationOptionProviders: {[key:string]:IModelDecorationOptionsProvider}; constructor() { super(); this._styleSheet = dom.createStyleSheet(); - this._decorationRenderOptions = Object.create(null); + this._decorationOptionProviders = Object.create(null); } public registerDecorationType(key:string, options: IDecorationRenderOptions): void { - if (this._decorationRenderOptions[key]) { - this._decorationRenderOptions[key].dispose(); - delete this._decorationRenderOptions[key]; + let provider = this._decorationOptionProviders[key]; + if (provider) { + provider.dispose(); + delete this._decorationOptionProviders[key]; + } + provider = new DecorationTypeOptionsProvider(this._styleSheet, key, options); + this._decorationOptionProviders[key] = provider; + provider.refCount++; + } + + public registerDecorationSubType(key:string, parentTypeKey: string, options: IDecorationInstanceRenderOptions): void { + let provider = this._decorationOptionProviders[key]; + if (!provider) { + provider = new DecorationSubTypeOptionsProvider(this._styleSheet, key, parentTypeKey, options); + this._decorationOptionProviders[key] = provider; + provider.refCount++; } - var decorationRenderOptions = new DecorationRenderOptions(this._styleSheet, key, options); - this._decorationRenderOptions[key] = decorationRenderOptions; } public removeDecorationType(key:string): void { - if (this._decorationRenderOptions[key]) { - this._decorationRenderOptions[key].dispose(); - delete this._decorationRenderOptions[key]; - - this.listCodeEditors().forEach((ed) => ed.removeDecorations(key)); + let provider = this._decorationOptionProviders[key]; + if (provider) { + provider.refCount--; + if (provider.refCount <= 0) { + delete this._decorationOptionProviders[key]; + provider.dispose(); + this.listCodeEditors().forEach((ed) => ed.removeDecorations(key)); + } } } - public resolveDecorationType(key:string): IModelDecorationOptions { - if (this._decorationRenderOptions[key]) { - return this._decorationRenderOptions[key]; + public resolveDecorationOptions(decorationTypeKey:string, writable: boolean): IModelDecorationOptions { + let provider = this._decorationOptionProviders[decorationTypeKey]; + if (!provider) { + throw new Error('Unknown decoration type key: ' + decorationTypeKey); } - throw new Error('Unknown decoration type key: ' + key); + return provider.getOptions(this, writable); } } -class DecorationRenderOptions implements IModelDecorationOptions { +interface IModelDecorationOptionsProvider extends IDisposable { + refCount: number; + getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions; +} + +class DecorationSubTypeOptionsProvider implements IModelDecorationOptionsProvider { + + public refCount: number; + + private _disposable: IDisposable; + private _parentTypeKey: string; + private _inlineClassName: string; + private _stickiness: TrackedRangeStickiness; + + constructor(styleSheet: HTMLStyleElement, key: string, parentTypeKey: string, options:IDecorationInstanceRenderOptions) { + this._parentTypeKey = parentTypeKey; + this.refCount = 0; + + var themedOpts = getThemedRenderOptions(options); + + let inlineClassName = DecorationRenderHelper.handle( + styleSheet, + key, + parentTypeKey, + ModelDecorationCSSRuleType.InlineClassName, + { + light: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.light.after), + dark: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.dark.after), + selector: '::after' + }, + { + light: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.light.before), + dark: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.dark.before), + selector: '::before' + } + ); + if (inlineClassName) { + this._stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + this._inlineClassName = inlineClassName; + + this._disposable = toDisposable(() => { + dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.Light, key), styleSheet); + dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.Dark, key), styleSheet); + dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.HighContrastBlack, key), styleSheet); + }); + } + } + + public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions { + let options = codeEditorService.resolveDecorationOptions(this._parentTypeKey, true); + if (this._inlineClassName) { + options.inlineClassName = this._inlineClassName; + } + if (this._stickiness) { + options.stickiness = this._stickiness; + } + return options; + } + + public dispose(): void { + if (this._disposable) { + this._disposable.dispose(); + delete this._disposable; + } + } +} + +class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { private _styleSheet: HTMLStyleElement; - private _key: string; + public _key: string; + + public refCount: number; public className: string; public inlineClassName: string; public glyphMarginClassName: string; public isWholeLine:boolean; public overviewRuler:IModelDecorationOverviewRulerOptions; + public stickiness: TrackedRangeStickiness; constructor(styleSheet: HTMLStyleElement, key:string, options:IDecorationRenderOptions) { - var themedOpts = resolveDecorationRenderOptions(options); + var themedOpts = getThemedRenderOptions(options); this._styleSheet = styleSheet; this._key = key; - this.className = DecorationRenderOptions._handle( + this.className = DecorationRenderHelper.handle( this._styleSheet, this._key, + null, ModelDecorationCSSRuleType.ClassName, - DecorationRenderOptions._getCSSTextForModelDecorationClassName(themedOpts.light), - DecorationRenderOptions._getCSSTextForModelDecorationClassName(themedOpts.dark) + { + light: DecorationRenderHelper.getCSSTextForModelDecorationClassName(themedOpts.light), + dark: DecorationRenderHelper.getCSSTextForModelDecorationClassName(themedOpts.dark) + } ); - this.inlineClassName = DecorationRenderOptions._handle( + this.inlineClassName = DecorationRenderHelper.handle( this._styleSheet, this._key, + null, ModelDecorationCSSRuleType.InlineClassName, - DecorationRenderOptions._getCSSTextForModelDecorationInlineClassName(themedOpts.light), - DecorationRenderOptions._getCSSTextForModelDecorationInlineClassName(themedOpts.dark) + { + light: DecorationRenderHelper.getCSSTextForModelDecorationInlineClassName(themedOpts.light), + dark: DecorationRenderHelper.getCSSTextForModelDecorationInlineClassName(themedOpts.dark) + }, + { + light: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.light.after), + dark: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.dark.after), + selector: '::after' + }, + { + light: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.light.before), + dark: DecorationRenderHelper.getCSSTextForModelDecorationContentClassName(themedOpts.dark.before), + selector: '::before' + } ); - this.glyphMarginClassName = DecorationRenderOptions._handle( + if (themedOpts.light.before || themedOpts.light.after || themedOpts.dark.before || themedOpts.dark.after) { + this.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + } + + this.glyphMarginClassName = DecorationRenderHelper.handle( this._styleSheet, this._key, + null, ModelDecorationCSSRuleType.GlyphMarginClassName, - DecorationRenderOptions._getCSSTextForModelDecorationGlyphMarginClassName(themedOpts.light), - DecorationRenderOptions._getCSSTextForModelDecorationGlyphMarginClassName(themedOpts.dark) + { + light: DecorationRenderHelper.getCSSTextForModelDecorationGlyphMarginClassName(themedOpts.light), + dark: DecorationRenderHelper.getCSSTextForModelDecorationGlyphMarginClassName(themedOpts.dark) + } ); this.isWholeLine = Boolean(options.isWholeLine); @@ -104,12 +214,28 @@ class DecorationRenderOptions implements IModelDecorationOptions { } } + public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions { + if (!writable) { + return this; + } + return { + inlineClassName: this.inlineClassName, + className: this.className, + glyphMarginClassName: this.glyphMarginClassName, + isWholeLine: this.isWholeLine, + overviewRuler: this.overviewRuler, + stickiness: this.stickiness + }; + } + public dispose(): void { dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.Light, this._key), this._styleSheet); dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.Dark, this._key), this._styleSheet); dom.removeCSSRulesWithPrefix(CSSNameHelper.getDeletionPrefixFor(ThemeType.HighContrastBlack, this._key), this._styleSheet); } +} +class DecorationRenderHelper { private static _CSS_MAP = { color: 'color:{0} !important;', backgroundColor: 'background-color:{0};', @@ -129,12 +255,17 @@ class DecorationRenderOptions implements IModelDecorationOptions { letterSpacing: 'letter-spacing:{0};', gutterIconPath: 'background:url(\'{0}\') center center no-repeat;', + + content: 'content:{0};', + margin: 'margin:{0};', + width: 'width:{0};', + height: 'height:{0};' }; /** * Build the CSS for decorations styled via `className`. */ - private static _getCSSTextForModelDecorationClassName(opts:IThemeDecorationRenderOptions): string { + public static getCSSTextForModelDecorationClassName(opts:IThemeDecorationRenderOptions): string { let cssTextArr = []; if (typeof opts.backgroundColor !== 'undefined') { @@ -183,7 +314,7 @@ class DecorationRenderOptions implements IModelDecorationOptions { /** * Build the CSS for decorations styled via `inlineClassName`. */ - private static _getCSSTextForModelDecorationInlineClassName(opts:IThemeDecorationRenderOptions): string { + public static getCSSTextForModelDecorationInlineClassName(opts:IThemeDecorationRenderOptions): string { let cssTextArr = []; if (typeof opts.textDecoration !== 'undefined') { @@ -202,10 +333,74 @@ class DecorationRenderOptions implements IModelDecorationOptions { return cssTextArr.join(''); } + /** + * Build the CSS for decorations styled before or after content. + */ + public static getCSSTextForModelDecorationContentClassName(opts:IContentDecorationRenderOptions): string { + let cssTextArr = []; + + if (typeof opts !== 'undefined') { + if ( + typeof opts.borderColor !== 'undefined' + || typeof opts.borderRadius !== 'undefined' + || typeof opts.borderSpacing !== 'undefined' + || typeof opts.borderStyle !== 'undefined' + || typeof opts.borderWidth !== 'undefined' + ) { + cssTextArr.push(strings.format('box-sizing: border-box;')); + } + + if (typeof opts.borderColor !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.borderColor, opts.borderColor)); + } + if (typeof opts.borderRadius !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.borderRadius, opts.borderRadius)); + } + if (typeof opts.borderSpacing !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.borderSpacing, opts.borderSpacing)); + } + if (typeof opts.borderStyle !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.borderStyle, opts.borderStyle)); + } + if (typeof opts.borderWidth !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.borderWidth, opts.borderWidth)); + } + if (typeof opts.content !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.content, opts.content)); + } + if (typeof opts.textDecoration !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.textDecoration, opts.textDecoration)); + } + if (typeof opts.color !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.color, opts.color)); + } + if (typeof opts.backgroundColor !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.backgroundColor, opts.backgroundColor)); + } + if (typeof opts.margin !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.margin, opts.margin)); + } + let setInlineBlock = false; + if (typeof opts.width !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.width, opts.width)); + setInlineBlock = true; + } + if (typeof opts.height !== 'undefined') { + cssTextArr.push(strings.format(this._CSS_MAP.height, opts.height)); + setInlineBlock = true; + } + if (setInlineBlock) { + cssTextArr.push('display:inline-block;'); + } + } + + return cssTextArr.join(''); + } + /** * Build the CSS for decorations styled via `glpyhMarginClassName`. */ - private static _getCSSTextForModelDecorationGlyphMarginClassName(opts:IThemeDecorationRenderOptions): string { + public static getCSSTextForModelDecorationGlyphMarginClassName(opts:IThemeDecorationRenderOptions): string { let cssTextArr = []; if (typeof opts.gutterIconPath !== 'undefined') { @@ -215,22 +410,32 @@ class DecorationRenderOptions implements IModelDecorationOptions { return cssTextArr.join(''); } - private static _handle(styleSheet: HTMLStyleElement, key:string, ruleType:ModelDecorationCSSRuleType, lightCSS:string, darkCSS:string): string { - if (lightCSS.length > 0 || darkCSS.length > 0) { - if (lightCSS.length > 0) { - this._createCSSSelector(styleSheet, ThemeType.Light, key, ruleType, lightCSS); - } - if (darkCSS.length > 0) { - this._createCSSSelector(styleSheet, ThemeType.Dark, key, ruleType, darkCSS); - this._createCSSSelector(styleSheet, ThemeType.HighContrastBlack, key, ruleType, darkCSS); - } - return CSSNameHelper.getClassName(key, ruleType); + public static handle(styleSheet: HTMLStyleElement, key:string, parentKey: string, ruleType:ModelDecorationCSSRuleType, ...ruleSets: {selector?: string, light:string, dark:string}[]): string { + function createCSSSelector(themeType:ThemeType, pseudoSelector: string, cssText:string) { + let selector = CSSNameHelper.getSelector(themeType, key, parentKey, ruleType, pseudoSelector); + dom.createCSSRule(selector, cssText, styleSheet); } - return undefined; - } - private static _createCSSSelector(styleSheet: HTMLStyleElement, themeType:ThemeType, key:string, ruleType:ModelDecorationCSSRuleType, cssText:string): void { - dom.createCSSRule(CSSNameHelper.getSelector(themeType, key, ruleType), cssText, styleSheet); + let hasContent = false; + for (let ruleSet of ruleSets) { + if (ruleSet.light.length > 0) { + createCSSSelector(ThemeType.Light, ruleSet.selector, ruleSet.light); + hasContent = true; + } + if (ruleSet.dark.length > 0) { + createCSSSelector(ThemeType.Dark, ruleSet.selector, ruleSet.dark); + createCSSSelector(ThemeType.HighContrastBlack, ruleSet.selector, ruleSet.dark); + hasContent = true; + } + } + if (hasContent) { + let className = CSSNameHelper.getClassName(key, ruleType); + if (parentKey) { + className = className + ' ' + CSSNameHelper.getClassName(parentKey, ruleType); + } + return className; + } + return void 0; } } @@ -244,6 +449,7 @@ enum ModelDecorationCSSRuleType { InlineClassName = 1, GlyphMarginClassName = 2 } + class CSSNameHelper { private static _getSelectorPrefixOf(theme:ThemeType): string { @@ -260,8 +466,15 @@ class CSSNameHelper { return 'ced-' + key + '-' + type; } - public static getSelector(themeType:ThemeType, key:string, ruleType:ModelDecorationCSSRuleType): string { - return this._getSelectorPrefixOf(themeType) + ' .' + this.getClassName(key, ruleType); + public static getSelector(themeType:ThemeType, key:string, parentKey: string, ruleType:ModelDecorationCSSRuleType, pseudoSelector:string): string { + let selector = this._getSelectorPrefixOf(themeType) + ' .' + this.getClassName(key, ruleType); + if (parentKey) { + selector = selector + '.' + this.getClassName(parentKey, ruleType); + } + if (pseudoSelector) { + selector = selector + pseudoSelector; + } + return selector; } public static getDeletionPrefixFor(themeType:ThemeType, key:string): string { @@ -274,11 +487,11 @@ interface IResolvedDecorationRenderOptions { light: IThemeDecorationRenderOptions; dark: IThemeDecorationRenderOptions; } -function resolveDecorationRenderOptions(opts:IDecorationRenderOptions): IResolvedDecorationRenderOptions { - var light = objects.deepClone(opts); +function getThemedRenderOptions(opts:{light?:T, dark?:T}): {light?:T, dark?:T} { + var light = objects.deepClone(opts); objects.mixin(light, opts.light); - var dark = objects.deepClone(opts); + var dark = objects.deepClone(opts); objects.mixin(dark, opts.dark); return { diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index f8b27640ab6..d7dce35a383 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -8,7 +8,6 @@ import {IAction, IActionProvider, isAction} from 'vs/base/common/actions'; import {onUnexpectedError} from 'vs/base/common/errors'; import {EventEmitter, IEventEmitter} from 'vs/base/common/eventEmitter'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; -import * as objects from 'vs/base/common/objects'; import * as timer from 'vs/base/common/timer'; import {TPromise} from 'vs/base/common/winjs.base'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; @@ -30,6 +29,7 @@ import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService'; import {CharacterHardWrappingLineMapperFactory} from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; import {SplitLinesCollection} from 'vs/editor/common/viewModel/splitLinesCollection'; import {ViewModel} from 'vs/editor/common/viewModel/viewModelImpl'; +import {hash} from 'vs/base/common/hash'; var EDITOR_ID = 0; @@ -103,7 +103,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr protected _instantiationService: IInstantiationService; protected _keybindingService: IKeybindingService; - private _decorationTypeKeysToIds: {[decorationTypeKey:string]:string[];}; + private _decorationTypeKeysToIds: {[decorationTypeKey:string]:{ [subtype:string]:string[]}}; private _codeEditorService: ICodeEditorService; private _editorIdContextKey: IKeybindingContextKey; @@ -651,28 +651,68 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr return this.model.deltaDecorations(oldDecorations, newDecorations, this.id); } - public setDecorations(decorationTypeKey: string, ranges:editorCommon.IRangeWithMessage[]): void { - var opts = this._codeEditorService.resolveDecorationType(decorationTypeKey); - var oldDecorationIds = this._decorationTypeKeysToIds[decorationTypeKey] || []; - this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationIds, ranges.map((r) : editorCommon.IModelDeltaDecoration => { - let decOpts: editorCommon.IModelDecorationOptions; - if (r.hoverMessage) { - // TODO@Alex: avoid objects.clone - decOpts = objects.clone(opts); - decOpts.htmlMessage = r.hoverMessage; - } else { - decOpts = opts; + public setDecorations(decorationTypeKey: string, decorationOptions:editorCommon.IDecorationOptions[]): void { + + let oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey]; + if (!oldDecorationsIds) { + oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey] = {}; + } + + // group all decoration options by render options. Each group is identified by a subType id. The subtype id is empty if + // there are no custom render options. + let decorationOptionsBySubTypes :{[key:string]: editorCommon.IDecorationOptions[]} = {}; + for (let decorationOption of decorationOptions) { + let subKey = decorationOption.renderOptions ? hash(decorationOption.renderOptions).toString(16) : ''; + let subTypeDecorationOptions = decorationOptionsBySubTypes[subKey]; + if (!subTypeDecorationOptions) { + decorationOptionsBySubTypes[subKey] = subTypeDecorationOptions = []; } - return { - range: r.range, - options: decOpts - }; - })); + subTypeDecorationOptions.push(decorationOption); + } + + // for each sub type create decorations. For custom render options register a decoration type if necessary + for (let subType in decorationOptionsBySubTypes) { + let decorationOptions = decorationOptionsBySubTypes[subType]; + let typeKey = decorationTypeKey; + let oldIds = oldDecorationsIds[subType] || []; + + if (subType.length > 0 && oldIds.length === 0) { + // custom render options: decoration type did not exist before, register new one + typeKey = decorationTypeKey + '-' + subType; + this._codeEditorService.registerDecorationSubType(typeKey, decorationTypeKey, decorationOptions[0].renderOptions); + } + + let modelDecorations: editorCommon.IModelDeltaDecoration[] = decorationOptions.map(r => { + let opts = this._codeEditorService.resolveDecorationOptions(typeKey, !!r.hoverMessage); + if (r.hoverMessage) { + opts.htmlMessage = r.hoverMessage; + } + return { + range: r.range, + options: opts + }; + }); + + oldDecorationsIds[subType] = this.deltaDecorations(oldIds, modelDecorations); + } + + // remove decorations that are no longer used, deregister decoration type if necessary + for (let subType in oldDecorationsIds) { + if (!decorationOptionsBySubTypes.hasOwnProperty(subType)) { + delete oldDecorationsIds[subType]; + if (subType.length > 0) { // custom render options: unregister decoration type + this._codeEditorService.removeDecorationType(decorationTypeKey + '-' + subType); + } + } + } } public removeDecorations(decorationTypeKey: string): void { - if (this._decorationTypeKeysToIds.hasOwnProperty(decorationTypeKey)) { - this.deltaDecorations(this._decorationTypeKeysToIds[decorationTypeKey], []); + let decorationIdsBySubType = this._decorationTypeKeysToIds[decorationTypeKey]; + if (decorationIdsBySubType) { + for (let subType in decorationIdsBySubType) { + this.deltaDecorations(decorationIdsBySubType[subType], []); + } delete this._decorationTypeKeysToIds[decorationTypeKey]; } } @@ -959,6 +999,20 @@ export abstract class CommonCodeEditor extends EventEmitter implements IActionPr this.viewModel = null; } + if (this._decorationTypeKeysToIds) { + for (let decorationType in this._decorationTypeKeysToIds) { + let decorationIdsBySubType = this._decorationTypeKeysToIds[decorationType]; + if (decorationIdsBySubType) { + for (let subType in decorationIdsBySubType) { + if (subType.length > 0) { + this._codeEditorService.removeDecorationType(decorationType + '-' + subType); + } + } + } + } + } + + var result = this.model; this.model = null; diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 762114792db..98d55fd571c 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -3385,6 +3385,26 @@ export interface IThemeDecorationRenderOptions { gutterIconPath?: string; overviewRulerColor?: string; + + before?: IContentDecorationRenderOptions; + after?: IContentDecorationRenderOptions; +} + +export interface IContentDecorationRenderOptions { + borderStyle?: string; + borderWidth?: string; + borderRadius?: string; + borderSpacing?: string; + borderColor?: string; + + content?: string; + textDecoration?: string; + + color?: string; + backgroundColor?: string; + margin?: string; + width?: string; + height?: string; } /** @@ -3398,14 +3418,30 @@ export interface IDecorationRenderOptions extends IThemeDecorationRenderOptions dark?: IThemeDecorationRenderOptions; } +export interface IThemeDecorationInstanceRenderOptions { + before?: { + content?: string; + }; + after?: { + content?: string; + }; +} + +export interface IDecorationInstanceRenderOptions extends IThemeDecorationInstanceRenderOptions { + light?: IThemeDecorationInstanceRenderOptions; + dark?: IThemeDecorationInstanceRenderOptions; +} + /** * @internal */ -export interface IRangeWithMessage { +export interface IDecorationOptions { range: IRange; hoverMessage?: IHTMLContentElement[]; + renderOptions? : IDecorationInstanceRenderOptions; } + export interface ICommonCodeEditor extends IEditor { onDidChangeModel(listener: (e:IModelChangedEvent)=>void): IDisposable; @@ -3536,7 +3572,7 @@ export interface ICommonCodeEditor extends IEditor { /** * @internal */ - setDecorations(decorationTypeKey: string, ranges:IRangeWithMessage[]): void; + setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void; /** * @internal diff --git a/src/vs/editor/common/services/abstractCodeEditorService.ts b/src/vs/editor/common/services/abstractCodeEditorService.ts index 59287476590..7448c4094e2 100644 --- a/src/vs/editor/common/services/abstractCodeEditorService.ts +++ b/src/vs/editor/common/services/abstractCodeEditorService.ts @@ -5,7 +5,7 @@ 'use strict'; import Event, {Emitter} from 'vs/base/common/event'; -import {ICommonCodeEditor, IDecorationRenderOptions, IModelDecorationOptions} from 'vs/editor/common/editorCommon'; +import {ICommonCodeEditor, IDecorationRenderOptions, IModelDecorationOptions, IDecorationInstanceRenderOptions} from 'vs/editor/common/editorCommon'; import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService'; export abstract class AbstractCodeEditorService implements ICodeEditorService { @@ -70,6 +70,7 @@ export abstract class AbstractCodeEditorService implements ICodeEditorService { } public abstract registerDecorationType(key:string, options: IDecorationRenderOptions): void; + public abstract registerDecorationSubType(key:string, parentKey:string, options: IDecorationInstanceRenderOptions): void; public abstract removeDecorationType(key:string): void; - public abstract resolveDecorationType(key:string): IModelDecorationOptions; + public abstract resolveDecorationOptions(decorationTypeKey:string, writable: boolean): IModelDecorationOptions; } diff --git a/src/vs/editor/common/services/codeEditorService.ts b/src/vs/editor/common/services/codeEditorService.ts index f9062a00b89..f222213d3df 100644 --- a/src/vs/editor/common/services/codeEditorService.ts +++ b/src/vs/editor/common/services/codeEditorService.ts @@ -6,7 +6,7 @@ import Event from 'vs/base/common/event'; import {ServiceIdentifier, createDecorator} from 'vs/platform/instantiation/common/instantiation'; -import {ICommonCodeEditor, IDecorationRenderOptions, IModelDecorationOptions} from 'vs/editor/common/editorCommon'; +import {ICommonCodeEditor, IDecorationRenderOptions, IModelDecorationOptions, IDecorationInstanceRenderOptions} from 'vs/editor/common/editorCommon'; export var ID_CODE_EDITOR_SERVICE = 'codeEditorService'; export var ICodeEditorService = createDecorator(ID_CODE_EDITOR_SERVICE); @@ -30,6 +30,7 @@ export interface ICodeEditorService { getFocusedCodeEditor(): ICommonCodeEditor; registerDecorationType(key:string, options: IDecorationRenderOptions): void; + registerDecorationSubType(key:string, parentTypeKey: string, options: IDecorationInstanceRenderOptions): void; removeDecorationType(key:string): void; - resolveDecorationType(key:string): IModelDecorationOptions; + resolveDecorationOptions(typeKey:string, writable: boolean): IModelDecorationOptions; } diff --git a/src/vs/editor/test/common/mocks/mockCodeEditorService.ts b/src/vs/editor/test/common/mocks/mockCodeEditorService.ts index 126bb4ffaf1..8a21597a0d1 100644 --- a/src/vs/editor/test/common/mocks/mockCodeEditorService.ts +++ b/src/vs/editor/test/common/mocks/mockCodeEditorService.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import {IDecorationRenderOptions, IModelDecorationOptions} from 'vs/editor/common/editorCommon'; +import {IDecorationRenderOptions, IModelDecorationOptions, IDecorationInstanceRenderOptions} from 'vs/editor/common/editorCommon'; import {AbstractCodeEditorService} from 'vs/editor/common/services/abstractCodeEditorService'; export class MockCodeEditorService extends AbstractCodeEditorService { public registerDecorationType(key:string, options: IDecorationRenderOptions): void { } + public registerDecorationSubType(key:string, parentKey:string, options: IDecorationInstanceRenderOptions): void {} public removeDecorationType(key:string): void { } - public resolveDecorationType(key:string): IModelDecorationOptions { return null; } + public resolveDecorationOptions(decorationTypeKey:string, writable: boolean): IModelDecorationOptions { return null; } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 32867f1b486..827a661aa9b 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -697,6 +697,31 @@ declare namespace vscode { * The color of the decoration in the overview ruler. Use rgba() and define transparent colors to play well with other decorations. */ overviewRulerColor?: string; + + /** + * Defines the rendering options of the attachment that is inserted before the decorated text + */ + before?: ThemableDecorationAttachmentRenderOptions; + + /** + * Defines the rendering options of the attachment that is inserted after the decorated text + */ + after?: ThemableDecorationAttachmentRenderOptions; + } + + export interface ThemableDecorationAttachmentRenderOptions { + borderStyle?: string; + borderWidth?: string; + borderRadius?: string; + borderSpacing?: string; + borderColor?: string; + content?: string; + textDecoration?: string; + color?: string; + backgroundColor?: string; + margin?: string; + width?: string; + height?: string; } /** @@ -739,6 +764,42 @@ declare namespace vscode { * A message that should be rendered when hovering over the decoration. */ hoverMessage: MarkedString | MarkedString[]; + + /** + * Render options applied to the current decoration. For performance reasons, keep the + * number of decoration specific options small, and use decoration types whereever possible. + */ + renderOptions?: DecorationInstanceRenderOptions; + } + + export interface ThemableDecorationInstanceAttachmentRenderOptions { + content?: string; + color?: string; + backgroundColor?: string; + } + + export interface ThemableDecorationInstanceRenderOptions { + /** + * Defines the rendering options of the attachment that is inserted before the decorated text + */ + before?: ThemableDecorationInstanceAttachmentRenderOptions; + + /** + * Defines the rendering options of the attachment that is inserted after the decorated text + */ + after?: ThemableDecorationInstanceAttachmentRenderOptions; + } + + export interface DecorationInstanceRenderOptions extends ThemableDecorationInstanceRenderOptions { + /** + * Overwrite options for light themes. + */ + light?: ThemableDecorationInstanceRenderOptions; + + /** + * Overwrite options for dark themes. + */ + dark?: ThemableDecorationInstanceRenderOptions } /** diff --git a/src/vs/workbench/api/node/extHostEditors.ts b/src/vs/workbench/api/node/extHostEditors.ts index 83f0ebc8b51..4037c81f285 100644 --- a/src/vs/workbench/api/node/extHostEditors.ts +++ b/src/vs/workbench/api/node/extHostEditors.ts @@ -13,7 +13,7 @@ import {TPromise} from 'vs/base/common/winjs.base'; import {Remotable, IThreadService} from 'vs/platform/thread/common/thread'; import {ExtHostModelService, ExtHostDocumentData} from 'vs/workbench/api/node/extHostDocuments'; import {Selection, Range, Position, EditorOptions, EndOfLine} from './extHostTypes'; -import {ISingleEditOperation, ISelection, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IRangeWithMessage} from 'vs/editor/common/editorCommon'; +import {ISingleEditOperation, ISelection, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IDecorationOptions} from 'vs/editor/common/editorCommon'; import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; @@ -689,7 +689,7 @@ export class MainThreadEditors { return TPromise.as(null); } - _trySetDecorations(id: string, key: string, ranges: IRangeWithMessage[]): TPromise { + _trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): TPromise { if (!this._textEditorsMap[id]) { return TPromise.wrapError('TextEditor disposed'); } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 450db22f7c0..411ebad84eb 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -11,7 +11,7 @@ import {IDisposable} from 'vs/base/common/lifecycle'; import * as modes from 'vs/editor/common/modes'; import * as types from './extHostTypes'; import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; -import {IPosition, ISelection, IRange, IRangeWithMessage, ISingleEditOperation} from 'vs/editor/common/editorCommon'; +import {IPosition, ISelection, IRange, IDecorationOptions, ISingleEditOperation} from 'vs/editor/common/editorCommon'; import {IHTMLContentElement} from 'vs/base/common/htmlContent'; import {ITypeBearing} from 'vs/workbench/parts/search/common/search'; import * as vscode from 'vscode'; @@ -155,27 +155,28 @@ function fromMarkedStringOrMarkedStringArr(something: vscode.MarkedString | vsco } } -function isRangeWithMessage(something: any): something is vscode.DecorationOptions { - return (typeof something.range !== 'undefined'); +function isDecorationOptions(something: any): something is vscode.DecorationOptions { + return (typeof something.range !== 'undefined') || (typeof something.after !== 'undefined') || (typeof something.before !== 'undefined'); } -function isRangeWithMessageArr(something: vscode.Range[]|vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { +function isDecorationOptionsArr(something: vscode.Range[]|vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { if (something.length === 0) { return true; } - return isRangeWithMessage(something[0]) ? true : false; + return isDecorationOptions(something[0]) ? true : false; } -export function fromRangeOrRangeWithMessage(ranges:vscode.Range[]|vscode.DecorationOptions[]): IRangeWithMessage[] { - if (isRangeWithMessageArr(ranges)) { - return ranges.map((r): IRangeWithMessage => { +export function fromRangeOrRangeWithMessage(ranges:vscode.Range[]|vscode.DecorationOptions[]): IDecorationOptions[] { + if (isDecorationOptionsArr(ranges)) { + return ranges.map((r): IDecorationOptions => { return { range: fromRange(r.range), - hoverMessage: fromMarkedStringOrMarkedStringArr(r.hoverMessage) + hoverMessage: fromMarkedStringOrMarkedStringArr(r.hoverMessage), + renderOptions: r.renderOptions }; }); } else { - return ranges.map((r): IRangeWithMessage => { + return ranges.map((r): IDecorationOptions => { return { range: fromRange(r) }; diff --git a/src/vs/workbench/api/node/mainThreadEditors.ts b/src/vs/workbench/api/node/mainThreadEditors.ts index e60d6e79491..32f317a4832 100644 --- a/src/vs/workbench/api/node/mainThreadEditors.ts +++ b/src/vs/workbench/api/node/mainThreadEditors.ts @@ -241,7 +241,7 @@ export class MainThreadTextEditor { } } - public setDecorations(key: string, ranges:EditorCommon.IRangeWithMessage[]): void { + public setDecorations(key: string, ranges:EditorCommon.IDecorationOptions[]): void { if (!this._codeEditor) { console.warn('setDecorations on invisible editor'); return;