SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEditorContributionCtor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { EditorAction, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
import { EditorBrowserRegistry } from 'vs/editor/browser/editorBrowserExtensions';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class CodeEditor extends CodeEditorWidget {
constructor(
domElement: HTMLElement,
options: IEditorOptions,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService
) {
super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService, themeService);
}
protected _getContributions(): IEditorContributionCtor[] {
return [].concat(EditorBrowserRegistry.getEditorContributions()).concat(CommonEditorRegistry.getEditorContributions());
}
protected _getActions(): EditorAction[] {
return CommonEditorRegistry.getEditorActions();
}
}

View File

@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
export const enum CharWidthRequestType {
Regular = 0,
Italic = 1,
Bold = 2
}
export class CharWidthRequest {
public readonly chr: string;
public readonly type: CharWidthRequestType;
public width: number;
constructor(chr: string, type: CharWidthRequestType) {
this.chr = chr;
this.type = type;
this.width = 0;
}
public fulfill(width: number) {
this.width = width;
}
}
interface ICharWidthReader {
read(): void;
}
class DomCharWidthReader implements ICharWidthReader {
private readonly _bareFontInfo: BareFontInfo;
private readonly _requests: CharWidthRequest[];
private _container: HTMLElement;
private _testElements: HTMLSpanElement[];
constructor(bareFontInfo: BareFontInfo, requests: CharWidthRequest[]) {
this._bareFontInfo = bareFontInfo;
this._requests = requests;
this._container = null;
this._testElements = null;
}
public read(): void {
// Create a test container with all these test elements
this._createDomElements();
// Add the container to the DOM
document.body.appendChild(this._container);
// Read character widths
this._readFromDomElements();
// Remove the container from the DOM
document.body.removeChild(this._container);
this._container = null;
this._testElements = null;
}
private _createDomElements(): void {
let container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '-50000px';
container.style.width = '50000px';
let regularDomNode = document.createElement('div');
regularDomNode.style.fontFamily = this._bareFontInfo.fontFamily;
regularDomNode.style.fontWeight = this._bareFontInfo.fontWeight;
regularDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px';
regularDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px';
regularDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px';
container.appendChild(regularDomNode);
let boldDomNode = document.createElement('div');
boldDomNode.style.fontFamily = this._bareFontInfo.fontFamily;
boldDomNode.style.fontWeight = 'bold';
boldDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px';
boldDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px';
boldDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px';
container.appendChild(boldDomNode);
let italicDomNode = document.createElement('div');
italicDomNode.style.fontFamily = this._bareFontInfo.fontFamily;
italicDomNode.style.fontWeight = this._bareFontInfo.fontWeight;
italicDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px';
italicDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px';
italicDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px';
italicDomNode.style.fontStyle = 'italic';
container.appendChild(italicDomNode);
let testElements: HTMLSpanElement[] = [];
for (let i = 0, len = this._requests.length; i < len; i++) {
const request = this._requests[i];
let parent: HTMLElement;
if (request.type === CharWidthRequestType.Regular) {
parent = regularDomNode;
}
if (request.type === CharWidthRequestType.Bold) {
parent = boldDomNode;
}
if (request.type === CharWidthRequestType.Italic) {
parent = italicDomNode;
}
parent.appendChild(document.createElement('br'));
let testElement = document.createElement('span');
DomCharWidthReader._render(testElement, request);
parent.appendChild(testElement);
testElements[i] = testElement;
}
this._container = container;
this._testElements = testElements;
}
private static _render(testElement: HTMLElement, request: CharWidthRequest): void {
if (request.chr === ' ') {
let htmlString = '&nbsp;';
// Repeat character 256 (2^8) times
for (let i = 0; i < 8; i++) {
htmlString += htmlString;
}
testElement.innerHTML = htmlString;
} else {
let testString = request.chr;
// Repeat character 256 (2^8) times
for (let i = 0; i < 8; i++) {
testString += testString;
}
testElement.textContent = testString;
}
}
private _readFromDomElements(): void {
for (let i = 0, len = this._requests.length; i < len; i++) {
const request = this._requests[i];
const testElement = this._testElements[i];
request.fulfill(testElement.offsetWidth / 256);
}
}
}
export function readCharWidths(bareFontInfo: BareFontInfo, requests: CharWidthRequest[]): void {
let reader = new DomCharWidthReader(bareFontInfo, requests);
reader.read();
}

View File

@@ -0,0 +1,364 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import Event, { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { CommonEditorConfiguration, IEnvConfiguration } from 'vs/editor/common/config/commonEditorConfig';
import { IDimension } from 'vs/editor/common/editorCommon';
import { FontInfo, BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { FastDomNode } from 'vs/base/browser/fastDomNode';
import { CharWidthRequest, CharWidthRequestType, readCharWidths } from 'vs/editor/browser/config/charWidthReader';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
class CSSBasedConfigurationCache {
private _keys: { [key: string]: BareFontInfo; };
private _values: { [key: string]: FontInfo; };
constructor() {
this._keys = Object.create(null);
this._values = Object.create(null);
}
public has(item: BareFontInfo): boolean {
let itemId = item.getId();
return !!this._values[itemId];
}
public get(item: BareFontInfo): FontInfo {
let itemId = item.getId();
return this._values[itemId];
}
public put(item: BareFontInfo, value: FontInfo): void {
let itemId = item.getId();
this._keys[itemId] = item;
this._values[itemId] = value;
}
public remove(item: BareFontInfo): void {
let itemId = item.getId();
delete this._keys[itemId];
delete this._values[itemId];
}
public getKeys(): BareFontInfo[] {
return Object.keys(this._keys).map(id => this._keys[id]);
}
public getValues(): FontInfo[] {
return Object.keys(this._keys).map(id => this._values[id]);
}
}
export function readFontInfo(bareFontInfo: BareFontInfo): FontInfo {
return CSSBasedConfiguration.INSTANCE.readConfiguration(bareFontInfo);
}
export function restoreFontInfo(storageService: IStorageService): void {
let strStoredFontInfo = storageService.get('editorFontInfo', StorageScope.GLOBAL);
if (typeof strStoredFontInfo !== 'string') {
return;
}
let storedFontInfo: ISerializedFontInfo[] = null;
try {
storedFontInfo = JSON.parse(strStoredFontInfo);
} catch (err) {
return;
}
if (!Array.isArray(storedFontInfo)) {
return;
}
CSSBasedConfiguration.INSTANCE.restoreFontInfo(storedFontInfo);
}
export function saveFontInfo(storageService: IStorageService): void {
let knownFontInfo = CSSBasedConfiguration.INSTANCE.saveFontInfo();
storageService.store('editorFontInfo', JSON.stringify(knownFontInfo), StorageScope.GLOBAL);
}
export interface ISerializedFontInfo {
readonly zoomLevel: number;
readonly fontFamily: string;
readonly fontWeight: string;
readonly fontSize: number;
readonly lineHeight: number;
readonly letterSpacing: number;
readonly isMonospace: boolean;
readonly typicalHalfwidthCharacterWidth: number;
readonly typicalFullwidthCharacterWidth: number;
readonly spaceWidth: number;
readonly maxDigitWidth: number;
}
class CSSBasedConfiguration extends Disposable {
public static INSTANCE = new CSSBasedConfiguration();
private _cache: CSSBasedConfigurationCache;
private _evictUntrustedReadingsTimeout: number;
private _onDidChange = this._register(new Emitter<void>());
public onDidChange: Event<void> = this._onDidChange.event;
constructor() {
super();
this._cache = new CSSBasedConfigurationCache();
this._evictUntrustedReadingsTimeout = -1;
}
public dispose(): void {
if (this._evictUntrustedReadingsTimeout !== -1) {
clearTimeout(this._evictUntrustedReadingsTimeout);
this._evictUntrustedReadingsTimeout = -1;
}
super.dispose();
}
private _writeToCache(item: BareFontInfo, value: FontInfo): void {
this._cache.put(item, value);
if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) {
// Try reading again after some time
this._evictUntrustedReadingsTimeout = setTimeout(() => {
this._evictUntrustedReadingsTimeout = -1;
this._evictUntrustedReadings();
}, 5000);
}
}
private _evictUntrustedReadings(): void {
let values = this._cache.getValues();
let somethingRemoved = false;
for (let i = 0, len = values.length; i < len; i++) {
let item = values[i];
if (!item.isTrusted) {
somethingRemoved = true;
this._cache.remove(item);
}
}
if (somethingRemoved) {
this._onDidChange.fire();
}
}
public saveFontInfo(): ISerializedFontInfo[] {
// Only save trusted font info (that has been measured in this running instance)
return this._cache.getValues().filter(item => item.isTrusted);
}
public restoreFontInfo(savedFontInfo: ISerializedFontInfo[]): void {
// Take all the saved font info and insert them in the cache without the trusted flag.
// The reason for this is that a font might have been installed on the OS in the meantime.
for (let i = 0, len = savedFontInfo.length; i < len; i++) {
let fontInfo = new FontInfo(savedFontInfo[i], false);
this._writeToCache(fontInfo, fontInfo);
}
}
public readConfiguration(bareFontInfo: BareFontInfo): FontInfo {
if (!this._cache.has(bareFontInfo)) {
let readConfig = CSSBasedConfiguration._actualReadConfiguration(bareFontInfo);
if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) {
// Hey, it's Bug 14341 ... we couldn't read
readConfig = new FontInfo({
zoomLevel: browser.getZoomLevel(),
fontFamily: readConfig.fontFamily,
fontWeight: readConfig.fontWeight,
fontSize: readConfig.fontSize,
lineHeight: readConfig.lineHeight,
letterSpacing: readConfig.letterSpacing,
isMonospace: readConfig.isMonospace,
typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5),
typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5),
spaceWidth: Math.max(readConfig.spaceWidth, 5),
maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5),
}, false);
}
this._writeToCache(bareFontInfo, readConfig);
}
return this._cache.get(bareFontInfo);
}
private static createRequest(chr: string, type: CharWidthRequestType, all: CharWidthRequest[], monospace: CharWidthRequest[]): CharWidthRequest {
let result = new CharWidthRequest(chr, type);
all.push(result);
if (monospace) {
monospace.push(result);
}
return result;
}
private static _actualReadConfiguration(bareFontInfo: BareFontInfo): FontInfo {
let all: CharWidthRequest[] = [];
let monospace: CharWidthRequest[] = [];
const typicalHalfwidthCharacter = this.createRequest('n', CharWidthRequestType.Regular, all, monospace);
const typicalFullwidthCharacter = this.createRequest('\uff4d', CharWidthRequestType.Regular, all, null);
const space = this.createRequest(' ', CharWidthRequestType.Regular, all, monospace);
const digit0 = this.createRequest('0', CharWidthRequestType.Regular, all, monospace);
const digit1 = this.createRequest('1', CharWidthRequestType.Regular, all, monospace);
const digit2 = this.createRequest('2', CharWidthRequestType.Regular, all, monospace);
const digit3 = this.createRequest('3', CharWidthRequestType.Regular, all, monospace);
const digit4 = this.createRequest('4', CharWidthRequestType.Regular, all, monospace);
const digit5 = this.createRequest('5', CharWidthRequestType.Regular, all, monospace);
const digit6 = this.createRequest('6', CharWidthRequestType.Regular, all, monospace);
const digit7 = this.createRequest('7', CharWidthRequestType.Regular, all, monospace);
const digit8 = this.createRequest('8', CharWidthRequestType.Regular, all, monospace);
const digit9 = this.createRequest('9', CharWidthRequestType.Regular, all, monospace);
// monospace test: used for whitespace rendering
this.createRequest('→', CharWidthRequestType.Regular, all, monospace);
this.createRequest('·', CharWidthRequestType.Regular, all, monospace);
// monospace test: some characters
this.createRequest('|', CharWidthRequestType.Regular, all, monospace);
this.createRequest('/', CharWidthRequestType.Regular, all, monospace);
this.createRequest('-', CharWidthRequestType.Regular, all, monospace);
this.createRequest('_', CharWidthRequestType.Regular, all, monospace);
this.createRequest('i', CharWidthRequestType.Regular, all, monospace);
this.createRequest('l', CharWidthRequestType.Regular, all, monospace);
this.createRequest('m', CharWidthRequestType.Regular, all, monospace);
// monospace italic test
this.createRequest('|', CharWidthRequestType.Italic, all, monospace);
this.createRequest('_', CharWidthRequestType.Italic, all, monospace);
this.createRequest('i', CharWidthRequestType.Italic, all, monospace);
this.createRequest('l', CharWidthRequestType.Italic, all, monospace);
this.createRequest('m', CharWidthRequestType.Italic, all, monospace);
this.createRequest('n', CharWidthRequestType.Italic, all, monospace);
// monospace bold test
this.createRequest('|', CharWidthRequestType.Bold, all, monospace);
this.createRequest('_', CharWidthRequestType.Bold, all, monospace);
this.createRequest('i', CharWidthRequestType.Bold, all, monospace);
this.createRequest('l', CharWidthRequestType.Bold, all, monospace);
this.createRequest('m', CharWidthRequestType.Bold, all, monospace);
this.createRequest('n', CharWidthRequestType.Bold, all, monospace);
readCharWidths(bareFontInfo, all);
const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width);
let isMonospace = true;
let referenceWidth = monospace[0].width;
for (let i = 1, len = monospace.length; i < len; i++) {
const diff = referenceWidth - monospace[i].width;
if (diff < -0.001 || diff > 0.001) {
isMonospace = false;
break;
}
}
// let's trust the zoom level only 2s after it was changed.
const canTrustBrowserZoomLevel = (browser.getTimeSinceLastZoomLevelChanged() > 2000);
return new FontInfo({
zoomLevel: browser.getZoomLevel(),
fontFamily: bareFontInfo.fontFamily,
fontWeight: bareFontInfo.fontWeight,
fontSize: bareFontInfo.fontSize,
lineHeight: bareFontInfo.lineHeight,
letterSpacing: bareFontInfo.letterSpacing,
isMonospace: isMonospace,
typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width,
typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width,
spaceWidth: space.width,
maxDigitWidth: maxDigitWidth
}, canTrustBrowserZoomLevel);
}
}
export class Configuration extends CommonEditorConfiguration {
public static applyFontInfoSlow(domNode: HTMLElement, fontInfo: BareFontInfo): void {
domNode.style.fontFamily = fontInfo.fontFamily;
domNode.style.fontWeight = fontInfo.fontWeight;
domNode.style.fontSize = fontInfo.fontSize + 'px';
domNode.style.lineHeight = fontInfo.lineHeight + 'px';
domNode.style.letterSpacing = fontInfo.letterSpacing + 'px';
}
public static applyFontInfo(domNode: FastDomNode<HTMLElement>, fontInfo: BareFontInfo): void {
domNode.setFontFamily(fontInfo.fontFamily);
domNode.setFontWeight(fontInfo.fontWeight);
domNode.setFontSize(fontInfo.fontSize);
domNode.setLineHeight(fontInfo.lineHeight);
domNode.setLetterSpacing(fontInfo.letterSpacing);
}
private readonly _elementSizeObserver: ElementSizeObserver;
constructor(options: IEditorOptions, referenceDomElement: HTMLElement = null) {
super(options);
this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, () => this._onReferenceDomElementSizeChanged()));
this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._onCSSBasedConfigurationChanged()));
if (this._validatedOptions.automaticLayout) {
this._elementSizeObserver.startObserving();
}
this._register(browser.onDidChangeZoomLevel(_ => this._recomputeOptions()));
this._register(browser.onDidChangeAccessibilitySupport(() => this._recomputeOptions()));
this._recomputeOptions();
}
private _onReferenceDomElementSizeChanged(): void {
this._recomputeOptions();
}
private _onCSSBasedConfigurationChanged(): void {
this._recomputeOptions();
}
public observeReferenceElement(dimension?: IDimension): void {
this._elementSizeObserver.observe(dimension);
}
public dispose(): void {
super.dispose();
}
private _getExtraEditorClassName(): string {
let extra = '';
if (browser.isIE) {
extra += 'ie ';
} else if (browser.isFirefox) {
extra += 'ff ';
} else if (browser.isEdge) {
extra += 'edge ';
}
if (platform.isMacintosh) {
extra += 'mac ';
}
return extra;
}
protected _getEnvConfiguration(): IEnvConfiguration {
return {
extraEditorClassName: this._getExtraEditorClassName(),
outerWidth: this._elementSizeObserver.getWidth(),
outerHeight: this._elementSizeObserver.getHeight(),
emptySelectionClipboard: browser.isWebKit,
pixelRatio: browser.getPixelRatio(),
zoomLevel: browser.getZoomLevel(),
accessibilitySupport: browser.getAccessibilitySupport()
};
}
protected readConfiguration(bareFontInfo: BareFontInfo): FontInfo {
return CSSBasedConfiguration.INSTANCE.readConfiguration(bareFontInfo);
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { IDimension } from 'vs/editor/common/editorCommon';
export class ElementSizeObserver extends Disposable {
private referenceDomElement: HTMLElement;
private measureReferenceDomElementToken: number;
private changeCallback: () => void;
private width: number;
private height: number;
constructor(referenceDomElement: HTMLElement, changeCallback: () => void) {
super();
this.referenceDomElement = referenceDomElement;
this.changeCallback = changeCallback;
this.measureReferenceDomElementToken = -1;
this.width = -1;
this.height = -1;
this.measureReferenceDomElement(false);
}
public dispose(): void {
this.stopObserving();
super.dispose();
}
public getWidth(): number {
return this.width;
}
public getHeight(): number {
return this.height;
}
public startObserving(): void {
if (this.measureReferenceDomElementToken === -1) {
this.measureReferenceDomElementToken = setInterval(() => this.measureReferenceDomElement(true), 100);
}
}
public stopObserving(): void {
if (this.measureReferenceDomElementToken !== -1) {
clearInterval(this.measureReferenceDomElementToken);
this.measureReferenceDomElementToken = -1;
}
}
public observe(dimension?: IDimension): void {
this.measureReferenceDomElement(true, dimension);
}
private measureReferenceDomElement(callChangeCallback: boolean, dimension?: IDimension): void {
let observedWidth = 0;
let observedHeight = 0;
if (dimension) {
observedWidth = dimension.width;
observedHeight = dimension.height;
} else if (this.referenceDomElement) {
observedWidth = this.referenceDomElement.clientWidth;
observedHeight = this.referenceDomElement.clientHeight;
}
observedWidth = Math.max(5, observedWidth);
observedHeight = Math.max(5, observedHeight);
if (this.width !== observedWidth || this.height !== observedHeight) {
this.width = observedWidth;
this.height = observedHeight;
if (callChangeCallback) {
this.changeCallback();
}
}
}
}

View File

@@ -0,0 +1,573 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { MouseTarget, MouseTargetFactory, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { TimeoutTimer, RunOnceScheduler } from 'vs/base/common/async';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
import { EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, EditorMouseEvent, createEditorPagePosition, ClientCoordinates } from 'vs/editor/browser/editorDom';
import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { EditorZoom } from 'vs/editor/common/config/editorZoom';
import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { ViewController } from 'vs/editor/browser/view/viewController';
/**
* Merges mouse events when mouse move events are throttled
*/
function createMouseMoveEventMerger(mouseTargetFactory: MouseTargetFactory) {
return function (lastEvent: EditorMouseEvent, currentEvent: EditorMouseEvent): EditorMouseEvent {
let targetIsWidget = false;
if (mouseTargetFactory) {
targetIsWidget = mouseTargetFactory.mouseTargetIsWidget(currentEvent);
}
if (!targetIsWidget) {
currentEvent.preventDefault();
}
return currentEvent;
};
}
export interface IPointerHandlerHelper {
viewDomNode: HTMLElement;
linesContentDomNode: HTMLElement;
focusTextArea(): void;
/**
* Get the last rendered information of the cursors.
*/
getLastViewCursorsRenderData(): IViewCursorRenderData[];
shouldSuppressMouseDownOnViewZone(viewZoneId: number): boolean;
shouldSuppressMouseDownOnWidget(widgetId: string): boolean;
/**
* Decode a position from a rendered dom node
*/
getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position;
visibleRangeForPosition2(lineNumber: number, column: number): HorizontalRange;
getLineWidth(lineNumber: number): number;
}
export class MouseHandler extends ViewEventHandler {
static MOUSE_MOVE_MINIMUM_TIME = 100; // ms
protected _context: ViewContext;
protected viewController: ViewController;
protected viewHelper: IPointerHandlerHelper;
protected mouseTargetFactory: MouseTargetFactory;
private _asyncFocus: RunOnceScheduler;
private _mouseDownOperation: MouseDownOperation;
private lastMouseLeaveTime: number;
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
super();
this._context = context;
this.viewController = viewController;
this.viewHelper = viewHelper;
this.mouseTargetFactory = new MouseTargetFactory(this._context, viewHelper);
this._mouseDownOperation = this._register(new MouseDownOperation(
this._context,
this.viewController,
this.viewHelper,
(e, testEventTarget) => this._createMouseTarget(e, testEventTarget),
(e) => this._getMouseColumn(e)
));
this._asyncFocus = this._register(new RunOnceScheduler(() => this.viewHelper.focusTextArea(), 0));
this.lastMouseLeaveTime = -1;
let mouseEvents = new EditorMouseEventFactory(this.viewHelper.viewDomNode);
this._register(mouseEvents.onContextMenu(this.viewHelper.viewDomNode, (e) => this._onContextMenu(e, true)));
this._register(mouseEvents.onMouseMoveThrottled(this.viewHelper.viewDomNode,
(e) => this._onMouseMove(e),
createMouseMoveEventMerger(this.mouseTargetFactory), MouseHandler.MOUSE_MOVE_MINIMUM_TIME));
this._register(mouseEvents.onMouseUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
this._register(mouseEvents.onMouseLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e)));
let onMouseWheel = (browserEvent: MouseWheelEvent) => {
if (!this._context.configuration.editor.viewInfo.mouseWheelZoom) {
return;
}
let e = new StandardMouseWheelEvent(browserEvent);
if (e.browserEvent.ctrlKey || e.browserEvent.metaKey) {
let zoomLevel: number = EditorZoom.getZoomLevel();
let delta = e.deltaY > 0 ? 1 : -1;
EditorZoom.setZoomLevel(zoomLevel + delta);
e.preventDefault();
e.stopPropagation();
}
};
this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, 'mousewheel', onMouseWheel, true));
this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, 'DOMMouseScroll', onMouseWheel, true));
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
super.dispose();
}
// --- begin event handlers
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._mouseDownOperation.onCursorStateChanged(e);
return false;
}
private _isFocused = false;
public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
this._isFocused = e.isFocused;
return false;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this._mouseDownOperation.onScrollChanged();
return false;
}
// --- end event handlers
public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget {
let clientPos = new ClientCoordinates(clientX, clientY);
let pos = clientPos.toPageCoordinates();
let editorPos = createEditorPagePosition(this.viewHelper.viewDomNode);
if (pos.y < editorPos.y || pos.y > editorPos.y + editorPos.height || pos.x < editorPos.x || pos.x > editorPos.x + editorPos.width) {
return null;
}
let lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData();
return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, editorPos, pos, null);
}
protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): editorBrowser.IMouseTarget {
let lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData();
return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, e.editorPos, e.pos, testEventTarget ? e.target : null);
}
private _getMouseColumn(e: EditorMouseEvent): number {
return this.mouseTargetFactory.getMouseColumn(e.editorPos, e.pos);
}
protected _onContextMenu(e: EditorMouseEvent, testEventTarget: boolean): void {
this.viewController.emitContextMenu({
event: e,
target: this._createMouseTarget(e, testEventTarget)
});
}
private _onMouseMove(e: EditorMouseEvent): void {
if (this._mouseDownOperation.isActive()) {
// In selection/drag operation
return;
}
let actualMouseMoveTime = e.timestamp;
if (actualMouseMoveTime < this.lastMouseLeaveTime) {
// Due to throttling, this event occurred before the mouse left the editor, therefore ignore it.
return;
}
this.viewController.emitMouseMove({
event: e,
target: this._createMouseTarget(e, true)
});
}
private _onMouseLeave(e: EditorMouseEvent): void {
this.lastMouseLeaveTime = (new Date()).getTime();
this.viewController.emitMouseLeave({
event: e,
target: null
});
}
public _onMouseUp(e: EditorMouseEvent): void {
this.viewController.emitMouseUp({
event: e,
target: this._createMouseTarget(e, true)
});
}
public _onMouseDown(e: EditorMouseEvent): void {
let t = this._createMouseTarget(e, true);
let targetIsContent = (t.type === editorBrowser.MouseTargetType.CONTENT_TEXT || t.type === editorBrowser.MouseTargetType.CONTENT_EMPTY);
let targetIsGutter = (t.type === editorBrowser.MouseTargetType.GUTTER_GLYPH_MARGIN || t.type === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS || t.type === editorBrowser.MouseTargetType.GUTTER_LINE_DECORATIONS);
let targetIsLineNumbers = (t.type === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS);
let selectOnLineNumbers = this._context.configuration.editor.viewInfo.selectOnLineNumbers;
let targetIsViewZone = (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE);
let targetIsWidget = (t.type === editorBrowser.MouseTargetType.CONTENT_WIDGET);
let shouldHandle = e.leftButton;
if (platform.isMacintosh && e.ctrlKey) {
shouldHandle = false;
}
let focus = () => {
// In IE11, if the focus is in the browser's address bar and
// then you click in the editor, calling preventDefault()
// will not move focus properly (focus remains the address bar)
if (browser.isIE && !this._isFocused) {
this._asyncFocus.schedule();
} else {
e.preventDefault();
this.viewHelper.focusTextArea();
}
};
if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) {
focus();
this._mouseDownOperation.start(t.type, e);
} else if (targetIsGutter) {
// Do not steal focus
e.preventDefault();
} else if (targetIsViewZone) {
let viewZoneData = <IViewZoneData>t.detail;
if (this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) {
focus();
this._mouseDownOperation.start(t.type, e);
e.preventDefault();
}
} else if (targetIsWidget && this.viewHelper.shouldSuppressMouseDownOnWidget(<string>t.detail)) {
focus();
e.preventDefault();
}
this.viewController.emitMouseDown({
event: e,
target: t
});
}
}
class MouseDownOperation extends Disposable {
private readonly _context: ViewContext;
private readonly _viewController: ViewController;
private readonly _viewHelper: IPointerHandlerHelper;
private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => editorBrowser.IMouseTarget;
private readonly _getMouseColumn: (e: EditorMouseEvent) => number;
private readonly _mouseMoveMonitor: GlobalEditorMouseMoveMonitor;
private readonly _onScrollTimeout: TimeoutTimer;
private readonly _mouseState: MouseDownState;
private _currentSelection: Selection;
private _isActive: boolean;
private _lastMouseEvent: EditorMouseEvent;
constructor(
context: ViewContext,
viewController: ViewController,
viewHelper: IPointerHandlerHelper,
createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => editorBrowser.IMouseTarget,
getMouseColumn: (e: EditorMouseEvent) => number
) {
super();
this._context = context;
this._viewController = viewController;
this._viewHelper = viewHelper;
this._createMouseTarget = createMouseTarget;
this._getMouseColumn = getMouseColumn;
this._mouseMoveMonitor = this._register(new GlobalEditorMouseMoveMonitor(this._viewHelper.viewDomNode));
this._onScrollTimeout = this._register(new TimeoutTimer());
this._mouseState = new MouseDownState();
this._currentSelection = new Selection(1, 1, 1, 1);
this._isActive = false;
this._lastMouseEvent = null;
}
public dispose(): void {
super.dispose();
}
public isActive(): boolean {
return this._isActive;
}
private _onMouseDownThenMove(e: EditorMouseEvent): void {
this._lastMouseEvent = e;
this._mouseState.setModifiers(e);
let position = this._findMousePosition(e, true);
if (!position) {
// Ignoring because position is unknown
return;
}
if (this._mouseState.isDragAndDrop) {
this._viewController.emitMouseDrag({
event: e,
target: position
});
} else {
this._dispatchMouse(position, true);
}
}
public start(targetType: editorBrowser.MouseTargetType, e: EditorMouseEvent): void {
this._lastMouseEvent = e;
this._mouseState.setStartedOnLineNumbers(targetType === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS);
this._mouseState.setModifiers(e);
let position = this._findMousePosition(e, true);
if (!position) {
// Ignoring because position is unknown
return;
}
this._mouseState.trySetCount(e.detail, position.position);
// Overwrite the detail of the MouseEvent, as it will be sent out in an event and contributions might rely on it.
e.detail = this._mouseState.count;
if (!this._context.configuration.editor.readOnly
&& this._context.configuration.editor.dragAndDrop
&& !this._mouseState.altKey // we don't support multiple mouse
&& e.detail < 2 // only single click on a selection can work
&& !this._isActive // the mouse is not down yet
&& !this._currentSelection.isEmpty() // we don't drag single cursor
&& this._currentSelection.containsPosition(position.position) // single click on a selection
) {
this._mouseState.isDragAndDrop = true;
this._isActive = true;
this._mouseMoveMonitor.startMonitoring(
createMouseMoveEventMerger(null),
(e) => this._onMouseDownThenMove(e),
() => {
let position = this._findMousePosition(this._lastMouseEvent, true);
this._viewController.emitMouseDrop({
event: this._lastMouseEvent,
target: position ? this._createMouseTarget(this._lastMouseEvent, true) : null // Ignoring because position is unknown, e.g., Content View Zone
});
this._stop();
}
);
return;
}
this._mouseState.isDragAndDrop = false;
this._dispatchMouse(position, e.shiftKey);
if (!this._isActive) {
this._isActive = true;
this._mouseMoveMonitor.startMonitoring(
createMouseMoveEventMerger(null),
(e) => this._onMouseDownThenMove(e),
() => this._stop()
);
}
}
private _stop(): void {
this._isActive = false;
this._onScrollTimeout.cancel();
}
public onScrollChanged(): void {
if (!this._isActive) {
return;
}
this._onScrollTimeout.setIfNotSet(() => {
let position = this._findMousePosition(this._lastMouseEvent, false);
if (!position) {
// Ignoring because position is unknown
return;
}
if (this._mouseState.isDragAndDrop) {
// Ignoring because users are dragging the text
return;
}
this._dispatchMouse(position, true);
}, 10);
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): void {
this._currentSelection = e.selections[0];
}
private _getPositionOutsideEditor(e: EditorMouseEvent): MouseTarget {
const editorContent = e.editorPos;
const model = this._context.model;
const viewLayout = this._context.viewLayout;
const mouseColumn = this._getMouseColumn(e);
if (e.posy < editorContent.y) {
let aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0));
return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1));
}
if (e.posy > editorContent.y + editorContent.height) {
let belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y));
return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)));
}
let possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y));
if (e.posx < editorContent.x) {
return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, 1));
}
if (e.posx > editorContent.x + editorContent.width) {
return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)));
}
return null;
}
private _findMousePosition(e: EditorMouseEvent, testEventTarget: boolean): MouseTarget {
let positionOutsideEditor = this._getPositionOutsideEditor(e);
if (positionOutsideEditor) {
return positionOutsideEditor;
}
let t = this._createMouseTarget(e, testEventTarget);
let hintedPosition = t.position;
if (!hintedPosition) {
return null;
}
if (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE) {
// Force position on view zones to go above or below depending on where selection started from
let selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn);
let viewZoneData = <IViewZoneData>t.detail;
let positionBefore = viewZoneData.positionBefore;
let positionAfter = viewZoneData.positionAfter;
if (positionBefore && positionAfter) {
if (positionBefore.isBefore(selectionStart)) {
return new MouseTarget(t.element, t.type, t.mouseColumn, positionBefore, null, t.detail);
} else {
return new MouseTarget(t.element, t.type, t.mouseColumn, positionAfter, null, t.detail);
}
}
}
return t;
}
private _dispatchMouse(position: MouseTarget, inSelectionMode: boolean): void {
this._viewController.dispatchMouse({
position: position.position,
mouseColumn: position.mouseColumn,
startedOnLineNumbers: this._mouseState.startedOnLineNumbers,
inSelectionMode: inSelectionMode,
mouseDownCount: this._mouseState.count,
altKey: this._mouseState.altKey,
ctrlKey: this._mouseState.ctrlKey,
metaKey: this._mouseState.metaKey,
shiftKey: this._mouseState.shiftKey,
});
}
}
class MouseDownState {
private static CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms
private _altKey: boolean;
public get altKey(): boolean { return this._altKey; }
private _ctrlKey: boolean;
public get ctrlKey(): boolean { return this._ctrlKey; }
private _metaKey: boolean;
public get metaKey(): boolean { return this._metaKey; }
private _shiftKey: boolean;
public get shiftKey(): boolean { return this._shiftKey; }
private _startedOnLineNumbers: boolean;
public get startedOnLineNumbers(): boolean { return this._startedOnLineNumbers; }
private _lastMouseDownPosition: Position;
private _lastMouseDownPositionEqualCount: number;
private _lastMouseDownCount: number;
private _lastSetMouseDownCountTime: number;
public isDragAndDrop: boolean;
constructor() {
this._altKey = false;
this._ctrlKey = false;
this._metaKey = false;
this._shiftKey = false;
this._startedOnLineNumbers = false;
this._lastMouseDownPosition = null;
this._lastMouseDownPositionEqualCount = 0;
this._lastMouseDownCount = 0;
this._lastSetMouseDownCountTime = 0;
this.isDragAndDrop = false;
}
public get count(): number {
return this._lastMouseDownCount;
}
public setModifiers(source: EditorMouseEvent) {
this._altKey = source.altKey;
this._ctrlKey = source.ctrlKey;
this._metaKey = source.metaKey;
this._shiftKey = source.shiftKey;
}
public setStartedOnLineNumbers(startedOnLineNumbers: boolean): void {
this._startedOnLineNumbers = startedOnLineNumbers;
}
public trySetCount(setMouseDownCount: number, newMouseDownPosition: Position): void {
// a. Invalidate multiple clicking if too much time has passed (will be hit by IE because the detail field of mouse events contains garbage in IE10)
let currentTime = (new Date()).getTime();
if (currentTime - this._lastSetMouseDownCountTime > MouseDownState.CLEAR_MOUSE_DOWN_COUNT_TIME) {
setMouseDownCount = 1;
}
this._lastSetMouseDownCountTime = currentTime;
// b. Ensure that we don't jump from single click to triple click in one go (will be hit by IE because the detail field of mouse events contains garbage in IE10)
if (setMouseDownCount > this._lastMouseDownCount + 1) {
setMouseDownCount = this._lastMouseDownCount + 1;
}
// c. Invalidate multiple clicking if the logical position is different
if (this._lastMouseDownPosition && this._lastMouseDownPosition.equals(newMouseDownPosition)) {
this._lastMouseDownPositionEqualCount++;
} else {
this._lastMouseDownPositionEqualCount = 1;
}
this._lastMouseDownPosition = newMouseDownPosition;
// Finally set the lastMouseDownCount
this._lastMouseDownCount = Math.min(setMouseDownCount, this._lastMouseDownPositionEqualCount);
}
}

View File

@@ -0,0 +1,923 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Position } from 'vs/editor/common/core/position';
import { Range as EditorRange } from 'vs/editor/common/core/range';
import { MouseTargetType, IMouseTarget } from 'vs/editor/browser/editorBrowser';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler';
import { EditorMouseEvent, PageCoordinates, ClientCoordinates, EditorPagePosition } from 'vs/editor/browser/editorDom';
import * as browser from 'vs/base/browser/browser';
import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions';
import { ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine';
export interface IViewZoneData {
viewZoneId: number;
positionBefore: Position;
positionAfter: Position;
position: Position;
afterLineNumber: number;
}
interface IETextRange {
boundingHeight: number;
boundingLeft: number;
boundingTop: number;
boundingWidth: number;
htmlText: string;
offsetLeft: number;
offsetTop: number;
text: string;
collapse(start?: boolean): void;
compareEndPoints(how: string, sourceRange: IETextRange): number;
duplicate(): IETextRange;
execCommand(cmdID: string, showUI?: boolean, value?: any): boolean;
execCommandShowHelp(cmdID: string): boolean;
expand(Unit: string): boolean;
findText(string: string, count?: number, flags?: number): boolean;
getBookmark(): string;
getBoundingClientRect(): ClientRect;
getClientRects(): ClientRectList;
inRange(range: IETextRange): boolean;
isEqual(range: IETextRange): boolean;
move(unit: string, count?: number): number;
moveEnd(unit: string, count?: number): number;
moveStart(unit: string, count?: number): number;
moveToBookmark(bookmark: string): boolean;
moveToElementText(element: Element): void;
moveToPoint(x: number, y: number): void;
parentElement(): Element;
pasteHTML(html: string): void;
queryCommandEnabled(cmdID: string): boolean;
queryCommandIndeterm(cmdID: string): boolean;
queryCommandState(cmdID: string): boolean;
queryCommandSupported(cmdID: string): boolean;
queryCommandText(cmdID: string): string;
queryCommandValue(cmdID: string): any;
scrollIntoView(fStart?: boolean): void;
select(): void;
setEndPoint(how: string, SourceRange: IETextRange): void;
}
declare var IETextRange: {
prototype: IETextRange;
new(): IETextRange;
};
interface IHitTestResult {
position: Position;
hitTarget: Element;
}
export class MouseTarget implements IMouseTarget {
public readonly element: Element;
public readonly type: MouseTargetType;
public readonly mouseColumn: number;
public readonly position: Position;
public readonly range: EditorRange;
public readonly detail: any;
constructor(element: Element, type: MouseTargetType, mouseColumn: number = 0, position: Position = null, range: EditorRange = null, detail: any = null) {
this.element = element;
this.type = type;
this.mouseColumn = mouseColumn;
this.position = position;
if (!range && position) {
range = new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
}
this.range = range;
this.detail = detail;
}
private static _typeToString(type: MouseTargetType): string {
if (type === MouseTargetType.TEXTAREA) {
return 'TEXTAREA';
}
if (type === MouseTargetType.GUTTER_GLYPH_MARGIN) {
return 'GUTTER_GLYPH_MARGIN';
}
if (type === MouseTargetType.GUTTER_LINE_NUMBERS) {
return 'GUTTER_LINE_NUMBERS';
}
if (type === MouseTargetType.GUTTER_LINE_DECORATIONS) {
return 'GUTTER_LINE_DECORATIONS';
}
if (type === MouseTargetType.GUTTER_VIEW_ZONE) {
return 'GUTTER_VIEW_ZONE';
}
if (type === MouseTargetType.CONTENT_TEXT) {
return 'CONTENT_TEXT';
}
if (type === MouseTargetType.CONTENT_EMPTY) {
return 'CONTENT_EMPTY';
}
if (type === MouseTargetType.CONTENT_VIEW_ZONE) {
return 'CONTENT_VIEW_ZONE';
}
if (type === MouseTargetType.CONTENT_WIDGET) {
return 'CONTENT_WIDGET';
}
if (type === MouseTargetType.OVERVIEW_RULER) {
return 'OVERVIEW_RULER';
}
if (type === MouseTargetType.SCROLLBAR) {
return 'SCROLLBAR';
}
if (type === MouseTargetType.OVERLAY_WIDGET) {
return 'OVERLAY_WIDGET';
}
return 'UNKNOWN';
}
public static toString(target: IMouseTarget): string {
return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + target.detail;
}
public toString(): string {
return MouseTarget.toString(this);
}
}
class ElementPath {
public static isTextArea(path: Uint8Array): boolean {
return (
path.length === 2
&& path[0] === PartFingerprint.OverflowGuard
&& path[1] === PartFingerprint.TextArea
);
}
public static isChildOfViewLines(path: Uint8Array): boolean {
return (
path.length >= 4
&& path[0] === PartFingerprint.OverflowGuard
&& path[3] === PartFingerprint.ViewLines
);
}
public static isChildOfScrollableElement(path: Uint8Array): boolean {
return (
path.length >= 2
&& path[0] === PartFingerprint.OverflowGuard
&& path[1] === PartFingerprint.ScrollableElement
);
}
public static isChildOfMinimap(path: Uint8Array): boolean {
return (
path.length >= 2
&& path[0] === PartFingerprint.OverflowGuard
&& path[1] === PartFingerprint.Minimap
);
}
public static isChildOfContentWidgets(path: Uint8Array): boolean {
return (
path.length >= 4
&& path[0] === PartFingerprint.OverflowGuard
&& path[3] === PartFingerprint.ContentWidgets
);
}
public static isChildOfOverflowingContentWidgets(path: Uint8Array): boolean {
return (
path.length >= 1
&& path[0] === PartFingerprint.OverflowingContentWidgets
);
}
public static isChildOfOverlayWidgets(path: Uint8Array): boolean {
return (
path.length >= 2
&& path[0] === PartFingerprint.OverflowGuard
&& path[1] === PartFingerprint.OverlayWidgets
);
}
}
class HitTestContext {
public readonly model: IViewModel;
public readonly layoutInfo: EditorLayoutInfo;
public readonly viewDomNode: HTMLElement;
public readonly lineHeight: number;
public readonly typicalHalfwidthCharacterWidth: number;
public readonly lastViewCursorsRenderData: IViewCursorRenderData[];
private readonly _context: ViewContext;
private readonly _viewHelper: IPointerHandlerHelper;
constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastViewCursorsRenderData: IViewCursorRenderData[]) {
this.model = context.model;
this.layoutInfo = context.configuration.editor.layoutInfo;
this.viewDomNode = viewHelper.viewDomNode;
this.lineHeight = context.configuration.editor.lineHeight;
this.typicalHalfwidthCharacterWidth = context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
this.lastViewCursorsRenderData = lastViewCursorsRenderData;
this._context = context;
this._viewHelper = viewHelper;
}
public getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData {
// The target is either a view zone or the empty space after the last view-line
let viewZoneWhitespace = this._context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
if (viewZoneWhitespace) {
let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2,
lineCount = this._context.model.getLineCount(),
positionBefore: Position = null,
position: Position,
positionAfter: Position = null;
if (viewZoneWhitespace.afterLineNumber !== lineCount) {
// There are more lines after this view zone
positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);
}
if (viewZoneWhitespace.afterLineNumber > 0) {
// There are more lines above this view zone
positionBefore = new Position(viewZoneWhitespace.afterLineNumber, this._context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
}
if (positionAfter === null) {
position = positionBefore;
} else if (positionBefore === null) {
position = positionAfter;
} else if (mouseVerticalOffset < viewZoneMiddle) {
position = positionBefore;
} else {
position = positionAfter;
}
return {
viewZoneId: viewZoneWhitespace.id,
afterLineNumber: viewZoneWhitespace.afterLineNumber,
positionBefore: positionBefore,
positionAfter: positionAfter,
position: position
};
}
return null;
}
public getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } {
if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {
// Below the last line
let lineNumber = this._context.model.getLineCount();
let maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
return {
range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
isAfterLines: true
};
}
let lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
let maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
return {
range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),
isAfterLines: false
};
}
public getLineNumberAtVerticalOffset(mouseVerticalOffset: number): number {
return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
}
public isAfterLines(mouseVerticalOffset: number): boolean {
return this._context.viewLayout.isAfterLines(mouseVerticalOffset);
}
public getVerticalOffsetForLineNumber(lineNumber: number): number {
return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
}
public findAttribute(element: Element, attr: string): string {
return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);
}
private static _findAttribute(element: Element, attr: string, stopAt: Element): string {
while (element && element !== document.body) {
if (element.hasAttribute && element.hasAttribute(attr)) {
return element.getAttribute(attr);
}
if (element === stopAt) {
return null;
}
element = <Element>element.parentNode;
}
return null;
}
public getLineWidth(lineNumber: number): number {
return this._viewHelper.getLineWidth(lineNumber);
}
public visibleRangeForPosition2(lineNumber: number, column: number) {
return this._viewHelper.visibleRangeForPosition2(lineNumber, column);
}
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position {
return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);
}
public getCurrentScrollTop(): number {
return this._context.viewLayout.getCurrentScrollTop();
}
public getCurrentScrollLeft(): number {
return this._context.viewLayout.getCurrentScrollLeft();
}
}
abstract class BareHitTestRequest {
public readonly editorPos: EditorPagePosition;
public readonly pos: PageCoordinates;
public readonly mouseVerticalOffset: number;
public readonly isInMarginArea: boolean;
public readonly isInContentArea: boolean;
public readonly mouseContentHorizontalOffset: number;
protected readonly mouseColumn: number;
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates) {
this.editorPos = editorPos;
this.pos = pos;
this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + pos.y - editorPos.y);
this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft;
this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft);
this.isInContentArea = !this.isInMarginArea;
this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
}
}
class HitTestRequest extends BareHitTestRequest {
private readonly _ctx: HitTestContext;
public readonly target: Element;
public readonly targetPath: Uint8Array;
constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, target: Element) {
super(ctx, editorPos, pos);
this._ctx = ctx;
if (target) {
this.target = target;
this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode);
} else {
this.target = null;
this.targetPath = new Uint8Array(0);
}
}
public toString(): string {
return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (<HTMLElement>this.target).outerHTML : null}`;
}
public fulfill(type: MouseTargetType, position: Position = null, range: EditorRange = null, detail: any = null): MouseTarget {
return new MouseTarget(this.target, type, this.mouseColumn, position, range, detail);
}
public withTarget(target: Element): HitTestRequest {
return new HitTestRequest(this._ctx, this.editorPos, this.pos, target);
}
}
export class MouseTargetFactory {
private _context: ViewContext;
private _viewHelper: IPointerHandlerHelper;
constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) {
this._context = context;
this._viewHelper = viewHelper;
}
public mouseTargetIsWidget(e: EditorMouseEvent): boolean {
let t = <Element>e.target;
let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);
// Is it a content widget?
if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {
return true;
}
// Is it an overlay widget?
if (ElementPath.isChildOfOverlayWidgets(path)) {
return true;
}
return false;
}
public createMouseTarget(lastViewCursorsRenderData: IViewCursorRenderData[], editorPos: EditorPagePosition, pos: PageCoordinates, target: HTMLElement): IMouseTarget {
const ctx = new HitTestContext(this._context, this._viewHelper, lastViewCursorsRenderData);
const request = new HitTestRequest(ctx, editorPos, pos, target);
try {
let r = MouseTargetFactory._createMouseTarget(ctx, request, false);
// console.log(r.toString());
return r;
} catch (err) {
// console.log(err);
return request.fulfill(MouseTargetType.UNKNOWN);
}
}
private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): MouseTarget {
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
// First ensure the request has a target
if (request.target === null) {
if (domHitTestExecuted) {
// Still no target... and we have already executed hit test...
return request.fulfill(MouseTargetType.UNKNOWN);
}
const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
if (hitTestResult.position) {
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column);
}
return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
}
let result: MouseTarget = null;
result = result || MouseTargetFactory._hitTestContentWidget(ctx, request);
result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, request);
result = result || MouseTargetFactory._hitTestMinimap(ctx, request);
result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, request);
result = result || MouseTargetFactory._hitTestViewZone(ctx, request);
result = result || MouseTargetFactory._hitTestMargin(ctx, request);
result = result || MouseTargetFactory._hitTestViewCursor(ctx, request);
result = result || MouseTargetFactory._hitTestTextArea(ctx, request);
result = result || MouseTargetFactory._hitTestViewLines(ctx, request, domHitTestExecuted);
result = result || MouseTargetFactory._hitTestScrollbar(ctx, request);
return (result || request.fulfill(MouseTargetType.UNKNOWN));
}
private static _hitTestContentWidget(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
// Is it a content widget?
if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
let widgetId = ctx.findAttribute(request.target, 'widgetId');
if (widgetId) {
return request.fulfill(MouseTargetType.CONTENT_WIDGET, null, null, widgetId);
} else {
return request.fulfill(MouseTargetType.UNKNOWN);
}
}
return null;
}
private static _hitTestOverlayWidget(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
// Is it an overlay widget?
if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) {
let widgetId = ctx.findAttribute(request.target, 'widgetId');
if (widgetId) {
return request.fulfill(MouseTargetType.OVERLAY_WIDGET, null, null, widgetId);
} else {
return request.fulfill(MouseTargetType.UNKNOWN);
}
}
return null;
}
private static _hitTestViewCursor(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
if (request.target) {
// Check if we've hit a painted cursor
const lastViewCursorsRenderData = ctx.lastViewCursorsRenderData;
for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) {
const d = lastViewCursorsRenderData[i];
if (request.target === d.domNode) {
return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position);
}
}
}
if (request.isInContentArea) {
// Edge has a bug when hit-testing the exact position of a cursor,
// instead of returning the correct dom node, it returns the
// first or last rendered view line dom node, therefore help it out
// and first check if we are on top of a cursor
const lastViewCursorsRenderData = ctx.lastViewCursorsRenderData;
const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
const mouseVerticalOffset = request.mouseVerticalOffset;
for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) {
const d = lastViewCursorsRenderData[i];
if (mouseContentHorizontalOffset < d.contentLeft) {
// mouse position is to the left of the cursor
continue;
}
if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
// mouse position is to the right of the cursor
continue;
}
const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
if (
cursorVerticalOffset <= mouseVerticalOffset
&& mouseVerticalOffset <= cursorVerticalOffset + d.height
) {
return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position);
}
}
}
return null;
}
private static _hitTestViewZone(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
let viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
if (viewZoneData) {
let mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE);
return request.fulfill(mouseTargetType, viewZoneData.position, null, viewZoneData);
}
return null;
}
private static _hitTestTextArea(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
// Is it the textarea?
if (ElementPath.isTextArea(request.targetPath)) {
return request.fulfill(MouseTargetType.TEXTAREA);
}
return null;
}
private static _hitTestMargin(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
if (request.isInMarginArea) {
let res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
let pos = res.range.getStartPosition();
let offset = Math.abs(request.pos.x - request.editorPos.x);
if (offset <= ctx.layoutInfo.glyphMarginWidth) {
// On the glyph margin
return request.fulfill(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, res.isAfterLines);
}
offset -= ctx.layoutInfo.glyphMarginWidth;
if (offset <= ctx.layoutInfo.lineNumbersWidth) {
// On the line numbers
return request.fulfill(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, res.isAfterLines);
}
offset -= ctx.layoutInfo.lineNumbersWidth;
// On the line decorations
return request.fulfill(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, res.isAfterLines);
}
return null;
}
private static _hitTestViewLines(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): MouseTarget {
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
return null;
}
// Check if it is below any lines and any view zones
if (ctx.isAfterLines(request.mouseVerticalOffset)) {
// This most likely indicates it happened after the last view-line
const lineCount = ctx.model.getLineCount();
const maxLineColumn = ctx.model.getLineMaxColumn(lineCount);
return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn));
}
if (domHitTestExecuted) {
// We have already executed hit test...
return request.fulfill(MouseTargetType.UNKNOWN);
}
const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
if (hitTestResult.position) {
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column);
}
return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
}
private static _hitTestMinimap(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
if (ElementPath.isChildOfMinimap(request.targetPath)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn));
}
return null;
}
private static _hitTestScrollbarSlider(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
if (request.target && request.target.nodeType === 1) {
let className = request.target.className;
if (className && /\b(slider|scrollbar)\b/.test(className)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn));
}
}
}
return null;
}
private static _hitTestScrollbar(ctx: HitTestContext, request: HitTestRequest): MouseTarget {
// Is it the overview ruler?
// Is it a child of the scrollable element?
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn));
}
return null;
}
public getMouseColumn(editorPos: EditorPagePosition, pos: PageCoordinates): number {
let layoutInfo = this._context.configuration.editor.layoutInfo;
let mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft;
return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth);
}
public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number {
if (mouseContentHorizontalOffset < 0) {
return 1;
}
let chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
return (chars + 1);
}
private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, lineNumber: number, column: number): MouseTarget {
let pos = new Position(lineNumber, column);
let lineWidth = ctx.getLineWidth(lineNumber);
if (request.mouseContentHorizontalOffset > lineWidth) {
if (browser.isEdge && pos.column === 1) {
// See https://github.com/Microsoft/vscode/issues/10875
return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)));
}
return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos);
}
let visibleRange = ctx.visibleRangeForPosition2(lineNumber, column);
if (!visibleRange) {
return request.fulfill(MouseTargetType.UNKNOWN, pos);
}
let columnHorizontalOffset = visibleRange.left;
if (request.mouseContentHorizontalOffset === columnHorizontalOffset) {
return request.fulfill(MouseTargetType.CONTENT_TEXT, pos);
}
let mouseIsBetween: boolean;
if (column > 1) {
let prevColumnHorizontalOffset = visibleRange.left;
mouseIsBetween = false;
mouseIsBetween = mouseIsBetween || (prevColumnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < columnHorizontalOffset); // LTR case
mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < prevColumnHorizontalOffset); // RTL case
if (mouseIsBetween) {
let rng = new EditorRange(lineNumber, column, lineNumber, column - 1);
return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng);
}
}
let lineMaxColumn = ctx.model.getLineMaxColumn(lineNumber);
if (column < lineMaxColumn) {
let nextColumnVisibleRange = ctx.visibleRangeForPosition2(lineNumber, column + 1);
if (nextColumnVisibleRange) {
let nextColumnHorizontalOffset = nextColumnVisibleRange.left;
mouseIsBetween = false;
mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < nextColumnHorizontalOffset); // LTR case
mouseIsBetween = mouseIsBetween || (nextColumnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < columnHorizontalOffset); // RTL case
if (mouseIsBetween) {
let rng = new EditorRange(lineNumber, column, lineNumber, column + 1);
return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng);
}
}
}
return request.fulfill(MouseTargetType.CONTENT_TEXT, pos);
}
/**
* Most probably WebKit browsers and Edge
*/
private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult {
// In Chrome, especially on Linux it is possible to click between lines,
// so try to adjust the `hity` below so that it lands in the center of a line
let lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
let lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
let lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2);
let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
if (adjustedPageY <= request.editorPos.y) {
adjustedPageY = request.editorPos.y + 1;
}
if (adjustedPageY >= request.editorPos.y + ctx.layoutInfo.height) {
adjustedPageY = request.editorPos.y + ctx.layoutInfo.height - 1;
}
let adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
let r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates());
if (r.position) {
return r;
}
// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates());
}
private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult {
let range: Range = document.caretRangeFromPoint(coords.clientX, coords.clientY);
if (!range || !range.startContainer) {
return {
position: null,
hitTarget: null
};
}
// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
let startContainer = range.startContainer;
let hitTarget: HTMLElement;
if (startContainer.nodeType === startContainer.TEXT_NODE) {
// startContainer is expected to be the token text
let parent1 = startContainer.parentNode; // expected to be the token span
let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
if (parent3ClassName === ViewLine.CLASS_NAME) {
let p = ctx.getPositionFromDOMInfo(<HTMLElement>parent1, range.startOffset);
return {
position: p,
hitTarget: null
};
} else {
hitTarget = <HTMLElement>startContainer.parentNode;
}
} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
// startContainer is expected to be the token span
let parent1 = startContainer.parentNode; // expected to be the view line container span
let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;
if (parent2ClassName === ViewLine.CLASS_NAME) {
let p = ctx.getPositionFromDOMInfo(<HTMLElement>startContainer, (<HTMLElement>startContainer).textContent.length);
return {
position: p,
hitTarget: null
};
} else {
hitTarget = <HTMLElement>startContainer;
}
}
return {
position: null,
hitTarget: hitTarget
};
}
/**
* Most probably Gecko
*/
private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult {
let hitResult: { offsetNode: Node; offset: number; } = (<any>document).caretPositionFromPoint(coords.clientX, coords.clientY);
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
// offsetNode is expected to be the token text
let parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
if (parent3ClassName === ViewLine.CLASS_NAME) {
let p = ctx.getPositionFromDOMInfo(<HTMLElement>hitResult.offsetNode.parentNode, hitResult.offset);
return {
position: p,
hitTarget: null
};
} else {
return {
position: null,
hitTarget: <HTMLElement>hitResult.offsetNode.parentNode
};
}
}
return {
position: null,
hitTarget: <HTMLElement>hitResult.offsetNode
};
}
/**
* Most probably IE
*/
private static _doHitTestWithMoveToPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult {
let resultPosition: Position = null;
let resultHitTarget: Element = null;
let textRange: IETextRange = (<any>document.body).createTextRange();
try {
textRange.moveToPoint(coords.clientX, coords.clientY);
} catch (err) {
return {
position: null,
hitTarget: null
};
}
textRange.collapse(true);
// Now, let's do our best to figure out what we hit :)
let parentElement = textRange ? textRange.parentElement() : null;
let parent1 = parentElement ? parentElement.parentNode : null;
let parent2 = parent1 ? parent1.parentNode : null;
let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : '';
if (parent2ClassName === ViewLine.CLASS_NAME) {
let rangeToContainEntireSpan = textRange.duplicate();
rangeToContainEntireSpan.moveToElementText(parentElement);
rangeToContainEntireSpan.setEndPoint('EndToStart', textRange);
resultPosition = ctx.getPositionFromDOMInfo(<HTMLElement>parentElement, rangeToContainEntireSpan.text.length);
// Move range out of the span node, IE doesn't like having many ranges in
// the same spot and will act badly for lines containing dashes ('-')
rangeToContainEntireSpan.moveToElementText(ctx.viewDomNode);
} else {
// Looks like we've hit the hover or something foreign
resultHitTarget = parentElement;
}
// Move range out of the span node, IE doesn't like having many ranges in
// the same spot and will act badly for lines containing dashes ('-')
textRange.moveToElementText(ctx.viewDomNode);
return {
position: resultPosition,
hitTarget: resultHitTarget
};
}
private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult {
// State of the art (18.10.2012):
// The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/)
// Gecko:
// - they tried to implement it once, but failed: https://bugzilla.mozilla.org/show_bug.cgi?id=654352
// - however, they do give out rangeParent/rangeOffset properties on mouse events
// Webkit:
// - they have implemented a previous version of the spec which was using document.caretRangeFromPoint
// IE:
// - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx
// 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy
// - when hit testing the cursor it returns the first or the last line in the viewport
// - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes
// - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line
// Thank you browsers for making this so 'easy' :)
if (document.caretRangeFromPoint) {
return this._doHitTestWithCaretRangeFromPoint(ctx, request);
} else if ((<any>document).caretPositionFromPoint) {
return this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());
} else if ((<any>document.body).createTextRange) {
return this._doHitTestWithMoveToPoint(ctx, request.pos.toClientCoordinates());
}
return {
position: null,
hitTarget: null
};
}
}

View File

@@ -0,0 +1,248 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as dom from 'vs/base/browser/dom';
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { MouseHandler, IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler';
import { IMouseTarget } from 'vs/editor/browser/editorBrowser';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { EditorMouseEvent } from 'vs/editor/browser/editorDom';
import { ViewController } from 'vs/editor/browser/view/viewController';
interface IThrottledGestureEvent {
translationX: number;
translationY: number;
}
function gestureChangeEventMerger(lastEvent: IThrottledGestureEvent, currentEvent: MSGestureEvent): IThrottledGestureEvent {
let r = {
translationY: currentEvent.translationY,
translationX: currentEvent.translationX
};
if (lastEvent) {
r.translationY += lastEvent.translationY;
r.translationX += lastEvent.translationX;
}
return r;
};
/**
* Basically IE10 and IE11
*/
class MsPointerHandler extends MouseHandler implements IDisposable {
private _lastPointerType: string;
private _installGestureHandlerTimeout: number;
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
super(context, viewController, viewHelper);
this.viewHelper.linesContentDomNode.style.msTouchAction = 'none';
this.viewHelper.linesContentDomNode.style.msContentZooming = 'none';
// TODO@Alex -> this expects that the view is added in 100 ms, might not be the case
// This handler should be added when the dom node is in the dom tree
this._installGestureHandlerTimeout = window.setTimeout(() => {
this._installGestureHandlerTimeout = -1;
if ((<any>window).MSGesture) {
let touchGesture = new MSGesture();
let penGesture = new MSGesture();
touchGesture.target = this.viewHelper.linesContentDomNode;
penGesture.target = this.viewHelper.linesContentDomNode;
this.viewHelper.linesContentDomNode.addEventListener('MSPointerDown', (e: MSPointerEvent) => {
// Circumvent IE11 breaking change in e.pointerType & TypeScript's stale definitions
let pointerType = <any>e.pointerType;
if (pointerType === ((<any>e).MSPOINTER_TYPE_MOUSE || 'mouse')) {
this._lastPointerType = 'mouse';
return;
} else if (pointerType === ((<any>e).MSPOINTER_TYPE_TOUCH || 'touch')) {
this._lastPointerType = 'touch';
touchGesture.addPointer(e.pointerId);
} else {
this._lastPointerType = 'pen';
penGesture.addPointer(e.pointerId);
}
});
this._register(dom.addDisposableThrottledListener<IThrottledGestureEvent>(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger));
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'MSGestureTap', (e) => this._onCaptureGestureTap(e), true));
}
}, 100);
this._lastPointerType = 'mouse';
}
public _onMouseDown(e: EditorMouseEvent): void {
if (this._lastPointerType === 'mouse') {
super._onMouseDown(e);
}
}
private _onCaptureGestureTap(rawEvent: MSGestureEvent): void {
let e = new EditorMouseEvent(<MouseEvent><any>rawEvent, this.viewHelper.viewDomNode);
let t = this._createMouseTarget(e, false);
if (t.position) {
this.viewController.moveTo(t.position);
}
// IE does not want to focus when coming in from the browser's address bar
if ((<any>e.browserEvent).fromElement) {
e.preventDefault();
this.viewHelper.focusTextArea();
} else {
// TODO@Alex -> cancel this is focus is lost
setTimeout(() => {
this.viewHelper.focusTextArea();
});
}
}
private _onGestureChange(e: IThrottledGestureEvent): void {
this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY);
}
public dispose(): void {
window.clearTimeout(this._installGestureHandlerTimeout);
super.dispose();
}
}
/**
* Basically Edge but should be modified to handle any pointerEnabled, even without support of MSGesture
*/
class StandardPointerHandler extends MouseHandler implements IDisposable {
private _lastPointerType: string;
private _installGestureHandlerTimeout: number;
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
super(context, viewController, viewHelper);
this.viewHelper.linesContentDomNode.style.touchAction = 'none';
// TODO@Alex -> this expects that the view is added in 100 ms, might not be the case
// This handler should be added when the dom node is in the dom tree
this._installGestureHandlerTimeout = window.setTimeout(() => {
this._installGestureHandlerTimeout = -1;
// TODO@Alex: replace the usage of MSGesture here with something that works across all browsers
if ((<any>window).MSGesture) {
let touchGesture = new MSGesture();
let penGesture = new MSGesture();
touchGesture.target = this.viewHelper.linesContentDomNode;
penGesture.target = this.viewHelper.linesContentDomNode;
this.viewHelper.linesContentDomNode.addEventListener('pointerdown', (e: MSPointerEvent) => {
let pointerType = <any>e.pointerType;
if (pointerType === 'mouse') {
this._lastPointerType = 'mouse';
return;
} else if (pointerType === 'touch') {
this._lastPointerType = 'touch';
touchGesture.addPointer(e.pointerId);
} else {
this._lastPointerType = 'pen';
penGesture.addPointer(e.pointerId);
}
});
this._register(dom.addDisposableThrottledListener<IThrottledGestureEvent>(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger));
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'MSGestureTap', (e) => this._onCaptureGestureTap(e), true));
}
}, 100);
this._lastPointerType = 'mouse';
}
public _onMouseDown(e: EditorMouseEvent): void {
if (this._lastPointerType === 'mouse') {
super._onMouseDown(e);
}
}
private _onCaptureGestureTap(rawEvent: MSGestureEvent): void {
let e = new EditorMouseEvent(<MouseEvent><any>rawEvent, this.viewHelper.viewDomNode);
let t = this._createMouseTarget(e, false);
if (t.position) {
this.viewController.moveTo(t.position);
}
// IE does not want to focus when coming in from the browser's address bar
if ((<any>e.browserEvent).fromElement) {
e.preventDefault();
this.viewHelper.focusTextArea();
} else {
// TODO@Alex -> cancel this is focus is lost
setTimeout(() => {
this.viewHelper.focusTextArea();
});
}
}
private _onGestureChange(e: IThrottledGestureEvent): void {
this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY);
}
public dispose(): void {
window.clearTimeout(this._installGestureHandlerTimeout);
super.dispose();
}
}
class TouchHandler extends MouseHandler {
private gesture: Gesture;
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
super(context, viewController, viewHelper);
this.gesture = new Gesture(this.viewHelper.linesContentDomNode);
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e)));
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e)));
this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e: MouseEvent) => this._onContextMenu(new EditorMouseEvent(e, this.viewHelper.viewDomNode), false)));
}
public dispose(): void {
this.gesture.dispose();
super.dispose();
}
private onTap(event: GestureEvent): void {
event.preventDefault();
this.viewHelper.focusTextArea();
let target = this._createMouseTarget(new EditorMouseEvent(event, this.viewHelper.viewDomNode), false);
if (target.position) {
this.viewController.moveTo(target.position);
}
}
private onChange(e: GestureEvent): void {
this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY);
}
}
export class PointerHandler implements IDisposable {
private handler: MouseHandler;
constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) {
if (window.navigator.msPointerEnabled) {
this.handler = new MsPointerHandler(context, viewController, viewHelper);
} else if ((<any>window).TouchEvent) {
this.handler = new TouchHandler(context, viewController, viewHelper);
} else if (window.navigator.pointerEnabled) {
this.handler = new StandardPointerHandler(context, viewController, viewHelper);
} else {
this.handler = new MouseHandler(context, viewController, viewHelper);
}
}
public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget {
return this.handler.getTargetAtClientPoint(clientX, clientY);
}
public dispose(): void {
this.handler.dispose();
}
}

View File

@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .inputarea {
min-width: 0;
min-height: 0;
margin: 0;
padding: 0;
position: absolute;
outline: none !important;
resize: none;
border: none;
overflow: hidden;
color: transparent;
background-color: transparent;
}
/*.monaco-editor .inputarea {
position: fixed !important;
width: 800px !important;
height: 500px !important;
top: initial !important;
left: initial !important;
bottom: 0 !important;
right: 0 !important;
color: black !important;
background: white !important;
line-height: 15px !important;
font-size: 14px !important;
}*/
.monaco-editor .inputarea.ime-input {
z-index: 10;
}

View File

@@ -0,0 +1,508 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./textAreaHandler';
import * as platform from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { TextAreaInput, ITextAreaInputHost, IPasteData, ICompositionData } from 'vs/editor/browser/controller/textAreaInput';
import { ISimpleModel, ITypeData, TextAreaState, PagedScreenReaderStrategy } from 'vs/editor/browser/controller/textAreaState';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { Position } from 'vs/editor/common/core/position';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { HorizontalRange, RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ViewController } from 'vs/editor/browser/view/viewController';
import { EndOfLinePreference, ScrollType } from 'vs/editor/common/editorCommon';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { PartFingerprints, PartFingerprint, ViewPart } from 'vs/editor/browser/view/viewPart';
import { Margin } from 'vs/editor/browser/viewParts/margin/margin';
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
export interface ITextAreaHandlerHelper {
visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalRange;
}
class VisibleTextAreaData {
_visibleTextAreaBrand: void;
public readonly top: number;
public readonly left: number;
public readonly width: number;
constructor(top: number, left: number, width: number) {
this.top = top;
this.left = left;
this.width = width;
}
public setWidth(width: number): VisibleTextAreaData {
return new VisibleTextAreaData(this.top, this.left, width);
}
}
const canUseZeroSizeTextarea = (browser.isEdgeOrIE || browser.isFirefox);
export class TextAreaHandler extends ViewPart {
private readonly _viewController: ViewController;
private readonly _viewHelper: ITextAreaHandlerHelper;
private _pixelRatio: number;
private _accessibilitySupport: platform.AccessibilitySupport;
private _contentLeft: number;
private _contentWidth: number;
private _contentHeight: number;
private _scrollLeft: number;
private _scrollTop: number;
private _fontInfo: BareFontInfo;
private _lineHeight: number;
private _emptySelectionClipboard: boolean;
/**
* Defined only when the text area is visible (composition case).
*/
private _visibleTextArea: VisibleTextAreaData;
private _selections: Selection[];
private _lastCopiedValue: string;
private _lastCopiedValueIsFromEmptySelection: boolean;
public readonly textArea: FastDomNode<HTMLTextAreaElement>;
public readonly textAreaCover: FastDomNode<HTMLElement>;
private readonly _textAreaInput: TextAreaInput;
constructor(context: ViewContext, viewController: ViewController, viewHelper: ITextAreaHandlerHelper) {
super(context);
this._viewController = viewController;
this._viewHelper = viewHelper;
const conf = this._context.configuration.editor;
this._pixelRatio = conf.pixelRatio;
this._accessibilitySupport = conf.accessibilitySupport;
this._contentLeft = conf.layoutInfo.contentLeft;
this._contentWidth = conf.layoutInfo.contentWidth;
this._contentHeight = conf.layoutInfo.contentHeight;
this._scrollLeft = 0;
this._scrollTop = 0;
this._fontInfo = conf.fontInfo;
this._lineHeight = conf.lineHeight;
this._emptySelectionClipboard = conf.emptySelectionClipboard;
this._visibleTextArea = null;
this._selections = [new Selection(1, 1, 1, 1)];
this._lastCopiedValue = null;
this._lastCopiedValueIsFromEmptySelection = false;
// Text Area (The focus will always be in the textarea when the cursor is blinking)
this.textArea = createFastDomNode(document.createElement('textarea'));
PartFingerprints.write(this.textArea, PartFingerprint.TextArea);
this.textArea.setClassName('inputarea');
this.textArea.setAttribute('wrap', 'off');
this.textArea.setAttribute('autocorrect', 'off');
this.textArea.setAttribute('autocapitalize', 'off');
this.textArea.setAttribute('autocomplete', 'off');
this.textArea.setAttribute('spellcheck', 'false');
this.textArea.setAttribute('aria-label', conf.viewInfo.ariaLabel);
this.textArea.setAttribute('role', 'textbox');
this.textArea.setAttribute('aria-multiline', 'true');
this.textArea.setAttribute('aria-haspopup', 'false');
this.textArea.setAttribute('aria-autocomplete', 'both');
this.textAreaCover = createFastDomNode(document.createElement('div'));
this.textAreaCover.setPosition('absolute');
const simpleModel: ISimpleModel = {
getLineCount: (): number => {
return this._context.model.getLineCount();
},
getLineMaxColumn: (lineNumber: number): number => {
return this._context.model.getLineMaxColumn(lineNumber);
},
getValueInRange: (range: Range, eol: EndOfLinePreference): string => {
return this._context.model.getValueInRange(range, eol);
}
};
const textAreaInputHost: ITextAreaInputHost = {
getPlainTextToCopy: (): string => {
const whatToCopy = this._context.model.getPlainTextToCopy(this._selections, this._emptySelectionClipboard);
if (this._emptySelectionClipboard) {
if (browser.isFirefox) {
// When writing "LINE\r\n" to the clipboard and then pasting,
// Firefox pastes "LINE\n", so let's work around this quirk
this._lastCopiedValue = whatToCopy.replace(/\r\n/g, '\n');
} else {
this._lastCopiedValue = whatToCopy;
}
let selections = this._selections;
this._lastCopiedValueIsFromEmptySelection = (selections.length === 1 && selections[0].isEmpty());
}
return whatToCopy;
},
getHTMLToCopy: (): string => {
return this._context.model.getHTMLToCopy(this._selections, this._emptySelectionClipboard);
},
getScreenReaderContent: (currentState: TextAreaState): TextAreaState => {
if (browser.isIPad) {
// Do not place anything in the textarea for the iPad
return TextAreaState.EMPTY;
}
if (this._accessibilitySupport === platform.AccessibilitySupport.Disabled) {
// We know for a fact that a screen reader is not attached
return TextAreaState.EMPTY;
}
return PagedScreenReaderStrategy.fromEditorSelection(currentState, simpleModel, this._selections[0]);
},
deduceModelPosition: (viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position => {
return this._context.model.deduceModelPositionRelativeToViewPosition(viewAnchorPosition, deltaOffset, lineFeedCnt);
}
};
this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, this.textArea));
this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {
this._viewController.emitKeyDown(e);
}));
this._register(this._textAreaInput.onKeyUp((e: IKeyboardEvent) => {
this._viewController.emitKeyUp(e);
}));
this._register(this._textAreaInput.onPaste((e: IPasteData) => {
let pasteOnNewLine = false;
if (this._emptySelectionClipboard) {
pasteOnNewLine = (e.text === this._lastCopiedValue && this._lastCopiedValueIsFromEmptySelection);
}
this._viewController.paste('keyboard', e.text, pasteOnNewLine);
}));
this._register(this._textAreaInput.onCut(() => {
this._viewController.cut('keyboard');
}));
this._register(this._textAreaInput.onType((e: ITypeData) => {
if (e.replaceCharCnt) {
this._viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
} else {
this._viewController.type('keyboard', e.text);
}
}));
this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection: Selection) => {
this._viewController.setSelection('keyboard', modelSelection);
}));
this._register(this._textAreaInput.onCompositionStart(() => {
const lineNumber = this._selections[0].startLineNumber;
const column = this._selections[0].startColumn;
this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
new Range(lineNumber, column, lineNumber, column),
viewEvents.VerticalRevealType.Simple,
true,
ScrollType.Immediate
));
// Find range pixel position
const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
if (visibleRange) {
this._visibleTextArea = new VisibleTextAreaData(
this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber),
visibleRange.left,
canUseZeroSizeTextarea ? 0 : 1
);
this._render();
}
// Show the textarea
this.textArea.setClassName('inputarea ime-input');
this._viewController.compositionStart('keyboard');
}));
this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
if (browser.isEdgeOrIE) {
// Due to isEdgeOrIE (where the textarea was not cleared initially)
// we cannot assume the text consists only of the composited text
this._visibleTextArea = this._visibleTextArea.setWidth(0);
} else {
// adjust width by its size
this._visibleTextArea = this._visibleTextArea.setWidth(measureText(e.data, this._fontInfo));
}
this._render();
}));
this._register(this._textAreaInput.onCompositionEnd(() => {
this._visibleTextArea = null;
this._render();
this.textArea.setClassName('inputarea');
this._viewController.compositionEnd('keyboard');
}));
this._register(this._textAreaInput.onFocus(() => {
this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(true));
}));
this._register(this._textAreaInput.onBlur(() => {
this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(false));
}));
}
public dispose(): void {
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
const conf = this._context.configuration.editor;
if (e.fontInfo) {
this._fontInfo = conf.fontInfo;
}
if (e.viewInfo) {
this.textArea.setAttribute('aria-label', conf.viewInfo.ariaLabel);
}
if (e.layoutInfo) {
this._contentLeft = conf.layoutInfo.contentLeft;
this._contentWidth = conf.layoutInfo.contentWidth;
this._contentHeight = conf.layoutInfo.contentHeight;
}
if (e.lineHeight) {
this._lineHeight = conf.lineHeight;
}
if (e.pixelRatio) {
this._pixelRatio = conf.pixelRatio;
}
if (e.accessibilitySupport) {
this._accessibilitySupport = conf.accessibilitySupport;
this._textAreaInput.writeScreenReaderContent('strategy changed');
}
if (e.emptySelectionClipboard) {
this._emptySelectionClipboard = conf.emptySelectionClipboard;
}
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._selections = e.selections.slice(0);
this._textAreaInput.writeScreenReaderContent('selection changed');
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this._scrollLeft = e.scrollLeft;
this._scrollTop = e.scrollTop;
return true;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
// --- begin view API
public isFocused(): boolean {
return this._textAreaInput.isFocused();
}
public focusTextArea(): void {
this._textAreaInput.focusTextArea();
}
public setAriaActiveDescendant(id: string): void {
if (id) {
this.textArea.setAttribute('role', 'combobox');
if (this.textArea.getAttribute('aria-activedescendant') !== id) {
this.textArea.setAttribute('aria-haspopup', 'true');
this.textArea.setAttribute('aria-activedescendant', id);
}
} else {
this.textArea.setAttribute('role', 'textbox');
this.textArea.removeAttribute('aria-activedescendant');
this.textArea.removeAttribute('aria-haspopup');
}
}
// --- end view API
private _primaryCursorVisibleRange: HorizontalRange = null;
public prepareRender(ctx: RenderingContext): void {
if (this._accessibilitySupport === platform.AccessibilitySupport.Enabled) {
// Do not move the textarea with the cursor, as this generates accessibility events that might confuse screen readers
// See https://github.com/Microsoft/vscode/issues/26730
this._primaryCursorVisibleRange = null;
} else {
const primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);
this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(primaryCursorPosition);
}
}
public render(ctx: RestrictedRenderingContext): void {
this._textAreaInput.writeScreenReaderContent('render');
this._render();
}
private _render(): void {
if (this._visibleTextArea) {
// The text area is visible for composition reasons
this._renderInsideEditor(
this._visibleTextArea.top - this._scrollTop,
this._contentLeft + this._visibleTextArea.left - this._scrollLeft,
this._visibleTextArea.width,
this._lineHeight,
true
);
return;
}
if (!this._primaryCursorVisibleRange) {
// The primary cursor is outside the viewport => place textarea to the top left
this._renderAtTopLeft();
return;
}
const left = this._contentLeft + this._primaryCursorVisibleRange.left - this._scrollLeft;
if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;
if (top < 0 || top > this._contentHeight) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
// The primary cursor is in the viewport (at least vertically) => place textarea on the cursor
this._renderInsideEditor(
top, left,
canUseZeroSizeTextarea ? 0 : 1, canUseZeroSizeTextarea ? 0 : 1,
false
);
}
private _renderInsideEditor(top: number, left: number, width: number, height: number, useEditorFont: boolean): void {
const ta = this.textArea;
const tac = this.textAreaCover;
if (useEditorFont) {
Configuration.applyFontInfo(ta, this._fontInfo);
} else {
ta.setFontSize(1);
ta.setLineHeight(this._fontInfo.lineHeight);
}
ta.setTop(top);
ta.setLeft(left);
ta.setWidth(width);
ta.setHeight(height);
tac.setTop(0);
tac.setLeft(0);
tac.setWidth(0);
tac.setHeight(0);
}
private _renderAtTopLeft(): void {
const ta = this.textArea;
const tac = this.textAreaCover;
Configuration.applyFontInfo(ta, this._fontInfo);
ta.setTop(0);
ta.setLeft(0);
tac.setTop(0);
tac.setLeft(0);
if (canUseZeroSizeTextarea) {
ta.setWidth(0);
ta.setHeight(0);
tac.setWidth(0);
tac.setHeight(0);
return;
}
// (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)
// specifically, when doing Korean IME, setting the textare to 0x0 breaks IME badly.
ta.setWidth(1);
ta.setHeight(1);
tac.setWidth(1);
tac.setHeight(1);
if (this._context.configuration.editor.viewInfo.glyphMargin) {
tac.setClassName('monaco-editor-background textAreaCover ' + Margin.CLASS_NAME);
} else {
if (this._context.configuration.editor.viewInfo.renderLineNumbers) {
tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
} else {
tac.setClassName('monaco-editor-background textAreaCover');
}
}
}
}
function measureText(text: string, fontInfo: BareFontInfo): number {
// adjust width by its size
const canvasElem = <HTMLCanvasElement>document.createElement('canvas');
const context = canvasElem.getContext('2d');
context.font = createFontString(fontInfo);
const metrics = context.measureText(text);
if (browser.isFirefox) {
return metrics.width + 2; // +2 for Japanese...
} else {
return metrics.width;
}
}
function createFontString(bareFontInfo: BareFontInfo): string {
return doCreateFontString('normal', bareFontInfo.fontWeight, bareFontInfo.fontSize, bareFontInfo.lineHeight, bareFontInfo.fontFamily);
}
function doCreateFontString(fontStyle: string, fontWeight: string, fontSize: number, lineHeight: number, fontFamily: string): string {
// The full font syntax is:
// style | variant | weight | stretch | size/line-height | fontFamily
// (https://developer.mozilla.org/en-US/docs/Web/CSS/font)
// But it appears Edge and IE11 cannot properly parse `stretch`.
return `${fontStyle} normal ${fontWeight} ${fontSize}px / ${lineHeight}px ${fontFamily}`;
}

View File

@@ -0,0 +1,585 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import * as strings from 'vs/base/common/strings';
import Event, { Emitter } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { ITypeData, TextAreaState, ITextAreaWrapper } from 'vs/editor/browser/controller/textAreaState';
import * as browser from 'vs/base/browser/browser';
import * as platform from 'vs/base/common/platform';
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { FastDomNode } from 'vs/base/browser/fastDomNode';
export interface ICompositionData {
data: string;
}
export const CopyOptions = {
forceCopyWithSyntaxHighlighting: false
};
const enum ReadFromTextArea {
Type,
Paste
}
export interface IPasteData {
text: string;
}
export interface ITextAreaInputHost {
getPlainTextToCopy(): string;
getHTMLToCopy(): string;
getScreenReaderContent(currentState: TextAreaState): TextAreaState;
deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position;
}
/**
* Writes screen reader content to the textarea and is able to analyze its input events to generate:
* - onCut
* - onPaste
* - onType
*
* Composition events are generated for presentation purposes (composition input is reflected in onType).
*/
export class TextAreaInput extends Disposable {
private _onFocus = this._register(new Emitter<void>());
public onFocus: Event<void> = this._onFocus.event;
private _onBlur = this._register(new Emitter<void>());
public onBlur: Event<void> = this._onBlur.event;
private _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
public onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
public onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
private _onCut = this._register(new Emitter<void>());
public onCut: Event<void> = this._onCut.event;
private _onPaste = this._register(new Emitter<IPasteData>());
public onPaste: Event<IPasteData> = this._onPaste.event;
private _onType = this._register(new Emitter<ITypeData>());
public onType: Event<ITypeData> = this._onType.event;
private _onCompositionStart = this._register(new Emitter<void>());
public onCompositionStart: Event<void> = this._onCompositionStart.event;
private _onCompositionUpdate = this._register(new Emitter<ICompositionData>());
public onCompositionUpdate: Event<ICompositionData> = this._onCompositionUpdate.event;
private _onCompositionEnd = this._register(new Emitter<void>());
public onCompositionEnd: Event<void> = this._onCompositionEnd.event;
private _onSelectionChangeRequest = this._register(new Emitter<Selection>());
public onSelectionChangeRequest: Event<Selection> = this._onSelectionChangeRequest.event;
// ---
private readonly _host: ITextAreaInputHost;
private readonly _textArea: TextAreaWrapper;
private readonly _asyncTriggerCut: RunOnceScheduler;
private _textAreaState: TextAreaState;
private _hasFocus: boolean;
private _isDoingComposition: boolean;
private _nextCommand: ReadFromTextArea;
constructor(host: ITextAreaInputHost, textArea: FastDomNode<HTMLTextAreaElement>) {
super();
this._host = host;
this._textArea = this._register(new TextAreaWrapper(textArea));
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
this._textAreaState = TextAreaState.EMPTY;
this.writeScreenReaderContent('ctor');
this._hasFocus = false;
this._isDoingComposition = false;
this._nextCommand = ReadFromTextArea.Type;
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keydown', (e: IKeyboardEvent) => {
if (this._isDoingComposition && e.equals(KeyCode.KEY_IN_COMPOSITION)) {
// Stop propagation for keyDown events if the IME is processing key input
e.stopPropagation();
}
if (e.equals(KeyCode.Escape)) {
// Prevent default always for `Esc`, otherwise it will generate a keypress
// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
e.preventDefault();
}
this._onKeyDown.fire(e);
}));
this._register(dom.addStandardDisposableListener(textArea.domNode, 'keyup', (e: IKeyboardEvent) => {
this._onKeyUp.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => {
if (this._isDoingComposition) {
return;
}
this._isDoingComposition = true;
// In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled.
if (!browser.isEdgeOrIE) {
this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY);
}
this._onCompositionStart.fire();
}));
/**
* Deduce the typed input from a text area's value and the last observed state.
*/
const deduceInputFromTextAreaValue = (couldBeEmojiInput: boolean): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = this._textAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)];
};
/**
* Deduce the composition input from a string.
*/
const deduceComposition = (text: string): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.selectedText(text);
const typeInput: ITypeData = {
text: newState.value,
replaceCharCnt: oldState.selectionEnd - oldState.selectionStart
};
return [newState, typeInput];
};
this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => {
if (browser.isChromev56) {
// See https://github.com/Microsoft/monaco-editor/issues/320
// where compositionupdate .data is broken in Chrome v55 and v56
// See https://bugs.chromium.org/p/chromium/issues/detail?id=677050#c9
// The textArea doesn't get the composition update yet, the value of textarea is still obsolete
// so we can't correct e at this moment.
return;
}
if (browser.isEdgeOrIE && e.locale === 'ja') {
// https://github.com/Microsoft/monaco-editor/issues/339
// Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue.
// The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME,
// which breaks this path of code.
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
return;
}
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => {
if (browser.isEdgeOrIE && e.locale === 'ja') {
// https://github.com/Microsoft/monaco-editor/issues/339
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false);
this._textAreaState = newState;
this._onType.fire(typeInput);
}
else {
const [newState, typeInput] = deduceComposition(e.data);
this._textAreaState = newState;
this._onType.fire(typeInput);
}
// Due to isEdgeOrIE (where the textarea was not cleared initially) and isChrome (the textarea is not updated correctly when composition ends)
// we cannot assume the text at the end consists only of the composited text
if (browser.isEdgeOrIE || browser.isChrome) {
this._textAreaState = this._textAreaState.readFromTextArea(this._textArea);
}
if (!this._isDoingComposition) {
return;
}
this._isDoingComposition = false;
this._onCompositionEnd.fire();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'input', () => {
// Pretend here we touched the text area, as the `input` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received input event');
if (this._isDoingComposition) {
// See https://github.com/Microsoft/monaco-editor/issues/320
if (browser.isChromev56) {
const [newState, typeInput] = deduceComposition(this._textArea.getValue());
this._textAreaState = newState;
this._onType.fire(typeInput);
let e: ICompositionData = {
data: typeInput.text
};
this._onCompositionUpdate.fire(e);
}
return;
}
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh);
if (typeInput.replaceCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
// Ignore invalid input but keep it around for next time
return;
}
this._textAreaState = newState;
// console.log('==> DEDUCED INPUT: ' + JSON.stringify(typeInput));
if (this._nextCommand === ReadFromTextArea.Type) {
if (typeInput.text !== '') {
this._onType.fire(typeInput);
}
} else {
if (typeInput.text !== '') {
this._onPaste.fire({
text: typeInput.text
});
}
this._nextCommand = ReadFromTextArea.Type;
}
}));
// --- Clipboard operations
this._register(dom.addDisposableListener(textArea.domNode, 'cut', (e: ClipboardEvent) => {
// Pretend here we touched the text area, as the `cut` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received cut event');
this._ensureClipboardGetsEditorSelection(e);
this._asyncTriggerCut.schedule();
}));
this._register(dom.addDisposableListener(textArea.domNode, 'copy', (e: ClipboardEvent) => {
this._ensureClipboardGetsEditorSelection(e);
}));
this._register(dom.addDisposableListener(textArea.domNode, 'paste', (e: ClipboardEvent) => {
// Pretend here we touched the text area, as the `paste` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received paste event');
if (ClipboardEventUtils.canUseTextData(e)) {
const pastePlainText = ClipboardEventUtils.getTextData(e);
if (pastePlainText !== '') {
this._onPaste.fire({
text: pastePlainText
});
}
} else {
if (this._textArea.getSelectionStart() !== this._textArea.getSelectionEnd()) {
// Clean up the textarea, to get a clean paste
this._setAndWriteTextAreaState('paste', TextAreaState.EMPTY);
}
this._nextCommand = ReadFromTextArea.Paste;
}
}));
this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => this._setHasFocus(true)));
this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => this._setHasFocus(false)));
// See https://github.com/Microsoft/vscode/issues/27216
// When using a Braille display, it is possible for users to reposition the
// system caret. This is reflected in Chrome as a `selectionchange` event.
//
// The `selectionchange` event appears to be emitted under numerous other circumstances,
// so it is quite a challenge to distinguish a `selectionchange` coming in from a user
// using a Braille display from all the other cases.
//
// The problems with the `selectionchange` event are:
// * the event is emitted when the textarea is focused programmatically -- textarea.focus()
// * the event is emitted when the selection is changed in the textarea programatically -- textarea.setSelectionRange(...)
// * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'
// * the event is emitted when tabbing into the textarea
// * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)
// * the event sometimes comes in bursts for a single logical textarea operation
// `selectionchange` events often come multiple times for a single logical change
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
this._register(dom.addDisposableListener(document, 'selectionchange', (e) => {
if (!this._hasFocus) {
return;
}
if (this._isDoingComposition) {
return;
}
if (!browser.isChrome || !platform.isWindows) {
// Support only for Chrome on Windows until testing happens on other browsers + OS configurations
return;
}
const now = Date.now();
const delta1 = now - previousSelectionChangeEventTime;
previousSelectionChangeEventTime = now;
if (delta1 < 5) {
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
// => ignore it
return;
}
const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();
this._textArea.resetSelectionChangeTime();
if (delta2 < 100) {
// received a `selectionchange` event within 100ms since we touched the textarea
// => ignore it, since we caused it
return;
}
if (!this._textAreaState.selectionStartPosition || !this._textAreaState.selectionEndPosition) {
// Cannot correlate a position in the textarea with a position in the editor...
return;
}
const newValue = this._textArea.getValue();
if (this._textAreaState.value !== newValue) {
// Cannot correlate a position in the textarea with a position in the editor...
return;
}
const newSelectionStart = this._textArea.getSelectionStart();
const newSelectionEnd = this._textArea.getSelectionEnd();
if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {
// Nothing to do...
return;
}
const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);
const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0], _newSelectionStartPosition[1], _newSelectionStartPosition[2]);
const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);
const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0], _newSelectionEndPosition[1], _newSelectionEndPosition[2]);
const newSelection = new Selection(
newSelectionStartPosition.lineNumber, newSelectionStartPosition.column,
newSelectionEndPosition.lineNumber, newSelectionEndPosition.column
);
this._onSelectionChangeRequest.fire(newSelection);
}));
}
public dispose(): void {
super.dispose();
}
public focusTextArea(): void {
// Setting this._hasFocus and writing the screen reader content
// will result in a focus() and setSelectionRange() in the textarea
this._setHasFocus(true);
}
public isFocused(): boolean {
return this._hasFocus;
}
private _setHasFocus(newHasFocus: boolean): void {
if (this._hasFocus === newHasFocus) {
// no change
return;
}
this._hasFocus = newHasFocus;
if (this._hasFocus) {
if (browser.isEdge) {
// Edge has a bug where setting the selection range while the focus event
// is dispatching doesn't work. To reproduce, "tab into" the editor.
this._setAndWriteTextAreaState('focusgain', TextAreaState.EMPTY);
} else {
this.writeScreenReaderContent('focusgain');
}
}
if (this._hasFocus) {
this._onFocus.fire();
} else {
this._onBlur.fire();
}
}
private _setAndWriteTextAreaState(reason: string, textAreaState: TextAreaState): void {
if (!this._hasFocus) {
textAreaState = textAreaState.collapseSelection();
}
textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);
this._textAreaState = textAreaState;
}
public writeScreenReaderContent(reason: string): void {
if (this._isDoingComposition) {
// Do not write to the text area when doing composition
return;
}
this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent(this._textAreaState));
}
private _ensureClipboardGetsEditorSelection(e: ClipboardEvent): void {
const copyPlainText = this._host.getPlainTextToCopy();
if (!ClipboardEventUtils.canUseTextData(e)) {
// Looks like an old browser. The strategy is to place the text
// we'd like to be copied to the clipboard in the textarea and select it.
this._setAndWriteTextAreaState('copy or cut', TextAreaState.selectedText(copyPlainText));
return;
}
let copyHTML: string = null;
if (!browser.isEdgeOrIE && (copyPlainText.length < 65536 || CopyOptions.forceCopyWithSyntaxHighlighting)) {
copyHTML = this._host.getHTMLToCopy();
}
ClipboardEventUtils.setTextData(e, copyPlainText, copyHTML);
}
}
class ClipboardEventUtils {
public static canUseTextData(e: ClipboardEvent): boolean {
if (e.clipboardData) {
return true;
}
if ((<any>window).clipboardData) {
return true;
}
return false;
}
public static getTextData(e: ClipboardEvent): string {
if (e.clipboardData) {
e.preventDefault();
return e.clipboardData.getData('text/plain');
}
if ((<any>window).clipboardData) {
e.preventDefault();
return (<any>window).clipboardData.getData('Text');
}
throw new Error('ClipboardEventUtils.getTextData: Cannot use text data!');
}
public static setTextData(e: ClipboardEvent, text: string, richText: string): void {
if (e.clipboardData) {
e.clipboardData.setData('text/plain', text);
if (richText !== null) {
e.clipboardData.setData('text/html', richText);
}
e.preventDefault();
return;
}
if ((<any>window).clipboardData) {
(<any>window).clipboardData.setData('Text', text);
e.preventDefault();
return;
}
throw new Error('ClipboardEventUtils.setTextData: Cannot use text data!');
}
}
class TextAreaWrapper extends Disposable implements ITextAreaWrapper {
private readonly _actual: FastDomNode<HTMLTextAreaElement>;
private _ignoreSelectionChangeTime: number;
constructor(_textArea: FastDomNode<HTMLTextAreaElement>) {
super();
this._actual = _textArea;
this._ignoreSelectionChangeTime = 0;
}
public setIgnoreSelectionChangeTime(reason: string): void {
this._ignoreSelectionChangeTime = Date.now();
}
public getIgnoreSelectionChangeTime(): number {
return this._ignoreSelectionChangeTime;
}
public resetSelectionChangeTime(): void {
this._ignoreSelectionChangeTime = 0;
}
public getValue(): string {
// console.log('current value: ' + this._textArea.value);
return this._actual.domNode.value;
}
public setValue(reason: string, value: string): void {
const textArea = this._actual.domNode;
if (textArea.value === value) {
// No change
return;
}
// console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);
this.setIgnoreSelectionChangeTime('setValue');
textArea.value = value;
}
public getSelectionStart(): number {
return this._actual.domNode.selectionStart;
}
public getSelectionEnd(): number {
return this._actual.domNode.selectionEnd;
}
public setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void {
const textArea = this._actual.domNode;
const currentIsFocused = (document.activeElement === textArea);
const currentSelectionStart = textArea.selectionStart;
const currentSelectionEnd = textArea.selectionEnd;
if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {
// No change
return;
}
// console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);
if (currentIsFocused) {
// No need to focus, only need to change the selection range
this.setIgnoreSelectionChangeTime('setSelectionRange');
textArea.setSelectionRange(selectionStart, selectionEnd);
return;
}
// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.
// Here, we try to undo the browser's desperate reveal.
try {
const scrollState = dom.saveParentsScrollTop(textArea);
this.setIgnoreSelectionChangeTime('setSelectionRange');
textArea.focus();
textArea.setSelectionRange(selectionStart, selectionEnd);
dom.restoreParentsScrollTop(textArea, scrollState);
} catch (e) {
// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
}
}
}

View File

@@ -0,0 +1,289 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { EndOfLinePreference } from 'vs/editor/common/editorCommon';
import * as strings from 'vs/base/common/strings';
export interface ITextAreaWrapper {
getValue(): string;
setValue(reason: string, value: string): void;
getSelectionStart(): number;
getSelectionEnd(): number;
setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void;
}
export interface ISimpleModel {
getLineCount(): number;
getLineMaxColumn(lineNumber: number): number;
getValueInRange(range: Range, eol: EndOfLinePreference): string;
}
export interface ITypeData {
text: string;
replaceCharCnt: number;
}
export class TextAreaState {
public static EMPTY = new TextAreaState('', 0, 0, null, null);
public readonly value: string;
public readonly selectionStart: number;
public readonly selectionEnd: number;
public readonly selectionStartPosition: Position;
public readonly selectionEndPosition: Position;
constructor(value: string, selectionStart: number, selectionEnd: number, selectionStartPosition: Position, selectionEndPosition: Position) {
this.value = value;
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
this.selectionStartPosition = selectionStartPosition;
this.selectionEndPosition = selectionEndPosition;
}
public equals(other: TextAreaState): boolean {
if (other instanceof TextAreaState) {
return (
this.value === other.value
&& this.selectionStart === other.selectionStart
&& this.selectionEnd === other.selectionEnd
&& Position.equals(this.selectionStartPosition, other.selectionStartPosition)
&& Position.equals(this.selectionEndPosition, other.selectionEndPosition)
);
}
return false;
}
public toString(): string {
return '[ <' + this.value + '>, selectionStart: ' + this.selectionStart + ', selectionEnd: ' + this.selectionEnd + ']';
}
public readFromTextArea(textArea: ITextAreaWrapper): TextAreaState {
return new TextAreaState(textArea.getValue(), textArea.getSelectionStart(), textArea.getSelectionEnd(), null, null);
}
public collapseSelection(): TextAreaState {
return new TextAreaState(this.value, this.value.length, this.value.length, null, null);
}
public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void {
// console.log(Date.now() + ': applyToTextArea ' + reason + ': ' + this.toString());
textArea.setValue(reason, this.value);
if (select) {
textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);
}
}
public deduceEditorPosition(offset: number): [Position, number, number] {
if (offset <= this.selectionStart) {
const str = this.value.substring(offset, this.selectionStart);
return this._finishDeduceEditorPosition(this.selectionStartPosition, str, -1);
}
if (offset >= this.selectionEnd) {
const str = this.value.substring(this.selectionEnd, offset);
return this._finishDeduceEditorPosition(this.selectionEndPosition, str, 1);
}
const str1 = this.value.substring(this.selectionStart, offset);
if (str1.indexOf(String.fromCharCode(8230)) === -1) {
return this._finishDeduceEditorPosition(this.selectionStartPosition, str1, 1);
}
const str2 = this.value.substring(offset, this.selectionEnd);
return this._finishDeduceEditorPosition(this.selectionEndPosition, str2, -1);
}
private _finishDeduceEditorPosition(anchor: Position, deltaText: string, signum: number): [Position, number, number] {
let lineFeedCnt = 0;
let lastLineFeedIndex = -1;
while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {
lineFeedCnt++;
}
return [anchor, signum * deltaText.length, lineFeedCnt];
}
public static selectedText(text: string): TextAreaState {
return new TextAreaState(text, 0, text.length, null, null);
}
public static deduceInput(previousState: TextAreaState, currentState: TextAreaState, couldBeEmojiInput: boolean): ITypeData {
if (!previousState) {
// This is the EMPTY state
return {
text: '',
replaceCharCnt: 0
};
}
// console.log('------------------------deduceInput');
// console.log('PREVIOUS STATE: ' + previousState.toString());
// console.log('CURRENT STATE: ' + currentState.toString());
let previousValue = previousState.value;
let previousSelectionStart = previousState.selectionStart;
let previousSelectionEnd = previousState.selectionEnd;
let currentValue = currentState.value;
let currentSelectionStart = currentState.selectionStart;
let currentSelectionEnd = currentState.selectionEnd;
// Strip the previous suffix from the value (without interfering with the current selection)
const previousSuffix = previousValue.substring(previousSelectionEnd);
const currentSuffix = currentValue.substring(currentSelectionEnd);
const suffixLength = strings.commonSuffixLength(previousSuffix, currentSuffix);
currentValue = currentValue.substring(0, currentValue.length - suffixLength);
previousValue = previousValue.substring(0, previousValue.length - suffixLength);
const previousPrefix = previousValue.substring(0, previousSelectionStart);
const currentPrefix = currentValue.substring(0, currentSelectionStart);
const prefixLength = strings.commonPrefixLength(previousPrefix, currentPrefix);
currentValue = currentValue.substring(prefixLength);
previousValue = previousValue.substring(prefixLength);
currentSelectionStart -= prefixLength;
previousSelectionStart -= prefixLength;
currentSelectionEnd -= prefixLength;
previousSelectionEnd -= prefixLength;
// console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd);
// console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) {
// on OSX, emojis from the emoji picker are inserted at random locations
// the only hints we can use is that the selection is immediately after the inserted emoji
// and that none of the old text has been deleted
let potentialEmojiInput: string = null;
if (currentSelectionStart === currentValue.length) {
// emoji potentially inserted "somewhere" after the previous selection => it should appear at the end of `currentValue`
if (strings.startsWith(currentValue, previousValue)) {
// only if all of the old text is accounted for
potentialEmojiInput = currentValue.substring(previousValue.length);
}
} else {
// emoji potentially inserted "somewhere" before the previous selection => it should appear at the start of `currentValue`
if (strings.endsWith(currentValue, previousValue)) {
// only if all of the old text is accounted for
potentialEmojiInput = currentValue.substring(0, currentValue.length - previousValue.length);
}
}
if (potentialEmojiInput !== null && potentialEmojiInput.length > 0) {
// now we check that this is indeed an emoji
// emojis can grow quite long, so a length check is of no help
// e.g. 1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 England
// Oftentimes, emojis use Variation Selector-16 (U+FE0F), so that is a good hint
// http://emojipedia.org/variation-selector-16/
// > An invisible codepoint which specifies that the preceding character
// > should be displayed with emoji presentation. Only required if the
// > preceding character defaults to text presentation.
if (/\uFE0F/.test(potentialEmojiInput) || strings.containsEmoji(potentialEmojiInput)) {
return {
text: potentialEmojiInput,
replaceCharCnt: 0
};
}
}
}
if (currentSelectionStart === currentSelectionEnd) {
// composition accept case (noticed in FF + Japanese)
// [blahblah] => blahblah|
if (
previousValue === currentValue
&& previousSelectionStart === 0
&& previousSelectionEnd === previousValue.length
&& currentSelectionStart === currentValue.length
&& currentValue.indexOf('\n') === -1
) {
if (strings.containsFullWidthCharacter(currentValue)) {
return {
text: '',
replaceCharCnt: 0
};
}
}
// no current selection
const replacePreviousCharacters = (previousPrefix.length - prefixLength);
// console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars');
return {
text: currentValue,
replaceCharCnt: replacePreviousCharacters
};
}
// there is a current selection => composition case
const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
return {
text: currentValue,
replaceCharCnt: replacePreviousCharacters
};
}
}
export class PagedScreenReaderStrategy {
private static _LINES_PER_PAGE = 10;
private static _getPageOfLine(lineNumber: number): number {
return Math.floor((lineNumber - 1) / PagedScreenReaderStrategy._LINES_PER_PAGE);
}
private static _getRangeForPage(page: number): Range {
let offset = page * PagedScreenReaderStrategy._LINES_PER_PAGE;
let startLineNumber = offset + 1;
let endLineNumber = offset + PagedScreenReaderStrategy._LINES_PER_PAGE;
return new Range(startLineNumber, 1, endLineNumber + 1, 1);
}
public static fromEditorSelection(previousState: TextAreaState, model: ISimpleModel, selection: Range): TextAreaState {
let selectionStartPage = PagedScreenReaderStrategy._getPageOfLine(selection.startLineNumber);
let selectionStartPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionStartPage);
let selectionEndPage = PagedScreenReaderStrategy._getPageOfLine(selection.endLineNumber);
let selectionEndPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionEndPage);
let pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn));
let pretext = model.getValueInRange(pretextRange, EndOfLinePreference.LF);
let lastLine = model.getLineCount();
let lastLineMaxColumn = model.getLineMaxColumn(lastLine);
let posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn));
let posttext = model.getValueInRange(posttextRange, EndOfLinePreference.LF);
let text: string = null;
if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
// take full selection
text = model.getValueInRange(selection, EndOfLinePreference.LF);
} else {
let selectionRange1 = selectionStartPageRange.intersectRanges(selection);
let selectionRange2 = selectionEndPageRange.intersectRanges(selection);
text = (
model.getValueInRange(selectionRange1, EndOfLinePreference.LF)
+ String.fromCharCode(8230)
+ model.getValueInRange(selectionRange2, EndOfLinePreference.LF)
);
}
// Chromium handles very poorly text even of a few thousand chars
// Cut text to avoid stalling the entire UI
const LIMIT_CHARS = 500;
if (pretext.length > LIMIT_CHARS) {
pretext = pretext.substring(pretext.length - LIMIT_CHARS, pretext.length);
}
if (posttext.length > LIMIT_CHARS) {
posttext = posttext.substring(0, LIMIT_CHARS);
}
if (text.length > 2 * LIMIT_CHARS) {
text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);
}
return new TextAreaState(pretext + text + posttext, pretext.length, pretext.length + text.length, new Position(selection.startLineNumber, selection.startColumn), new Position(selection.endLineNumber, selection.endColumn));
}
}

View File

@@ -0,0 +1,483 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { Position, IPosition } from 'vs/editor/common/core/position';
import { Range, IRange } from 'vs/editor/common/core/range';
import * as editorOptions from 'vs/editor/common/config/editorOptions';
import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer';
/**
* A view zone is a full horizontal rectangle that 'pushes' text down.
* The editor reserves space for view zones when rendering.
*/
export interface IViewZone {
/**
* The line number after which this zone should appear.
* Use 0 to place a view zone before the first line number.
*/
afterLineNumber: number;
/**
* The column after which this zone should appear.
* If not set, the maxLineColumn of `afterLineNumber` will be used.
*/
afterColumn?: number;
/**
* Suppress mouse down events.
* If set, the editor will attach a mouse down listener to the view zone and .preventDefault on it.
* Defaults to false
*/
suppressMouseDown?: boolean;
/**
* The height in lines of the view zone.
* If specified, `heightInPx` will be used instead of this.
* If neither `heightInPx` nor `heightInLines` is specified, a default of `heightInLines` = 1 will be chosen.
*/
heightInLines?: number;
/**
* The height in px of the view zone.
* If this is set, the editor will give preference to it rather than `heightInLines` above.
* If neither `heightInPx` nor `heightInLines` is specified, a default of `heightInLines` = 1 will be chosen.
*/
heightInPx?: number;
/**
* The dom node of the view zone
*/
domNode: HTMLElement;
/**
* An optional dom node for the view zone that will be placed in the margin area.
*/
marginDomNode?: HTMLElement;
/**
* Callback which gives the relative top of the view zone as it appears (taking scrolling into account).
*/
onDomNodeTop?: (top: number) => void;
/**
* Callback which gives the height in pixels of the view zone.
*/
onComputedHeight?: (height: number) => void;
}
/**
* An accessor that allows for zones to be added or removed.
*/
export interface IViewZoneChangeAccessor {
/**
* Create a new view zone.
* @param zone Zone to create
* @return A unique identifier to the view zone.
*/
addZone(zone: IViewZone): number;
/**
* Remove a zone
* @param id A unique identifier to the view zone, as returned by the `addZone` call.
*/
removeZone(id: number): void;
/**
* Change a zone's position.
* The editor will rescan the `afterLineNumber` and `afterColumn` properties of a view zone.
*/
layoutZone(id: number): void;
}
/**
* A positioning preference for rendering content widgets.
*/
export enum ContentWidgetPositionPreference {
/**
* Place the content widget exactly at a position
*/
EXACT,
/**
* Place the content widget above a position
*/
ABOVE,
/**
* Place the content widget below a position
*/
BELOW
}
/**
* A position for rendering content widgets.
*/
export interface IContentWidgetPosition {
/**
* Desired position for the content widget.
* `preference` will also affect the placement.
*/
position: IPosition;
/**
* Placement preference for position, in order of preference.
*/
preference: ContentWidgetPositionPreference[];
}
/**
* A content widget renders inline with the text and can be easily placed 'near' an editor position.
*/
export interface IContentWidget {
/**
* Render this content widget in a location where it could overflow the editor's view dom node.
*/
allowEditorOverflow?: boolean;
suppressMouseDown?: boolean;
/**
* Get a unique identifier of the content widget.
*/
getId(): string;
/**
* Get the dom node of the content widget.
*/
getDomNode(): HTMLElement;
/**
* Get the placement of the content widget.
* If null is returned, the content widget will be placed off screen.
*/
getPosition(): IContentWidgetPosition;
}
/**
* A positioning preference for rendering overlay widgets.
*/
export enum OverlayWidgetPositionPreference {
/**
* Position the overlay widget in the top right corner
*/
TOP_RIGHT_CORNER,
/**
* Position the overlay widget in the bottom right corner
*/
BOTTOM_RIGHT_CORNER,
/**
* Position the overlay widget in the top center
*/
TOP_CENTER
}
/**
* A position for rendering overlay widgets.
*/
export interface IOverlayWidgetPosition {
/**
* The position preference for the overlay widget.
*/
preference: OverlayWidgetPositionPreference;
}
/**
* An overlay widgets renders on top of the text.
*/
export interface IOverlayWidget {
/**
* Get a unique identifier of the overlay widget.
*/
getId(): string;
/**
* Get the dom node of the overlay widget.
*/
getDomNode(): HTMLElement;
/**
* Get the placement of the overlay widget.
* If null is returned, the overlay widget is responsible to place itself.
*/
getPosition(): IOverlayWidgetPosition;
}
/**
* Type of hit element with the mouse in the editor.
*/
export enum MouseTargetType {
/**
* Mouse is on top of an unknown element.
*/
UNKNOWN,
/**
* Mouse is on top of the textarea used for input.
*/
TEXTAREA,
/**
* Mouse is on top of the glyph margin
*/
GUTTER_GLYPH_MARGIN,
/**
* Mouse is on top of the line numbers
*/
GUTTER_LINE_NUMBERS,
/**
* Mouse is on top of the line decorations
*/
GUTTER_LINE_DECORATIONS,
/**
* Mouse is on top of the whitespace left in the gutter by a view zone.
*/
GUTTER_VIEW_ZONE,
/**
* Mouse is on top of text in the content.
*/
CONTENT_TEXT,
/**
* Mouse is on top of empty space in the content (e.g. after line text or below last line)
*/
CONTENT_EMPTY,
/**
* Mouse is on top of a view zone in the content.
*/
CONTENT_VIEW_ZONE,
/**
* Mouse is on top of a content widget.
*/
CONTENT_WIDGET,
/**
* Mouse is on top of the decorations overview ruler.
*/
OVERVIEW_RULER,
/**
* Mouse is on top of a scrollbar.
*/
SCROLLBAR,
/**
* Mouse is on top of an overlay widget.
*/
OVERLAY_WIDGET,
/**
* Mouse is outside of the editor.
*/
OUTSIDE_EDITOR,
}
/**
* Target hit with the mouse in the editor.
*/
export interface IMouseTarget {
/**
* The target element
*/
readonly element: Element;
/**
* The target type
*/
readonly type: MouseTargetType;
/**
* The 'approximate' editor position
*/
readonly position: Position;
/**
* Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line).
*/
readonly mouseColumn: number;
/**
* The 'approximate' editor range
*/
readonly range: Range;
/**
* Some extra detail.
*/
readonly detail: any;
}
/**
* A mouse event originating from the editor.
*/
export interface IEditorMouseEvent {
readonly event: IMouseEvent;
readonly target: IMouseTarget;
}
/**
* @internal
*/
export type IEditorContributionCtor = IConstructorSignature1<ICodeEditor, editorCommon.IEditorContribution>;
/**
* An overview ruler
* @internal
*/
export interface IOverviewRuler {
getDomNode(): HTMLElement;
dispose(): void;
setZones(zones: OverviewRulerZone[]): void;
setLayout(position: editorOptions.OverviewRulerPosition): void;
}
/**
* A rich code editor.
*/
export interface ICodeEditor extends editorCommon.ICommonCodeEditor {
/**
* An event emitted on a "mouseup".
* @event
*/
onMouseUp(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "mousedown".
* @event
*/
onMouseDown(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "mousedrag".
* @internal
* @event
*/
onMouseDrag(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "mousedrop".
* @internal
* @event
*/
onMouseDrop(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "contextmenu".
* @event
*/
onContextMenu(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "mousemove".
* @event
*/
onMouseMove(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "mouseleave".
* @event
*/
onMouseLeave(listener: (e: IEditorMouseEvent) => void): IDisposable;
/**
* An event emitted on a "keyup".
* @event
*/
onKeyUp(listener: (e: IKeyboardEvent) => void): IDisposable;
/**
* An event emitted on a "keydown".
* @event
*/
onKeyDown(listener: (e: IKeyboardEvent) => void): IDisposable;
/**
* An event emitted when the layout of the editor has changed.
* @event
*/
onDidLayoutChange(listener: (e: editorOptions.EditorLayoutInfo) => void): IDisposable;
/**
* An event emitted when the scroll in the editor has changed.
* @event
*/
onDidScrollChange(listener: (e: editorCommon.IScrollEvent) => void): IDisposable;
/**
* Returns the editor's dom node
*/
getDomNode(): HTMLElement;
/**
* Add a content widget. Widgets must have unique ids, otherwise they will be overwritten.
*/
addContentWidget(widget: IContentWidget): void;
/**
* Layout/Reposition a content widget. This is a ping to the editor to call widget.getPosition()
* and update appropiately.
*/
layoutContentWidget(widget: IContentWidget): void;
/**
* Remove a content widget.
*/
removeContentWidget(widget: IContentWidget): void;
/**
* Add an overlay widget. Widgets must have unique ids, otherwise they will be overwritten.
*/
addOverlayWidget(widget: IOverlayWidget): void;
/**
* Layout/Reposition an overlay widget. This is a ping to the editor to call widget.getPosition()
* and update appropiately.
*/
layoutOverlayWidget(widget: IOverlayWidget): void;
/**
* Remove an overlay widget.
*/
removeOverlayWidget(widget: IOverlayWidget): void;
/**
* Change the view zones. View zones are lost when a new model is attached to the editor.
*/
changeViewZones(callback: (accessor: IViewZoneChangeAccessor) => void): void;
/**
* Returns the range that is currently centered in the view port.
*/
getCenteredRangeInViewport(): Range;
/**
* Get the view zones.
* @internal
*/
getWhitespaces(): IEditorWhitespace[];
/**
* Get the horizontal position (left offset) for the column w.r.t to the beginning of the line.
* This method works only if the line `lineNumber` is currently rendered (in the editor's viewport).
* Use this method with caution.
*/
getOffsetForColumn(lineNumber: number, column: number): number;
/**
* Force an editor render now.
*/
render(): void;
/**
* Get the vertical position (top offset) for the line w.r.t. to the first line.
*/
getTopForLineNumber(lineNumber: number): number;
/**
* Get the vertical position (top offset) for the position w.r.t. to the first line.
*/
getTopForPosition(lineNumber: number, column: number): number;
/**
* Get the hit test target at coordinates `clientX` and `clientY`.
* The coordinates are relative to the top-left of the viewport.
*
* @returns Hit test target or null if the coordinates fall outside the editor or the editor has no model.
*/
getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget;
/**
* Get the visible position for `position`.
* The result position takes scrolling into account and is relative to the top left corner of the editor.
* Explanation 1: the results of this method will change for the same `position` if the user scrolls the editor.
* Explanation 2: the results of this method will not change if the container of the editor gets repositioned.
* Warning: the results of this method are innacurate for positions that are outside the current editor viewport.
*/
getScrolledVisiblePosition(position: IPosition): { top: number; left: number; height: number; };
/**
* Set the model ranges that will be hidden in the view.
* @internal
*/
setHiddenAreas(ranges: IRange[]): void;
/**
* @internal
*/
setAriaActiveDescendant(id: string): void;
/**
* Apply the same font settings as the editor to `target`.
*/
applyFontInfo(target: HTMLElement): void;
}
/**
* A rich diff editor.
*/
export interface IDiffEditor extends editorCommon.ICommonDiffEditor {
/**
* @see ICodeEditor.getDomNode
*/
getDomNode(): HTMLElement;
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IEditorContributionCtor } from 'vs/editor/browser/editorBrowser';
export function editorContribution(ctor: IEditorContributionCtor): void {
EditorContributionRegistry.INSTANCE.registerEditorBrowserContribution(ctor);
}
export namespace EditorBrowserRegistry {
export function getEditorContributions(): IEditorContributionCtor[] {
return EditorContributionRegistry.INSTANCE.getEditorBrowserContributions();
}
}
const Extensions = {
EditorContributions: 'editor.contributions'
};
class EditorContributionRegistry {
public static INSTANCE = new EditorContributionRegistry();
private editorContributions: IEditorContributionCtor[];
constructor() {
this.editorContributions = [];
}
public registerEditorBrowserContribution(ctor: IEditorContributionCtor): void {
this.editorContributions.push(ctor);
}
public getEditorBrowserContributions(): IEditorContributionCtor[] {
return this.editorContributions.slice(0);
}
}
Registry.add(Extensions.EditorContributions, EditorContributionRegistry.INSTANCE);

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import * as dom from 'vs/base/browser/dom';
import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor';
/**
* Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
*/
export class PageCoordinates {
_pageCoordinatesBrand: void;
public readonly x: number;
public readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public toClientCoordinates(): ClientCoordinates {
return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY);
}
}
/**
* Coordinates within the application's client area (i.e. origin is document's scroll position).
*
* For example, clicking in the top-left corner of the client area will
* always result in a mouse event with a client.x value of 0, regardless
* of whether the page is scrolled horizontally.
*/
export class ClientCoordinates {
_clientCoordinatesBrand: void;
public readonly clientX: number;
public readonly clientY: number;
constructor(clientX: number, clientY: number) {
this.clientX = clientX;
this.clientY = clientY;
}
public toPageCoordinates(): PageCoordinates {
return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY);
}
}
/**
* The position of the editor in the page.
*/
export class EditorPagePosition {
_editorPagePositionBrand: void;
public readonly x: number;
public readonly y: number;
public readonly width: number;
public readonly height: number;
constructor(x: number, y: number, width: number, height: number) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
export function createEditorPagePosition(editorViewDomNode: HTMLElement): EditorPagePosition {
let editorPos = dom.getDomNodePagePosition(editorViewDomNode);
return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
}
export class EditorMouseEvent extends StandardMouseEvent {
_editorMouseEventBrand: void;
/**
* Coordinates relative to the whole document.
*/
public readonly pos: PageCoordinates;
/**
* Editor's coordinates relative to the whole document.
*/
public readonly editorPos: EditorPagePosition;
constructor(e: MouseEvent, editorViewDomNode: HTMLElement) {
super(e);
this.pos = new PageCoordinates(this.posx, this.posy);
this.editorPos = createEditorPagePosition(editorViewDomNode);
}
}
export interface EditorMouseEventMerger {
(lastEvent: EditorMouseEvent, currentEvent: EditorMouseEvent): EditorMouseEvent;
}
export class EditorMouseEventFactory {
private _editorViewDomNode: HTMLElement;
constructor(editorViewDomNode: HTMLElement) {
this._editorViewDomNode = editorViewDomNode;
}
private _create(e: MouseEvent): EditorMouseEvent {
return new EditorMouseEvent(e, this._editorViewDomNode);
}
public onContextMenu(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
return dom.addDisposableListener(target, 'contextmenu', (e: MouseEvent) => {
callback(this._create(e));
});
}
public onMouseUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
return dom.addDisposableListener(target, 'mouseup', (e: MouseEvent) => {
callback(this._create(e));
});
}
public onMouseDown(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
return dom.addDisposableListener(target, 'mousedown', (e: MouseEvent) => {
callback(this._create(e));
});
}
public onMouseLeave(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
return dom.addDisposableNonBubblingMouseOutListener(target, (e: MouseEvent) => {
callback(this._create(e));
});
}
public onMouseMoveThrottled(target: HTMLElement, callback: (e: EditorMouseEvent) => void, merger: EditorMouseEventMerger, minimumTimeMs: number): IDisposable {
let myMerger: dom.IEventMerger<EditorMouseEvent> = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => {
return merger(lastEvent, this._create(currentEvent));
};
return dom.addDisposableThrottledListener<EditorMouseEvent>(target, 'mousemove', callback, myMerger, minimumTimeMs);
}
}
export class GlobalEditorMouseMoveMonitor extends Disposable {
private _editorViewDomNode: HTMLElement;
private _globalMouseMoveMonitor: GlobalMouseMoveMonitor<EditorMouseEvent>;
private _keydownListener: IDisposable;
constructor(editorViewDomNode: HTMLElement) {
super();
this._editorViewDomNode = editorViewDomNode;
this._globalMouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<EditorMouseEvent>());
this._keydownListener = null;
}
public startMonitoring(merger: EditorMouseEventMerger, mouseMoveCallback: (e: EditorMouseEvent) => void, onStopCallback: () => void): void {
// Add a <<capture>> keydown event listener that will cancel the monitoring
// if something other than a modifier key is pressed
this._keydownListener = dom.addStandardDisposableListener(<any>document, 'keydown', (e) => {
const kb = e.toKeybinding();
if (kb.isModifierKey()) {
// Allow modifier keys
return;
}
this._globalMouseMoveMonitor.stopMonitoring(true);
}, true);
let myMerger: dom.IEventMerger<EditorMouseEvent> = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => {
return merger(lastEvent, new EditorMouseEvent(currentEvent, this._editorViewDomNode));
};
this._globalMouseMoveMonitor.startMonitoring(myMerger, mouseMoveCallback, () => {
this._keydownListener.dispose();
onStopCallback();
});
}
}

View File

@@ -0,0 +1,476 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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,
IContentDecorationRenderOptions, OverviewRulerLane, TrackedRangeStickiness, isThemeColor
} from 'vs/editor/common/editorCommon';
import { AbstractCodeEditorService } from 'vs/editor/common/services/abstractCodeEditorService';
import { IDisposable, dispose as disposeAll } from 'vs/base/common/lifecycle';
import { IThemeService, ITheme, ThemeColor } from 'vs/platform/theme/common/themeService';
export class CodeEditorServiceImpl extends AbstractCodeEditorService {
private _styleSheet: HTMLStyleElement;
private _decorationOptionProviders: { [key: string]: IModelDecorationOptionsProvider };
private _themeService: IThemeService;
constructor( @IThemeService themeService: IThemeService, styleSheet = dom.createStyleSheet()) {
super();
this._styleSheet = styleSheet;
this._decorationOptionProviders = Object.create(null);
this._themeService = themeService;
}
public registerDecorationType(key: string, options: IDecorationRenderOptions, parentTypeKey?: string): void {
let provider = this._decorationOptionProviders[key];
if (!provider) {
let providerArgs: ProviderArguments = {
styleSheet: this._styleSheet,
key: key,
parentTypeKey: parentTypeKey,
options: options
};
if (!parentTypeKey) {
provider = new DecorationTypeOptionsProvider(this._themeService, providerArgs);
} else {
provider = new DecorationSubTypeOptionsProvider(this._themeService, providerArgs);
}
this._decorationOptionProviders[key] = provider;
}
provider.refCount++;
}
public removeDecorationType(key: string): void {
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 resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions {
let provider = this._decorationOptionProviders[decorationTypeKey];
if (!provider) {
throw new Error('Unknown decoration type key: ' + decorationTypeKey);
}
return provider.getOptions(this, writable);
}
}
interface IModelDecorationOptionsProvider extends IDisposable {
refCount: number;
getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions;
}
class DecorationSubTypeOptionsProvider implements IModelDecorationOptionsProvider {
public refCount: number;
private _parentTypeKey: string;
private _beforeContentRules: DecorationCSSRules;
private _afterContentRules: DecorationCSSRules;
constructor(themeService: IThemeService, providerArgs: ProviderArguments) {
this._parentTypeKey = providerArgs.parentTypeKey;
this.refCount = 0;
this._beforeContentRules = new DecorationCSSRules(ModelDecorationCSSRuleType.BeforeContentClassName, providerArgs, themeService);
this._afterContentRules = new DecorationCSSRules(ModelDecorationCSSRuleType.AfterContentClassName, providerArgs, themeService);
}
public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions {
let options = codeEditorService.resolveDecorationOptions(this._parentTypeKey, true);
if (this._beforeContentRules) {
options.beforeContentClassName = this._beforeContentRules.className;
}
if (this._afterContentRules) {
options.afterContentClassName = this._afterContentRules.className;
}
return options;
}
public dispose(): void {
if (this._beforeContentRules) {
this._beforeContentRules.dispose();
this._beforeContentRules = null;
}
if (this._afterContentRules) {
this._afterContentRules.dispose();
this._afterContentRules = null;
}
}
}
interface ProviderArguments {
styleSheet: HTMLStyleElement;
key: string;
parentTypeKey?: string;
options: IDecorationRenderOptions;
}
class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider {
private _disposables: IDisposable[];
public refCount: number;
public className: string;
public inlineClassName: string;
public beforeContentClassName: string;
public afterContentClassName: string;
public glyphMarginClassName: string;
public isWholeLine: boolean;
public overviewRuler: IModelDecorationOverviewRulerOptions;
public stickiness: TrackedRangeStickiness;
constructor(themeService: IThemeService, providerArgs: ProviderArguments) {
this.refCount = 0;
this._disposables = [];
let createCSSRules = (type: ModelDecorationCSSRuleType) => {
let rules = new DecorationCSSRules(type, providerArgs, themeService);
if (rules.hasContent) {
this._disposables.push(rules);
return rules.className;
}
return void 0;
};
this.className = createCSSRules(ModelDecorationCSSRuleType.ClassName);
this.inlineClassName = createCSSRules(ModelDecorationCSSRuleType.InlineClassName);
this.beforeContentClassName = createCSSRules(ModelDecorationCSSRuleType.BeforeContentClassName);
this.afterContentClassName = createCSSRules(ModelDecorationCSSRuleType.AfterContentClassName);
this.glyphMarginClassName = createCSSRules(ModelDecorationCSSRuleType.GlyphMarginClassName);
let options = providerArgs.options;
this.isWholeLine = Boolean(options.isWholeLine);
this.stickiness = options.rangeBehavior;
let lightOverviewRulerColor = options.light && options.light.overviewRulerColor || options.overviewRulerColor;
let darkOverviewRulerColor = options.dark && options.dark.overviewRulerColor || options.overviewRulerColor;
if (
typeof lightOverviewRulerColor !== 'undefined'
|| typeof darkOverviewRulerColor !== 'undefined'
) {
this.overviewRuler = {
color: lightOverviewRulerColor || darkOverviewRulerColor,
darkColor: darkOverviewRulerColor || lightOverviewRulerColor,
position: options.overviewRulerLane || OverviewRulerLane.Center
};
}
}
public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions {
if (!writable) {
return this;
}
return {
inlineClassName: this.inlineClassName,
beforeContentClassName: this.beforeContentClassName,
afterContentClassName: this.afterContentClassName,
className: this.className,
glyphMarginClassName: this.glyphMarginClassName,
isWholeLine: this.isWholeLine,
overviewRuler: this.overviewRuler,
stickiness: this.stickiness
};
}
public dispose(): void {
this._disposables = disposeAll(this._disposables);
}
}
const _CSS_MAP = {
color: 'color:{0} !important;',
backgroundColor: 'background-color:{0};',
outline: 'outline:{0};',
outlineColor: 'outline-color:{0};',
outlineStyle: 'outline-style:{0};',
outlineWidth: 'outline-width:{0};',
border: 'border:{0};',
borderColor: 'border-color:{0};',
borderRadius: 'border-radius:{0};',
borderSpacing: 'border-spacing:{0};',
borderStyle: 'border-style:{0};',
borderWidth: 'border-width:{0};',
textDecoration: 'text-decoration:{0};',
cursor: 'cursor:{0};',
letterSpacing: 'letter-spacing:{0};',
gutterIconPath: 'background:url(\'{0}\') center center no-repeat;',
gutterIconSize: 'background-size:{0};',
contentText: 'content:\'{0}\';',
contentIconPath: 'content:url(\'{0}\');',
margin: 'margin:{0};',
width: 'width:{0};',
height: 'height:{0};'
};
class DecorationCSSRules {
private _theme: ITheme;
private _className: string;
private _unThemedSelector: string;
private _hasContent: boolean;
private _ruleType: ModelDecorationCSSRuleType;
private _themeListener: IDisposable;
private _providerArgs: ProviderArguments;
private _usesThemeColors: boolean;
public constructor(ruleType: ModelDecorationCSSRuleType, providerArgs: ProviderArguments, themeService: IThemeService) {
this._theme = themeService.getTheme();
this._ruleType = ruleType;
this._providerArgs = providerArgs;
this._usesThemeColors = false;
this._hasContent = false;
let className = CSSNameHelper.getClassName(this._providerArgs.key, ruleType);
if (this._providerArgs.parentTypeKey) {
className = className + ' ' + CSSNameHelper.getClassName(this._providerArgs.parentTypeKey, ruleType);
}
this._className = className;
this._unThemedSelector = CSSNameHelper.getSelector(this._providerArgs.key, this._providerArgs.parentTypeKey, ruleType);
this._buildCSS();
if (this._usesThemeColors) {
this._themeListener = themeService.onThemeChange(theme => {
this._theme = themeService.getTheme();
this._removeCSS();
this._buildCSS();
});
}
}
public dispose() {
if (this._hasContent) {
this._removeCSS();
this._hasContent = false;
}
if (this._themeListener) {
this._themeListener.dispose();
this._themeListener = null;
}
}
public get hasContent(): boolean {
return this._hasContent;
}
public get className(): string {
return this._className;
}
private _buildCSS(): void {
let options = this._providerArgs.options;
let unthemedCSS, lightCSS, darkCSS: string;
switch (this._ruleType) {
case ModelDecorationCSSRuleType.ClassName:
unthemedCSS = this.getCSSTextForModelDecorationClassName(options);
lightCSS = this.getCSSTextForModelDecorationClassName(options.light);
darkCSS = this.getCSSTextForModelDecorationClassName(options.dark);
break;
case ModelDecorationCSSRuleType.InlineClassName:
unthemedCSS = this.getCSSTextForModelDecorationInlineClassName(options);
lightCSS = this.getCSSTextForModelDecorationInlineClassName(options.light);
darkCSS = this.getCSSTextForModelDecorationInlineClassName(options.dark);
break;
case ModelDecorationCSSRuleType.GlyphMarginClassName:
unthemedCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options);
lightCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options.light);
darkCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options.dark);
break;
case ModelDecorationCSSRuleType.BeforeContentClassName:
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.before);
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.before);
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.before);
break;
case ModelDecorationCSSRuleType.AfterContentClassName:
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.after);
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.after);
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.after);
break;
default:
throw new Error('Unknown rule type: ' + this._ruleType);
}
let sheet = <CSSStyleSheet>this._providerArgs.styleSheet.sheet;
let hasContent = false;
if (unthemedCSS.length > 0) {
sheet.insertRule(`${this._unThemedSelector} {${unthemedCSS}}`, 0);
hasContent = true;
}
if (lightCSS.length > 0) {
sheet.insertRule(`.vs${this._unThemedSelector} {${lightCSS}}`, 0);
hasContent = true;
}
if (darkCSS.length > 0) {
sheet.insertRule(`.vs-dark${this._unThemedSelector}, .hc-black${this._unThemedSelector} {${darkCSS}}`, 0);
hasContent = true;
}
this._hasContent = hasContent;
}
private _removeCSS(): void {
dom.removeCSSRulesContainingSelector(this._unThemedSelector, this._providerArgs.styleSheet);
}
/**
* Build the CSS for decorations styled via `className`.
*/
private getCSSTextForModelDecorationClassName(opts: IThemeDecorationRenderOptions): string {
if (!opts) {
return '';
}
let cssTextArr: string[] = [];
this.collectCSSText(opts, ['backgroundColor'], cssTextArr);
this.collectCSSText(opts, ['outline', 'outlineColor', 'outlineStyle', 'outlineWidth'], cssTextArr);
this.collectBorderSettingsCSSText(opts, cssTextArr);
return cssTextArr.join('');
}
/**
* Build the CSS for decorations styled via `inlineClassName`.
*/
private getCSSTextForModelDecorationInlineClassName(opts: IThemeDecorationRenderOptions): string {
if (!opts) {
return '';
}
let cssTextArr: string[] = [];
this.collectCSSText(opts, ['textDecoration', 'cursor', 'color', 'letterSpacing'], cssTextArr);
return cssTextArr.join('');
}
/**
* Build the CSS for decorations styled before or after content.
*/
private getCSSTextForModelDecorationContentClassName(opts: IContentDecorationRenderOptions): string {
if (!opts) {
return '';
}
let cssTextArr: string[] = [];
if (typeof opts !== 'undefined') {
this.collectBorderSettingsCSSText(opts, cssTextArr);
if (typeof opts.contentIconPath === 'string') {
cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, URI.file(opts.contentIconPath).toString().replace(/'/g, '%27')));
} else if (opts.contentIconPath instanceof URI) {
cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, opts.contentIconPath.toString(true).replace(/'/g, '%27')));
}
if (typeof opts.contentText === 'string') {
const truncated = opts.contentText.match(/^.*$/m)[0]; // only take first line
const escaped = truncated.replace(/['\\]/g, '\\$&');
cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped));
}
this.collectCSSText(opts, ['textDecoration', 'color', 'backgroundColor', 'margin'], cssTextArr);
if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) {
cssTextArr.push('display:inline-block;');
}
}
return cssTextArr.join('');
}
/**
* Build the CSS for decorations styled via `glpyhMarginClassName`.
*/
private getCSSTextForModelDecorationGlyphMarginClassName(opts: IThemeDecorationRenderOptions): string {
if (!opts) {
return '';
}
let cssTextArr = [];
if (typeof opts.gutterIconPath !== 'undefined') {
if (typeof opts.gutterIconPath === 'string') {
cssTextArr.push(strings.format(_CSS_MAP.gutterIconPath, URI.file(opts.gutterIconPath).toString()));
} else {
cssTextArr.push(strings.format(_CSS_MAP.gutterIconPath, opts.gutterIconPath.toString(true).replace(/'/g, '%27')));
}
if (typeof opts.gutterIconSize !== 'undefined') {
cssTextArr.push(strings.format(_CSS_MAP.gutterIconSize, opts.gutterIconSize));
}
}
return cssTextArr.join('');
}
private collectBorderSettingsCSSText(opts: any, cssTextArr: string[]): boolean {
if (this.collectCSSText(opts, ['border', 'borderColor', 'borderRadius', 'borderSpacing', 'borderStyle', 'borderWidth'], cssTextArr)) {
cssTextArr.push(strings.format('box-sizing: border-box;'));
return true;
}
return false;
}
private collectCSSText(opts: any, properties: string[], cssTextArr: string[]): boolean {
let lenBefore = cssTextArr.length;
for (let property of properties) {
let value = this.resolveValue(opts[property]);
if (typeof value === 'string') {
cssTextArr.push(strings.format(_CSS_MAP[property], value));
}
}
return cssTextArr.length !== lenBefore;
}
private resolveValue(value: string | ThemeColor): string {
if (isThemeColor(value)) {
this._usesThemeColors = true;
let color = this._theme.getColor(value.id);
if (color) {
return color.toString();
}
return 'transparent';
}
return value;
}
}
const enum ModelDecorationCSSRuleType {
ClassName = 0,
InlineClassName = 1,
GlyphMarginClassName = 2,
BeforeContentClassName = 3,
AfterContentClassName = 4
}
class CSSNameHelper {
public static getClassName(key: string, type: ModelDecorationCSSRuleType): string {
return 'ced-' + key + '-' + type;
}
public static getSelector(key: string, parentKey: string, ruleType: ModelDecorationCSSRuleType): string {
let selector = '.monaco-editor .' + this.getClassName(key, ruleType);
if (parentKey) {
selector = selector + '.' + this.getClassName(parentKey, ruleType);
}
if (ruleType === ModelDecorationCSSRuleType.BeforeContentClassName) {
selector += '::before';
} else if (ruleType === ModelDecorationCSSRuleType.AfterContentClassName) {
selector += '::after';
}
return selector;
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
export abstract class DynamicViewOverlay extends ViewEventHandler {
public abstract prepareRender(ctx: RenderingContext): void;
public abstract render(startLineNumber: number, lineNumber: number): string;
}

View File

@@ -0,0 +1,313 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IEditorMouseEvent } from 'vs/editor/browser/editorBrowser';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents';
import { CoreNavigationCommands, CoreEditorCommand } from 'vs/editor/common/controller/coreCommands';
import { Configuration } from 'vs/editor/browser/config/configuration';
export interface ExecCoreEditorCommandFunc {
(editorCommand: CoreEditorCommand, args: any): void;
}
export interface IMouseDispatchData {
position: Position;
/**
* Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line).
*/
mouseColumn: number;
startedOnLineNumbers: boolean;
inSelectionMode: boolean;
mouseDownCount: number;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
}
export class ViewController {
private readonly configuration: Configuration;
private readonly viewModel: IViewModel;
private readonly _execCoreEditorCommandFunc: ExecCoreEditorCommandFunc;
private readonly outgoingEvents: ViewOutgoingEvents;
private readonly commandService: ICommandService;
constructor(
configuration: Configuration,
viewModel: IViewModel,
execCommandFunc: ExecCoreEditorCommandFunc,
outgoingEvents: ViewOutgoingEvents,
commandService: ICommandService
) {
this.configuration = configuration;
this.viewModel = viewModel;
this._execCoreEditorCommandFunc = execCommandFunc;
this.outgoingEvents = outgoingEvents;
this.commandService = commandService;
}
private _execMouseCommand(editorCommand: CoreEditorCommand, args: any): void {
args.source = 'mouse';
this._execCoreEditorCommandFunc(editorCommand, args);
}
public paste(source: string, text: string, pasteOnNewLine: boolean): void {
this.commandService.executeCommand(editorCommon.Handler.Paste, {
text: text,
pasteOnNewLine: pasteOnNewLine,
});
}
public type(source: string, text: string): void {
this.commandService.executeCommand(editorCommon.Handler.Type, {
text: text
});
}
public replacePreviousChar(source: string, text: string, replaceCharCnt: number): void {
this.commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, {
text: text,
replaceCharCnt: replaceCharCnt
});
}
public compositionStart(source: string): void {
this.commandService.executeCommand(editorCommon.Handler.CompositionStart, {});
}
public compositionEnd(source: string): void {
this.commandService.executeCommand(editorCommon.Handler.CompositionEnd, {});
}
public cut(source: string): void {
this.commandService.executeCommand(editorCommon.Handler.Cut, {});
}
public setSelection(source: string, modelSelection: Selection): void {
this._execCoreEditorCommandFunc(CoreNavigationCommands.SetSelection, {
source: source,
selection: modelSelection
});
}
private _validateViewColumn(viewPosition: Position): Position {
let minColumn = this.viewModel.getLineMinColumn(viewPosition.lineNumber);
if (viewPosition.column < minColumn) {
return new Position(viewPosition.lineNumber, minColumn);
}
return viewPosition;
}
private _hasMulticursorModifier(data: IMouseDispatchData): boolean {
switch (this.configuration.editor.multiCursorModifier) {
case 'altKey':
return data.altKey;
case 'ctrlKey':
return data.ctrlKey;
case 'metaKey':
return data.metaKey;
}
return false;
}
private _hasNonMulticursorModifier(data: IMouseDispatchData): boolean {
switch (this.configuration.editor.multiCursorModifier) {
case 'altKey':
return data.ctrlKey || data.metaKey;
case 'ctrlKey':
return data.altKey || data.metaKey;
case 'metaKey':
return data.ctrlKey || data.altKey;
}
return false;
}
public dispatchMouse(data: IMouseDispatchData): void {
if (data.startedOnLineNumbers) {
// If the dragging started on the gutter, then have operations work on the entire line
if (this._hasMulticursorModifier(data)) {
if (data.inSelectionMode) {
this.lastCursorLineSelect(data.position);
} else {
this.createCursor(data.position, true);
}
} else {
if (data.inSelectionMode) {
this.lineSelectDrag(data.position);
} else {
this.lineSelect(data.position);
}
}
} else if (data.mouseDownCount >= 4) {
this.selectAll();
} else if (data.mouseDownCount === 3) {
if (this._hasMulticursorModifier(data)) {
if (data.inSelectionMode) {
this.lastCursorLineSelectDrag(data.position);
} else {
this.lastCursorLineSelect(data.position);
}
} else {
if (data.inSelectionMode) {
this.lineSelectDrag(data.position);
} else {
this.lineSelect(data.position);
}
}
} else if (data.mouseDownCount === 2) {
if (this._hasMulticursorModifier(data)) {
this.lastCursorWordSelect(data.position);
} else {
if (data.inSelectionMode) {
this.wordSelectDrag(data.position);
} else {
this.wordSelect(data.position);
}
}
} else {
if (this._hasMulticursorModifier(data)) {
if (!this._hasNonMulticursorModifier(data)) {
if (data.shiftKey) {
this.columnSelect(data.position, data.mouseColumn);
} else {
// Do multi-cursor operations only when purely alt is pressed
if (data.inSelectionMode) {
this.lastCursorMoveToSelect(data.position);
} else {
this.createCursor(data.position, false);
}
}
}
} else {
if (data.inSelectionMode) {
this.moveToSelect(data.position);
} else {
this.moveTo(data.position);
}
}
}
}
private _usualArgs(viewPosition: Position) {
viewPosition = this._validateViewColumn(viewPosition);
return {
position: this.convertViewToModelPosition(viewPosition),
viewPosition: viewPosition
};
}
public moveTo(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.MoveTo, this._usualArgs(viewPosition));
}
private moveToSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.MoveToSelect, this._usualArgs(viewPosition));
}
private columnSelect(viewPosition: Position, mouseColumn: number): void {
viewPosition = this._validateViewColumn(viewPosition);
this._execMouseCommand(CoreNavigationCommands.ColumnSelect, {
position: this.convertViewToModelPosition(viewPosition),
viewPosition: viewPosition,
mouseColumn: mouseColumn
});
}
private createCursor(viewPosition: Position, wholeLine: boolean): void {
viewPosition = this._validateViewColumn(viewPosition);
this._execMouseCommand(CoreNavigationCommands.CreateCursor, {
position: this.convertViewToModelPosition(viewPosition),
viewPosition: viewPosition,
wholeLine: wholeLine
});
}
private lastCursorMoveToSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LastCursorMoveToSelect, this._usualArgs(viewPosition));
}
private wordSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.WordSelect, this._usualArgs(viewPosition));
}
private wordSelectDrag(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.WordSelectDrag, this._usualArgs(viewPosition));
}
private lastCursorWordSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LastCursorWordSelect, this._usualArgs(viewPosition));
}
private lineSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LineSelect, this._usualArgs(viewPosition));
}
private lineSelectDrag(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LineSelectDrag, this._usualArgs(viewPosition));
}
private lastCursorLineSelect(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LastCursorLineSelect, this._usualArgs(viewPosition));
}
private lastCursorLineSelectDrag(viewPosition: Position): void {
this._execMouseCommand(CoreNavigationCommands.LastCursorLineSelectDrag, this._usualArgs(viewPosition));
}
private selectAll(): void {
this._execMouseCommand(CoreNavigationCommands.SelectAll, {});
}
// ----------------------
private convertViewToModelPosition(viewPosition: Position): Position {
return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(viewPosition);
}
public emitKeyDown(e: IKeyboardEvent): void {
this.outgoingEvents.emitKeyDown(e);
}
public emitKeyUp(e: IKeyboardEvent): void {
this.outgoingEvents.emitKeyUp(e);
}
public emitContextMenu(e: IEditorMouseEvent): void {
this.outgoingEvents.emitContextMenu(e);
}
public emitMouseMove(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseMove(e);
}
public emitMouseLeave(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseLeave(e);
}
public emitMouseUp(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseUp(e);
}
public emitMouseDown(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseDown(e);
}
public emitMouseDrag(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseDrag(e);
}
public emitMouseDrop(e: IEditorMouseEvent): void {
this.outgoingEvents.emitMouseDrop(e);
}
}

View File

@@ -0,0 +1,597 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { Range } from 'vs/editor/common/core/range';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { TextAreaHandler, ITextAreaHandlerHelper } from 'vs/editor/browser/controller/textAreaHandler';
import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { ViewController, ExecCoreEditorCommandFunc } from 'vs/editor/browser/view/viewController';
import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher';
import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays';
import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/contentWidgets';
import { CurrentLineHighlightOverlay } from 'vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight';
import { CurrentLineMarginHighlightOverlay } from 'vs/editor/browser/viewParts/currentLineMarginHighlight/currentLineMarginHighlight';
import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations';
import { GlyphMarginOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides';
import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines';
import { Margin } from 'vs/editor/browser/viewParts/margin/margin';
import { LinesDecorationsOverlay } from 'vs/editor/browser/viewParts/linesDecorations/linesDecorations';
import { MarginViewLineDecorationsOverlay } from 'vs/editor/browser/viewParts/marginDecorations/marginDecorations';
import { ViewOverlayWidgets } from 'vs/editor/browser/viewParts/overlayWidgets/overlayWidgets';
import { DecorationsOverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler';
import { OverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/overviewRuler';
import { Rulers } from 'vs/editor/browser/viewParts/rulers/rulers';
import { ScrollDecorationViewPart } from 'vs/editor/browser/viewParts/scrollDecoration/scrollDecoration';
import { SelectionsOverlay } from 'vs/editor/browser/viewParts/selections/selections';
import { ViewCursors } from 'vs/editor/browser/viewParts/viewCursors/viewCursors';
import { ViewZones } from 'vs/editor/browser/viewParts/viewZones/viewZones';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler';
import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar';
import { Minimap } from 'vs/editor/browser/viewParts/minimap/minimap';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { IThemeService, getThemeTypeSelector } from 'vs/platform/theme/common/themeService';
import { Cursor } from 'vs/editor/common/controller/cursor';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
export interface IContentWidgetData {
widget: editorBrowser.IContentWidget;
position: editorBrowser.IContentWidgetPosition;
}
export interface IOverlayWidgetData {
widget: editorBrowser.IOverlayWidget;
position: editorBrowser.IOverlayWidgetPosition;
}
export class View extends ViewEventHandler {
private eventDispatcher: ViewEventDispatcher;
private _scrollbar: EditorScrollbar;
private _context: ViewContext;
private _cursor: Cursor;
// The view lines
private viewLines: ViewLines;
// These are parts, but we must do some API related calls on them, so we keep a reference
private viewZones: ViewZones;
private contentWidgets: ViewContentWidgets;
private overlayWidgets: ViewOverlayWidgets;
private viewCursors: ViewCursors;
private viewParts: ViewPart[];
private readonly _textAreaHandler: TextAreaHandler;
private readonly pointerHandler: PointerHandler;
private readonly outgoingEvents: ViewOutgoingEvents;
// Dom nodes
private linesContent: FastDomNode<HTMLElement>;
public domNode: FastDomNode<HTMLElement>;
private overflowGuardContainer: FastDomNode<HTMLElement>;
// Actual mutable state
private _isDisposed: boolean;
private _renderAnimationFrame: IDisposable;
constructor(
commandService: ICommandService,
configuration: Configuration,
themeService: IThemeService,
model: IViewModel,
cursor: Cursor,
execCoreEditorCommandFunc: ExecCoreEditorCommandFunc
) {
super();
this._isDisposed = false;
this._cursor = cursor;
this._renderAnimationFrame = null;
this.outgoingEvents = new ViewOutgoingEvents(model);
let viewController = new ViewController(configuration, model, execCoreEditorCommandFunc, this.outgoingEvents, commandService);
// The event dispatcher will always go through _renderOnce before dispatching any events
this.eventDispatcher = new ViewEventDispatcher((callback: () => void) => this._renderOnce(callback));
// Ensure the view is the first event handler in order to update the layout
this.eventDispatcher.addEventHandler(this);
// The view context is passed on to most classes (basically to reduce param. counts in ctors)
this._context = new ViewContext(configuration, themeService.getTheme(), model, this.eventDispatcher);
this._register(themeService.onThemeChange(theme => {
this._context.theme = theme;
this.eventDispatcher.emit(new viewEvents.ViewThemeChangedEvent());
this.render(true, false);
}));
this.viewParts = [];
// Keyboard handler
this._textAreaHandler = new TextAreaHandler(this._context, viewController, this.createTextAreaHandlerHelper());
this.viewParts.push(this._textAreaHandler);
this.createViewParts();
this._setLayout();
// Pointer handler
this.pointerHandler = new PointerHandler(this._context, viewController, this.createPointerHandlerHelper());
this._register(model.addEventListener((events: viewEvents.ViewEvent[]) => {
this.eventDispatcher.emitMany(events);
}));
this._register(this._cursor.addEventListener((events: viewEvents.ViewEvent[]) => {
this.eventDispatcher.emitMany(events);
}));
}
private createViewParts(): void {
// These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.)
this.linesContent = createFastDomNode(document.createElement('div'));
this.linesContent.setClassName('lines-content' + ' monaco-editor-background');
this.linesContent.setPosition('absolute');
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName(this.getEditorClassName());
this.overflowGuardContainer = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this.overflowGuardContainer, PartFingerprint.OverflowGuard);
this.overflowGuardContainer.setClassName('overflow-guard');
this._scrollbar = new EditorScrollbar(this._context, this.linesContent, this.domNode, this.overflowGuardContainer);
this.viewParts.push(this._scrollbar);
// View Lines
this.viewLines = new ViewLines(this._context, this.linesContent);
// View Zones
this.viewZones = new ViewZones(this._context);
this.viewParts.push(this.viewZones);
// Decorations overview ruler
let decorationsOverviewRuler = new DecorationsOverviewRuler(this._context);
this.viewParts.push(decorationsOverviewRuler);
let scrollDecoration = new ScrollDecorationViewPart(this._context);
this.viewParts.push(scrollDecoration);
let contentViewOverlays = new ContentViewOverlays(this._context);
this.viewParts.push(contentViewOverlays);
contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new IndentGuidesOverlay(this._context));
let marginViewOverlays = new MarginViewOverlays(this._context);
this.viewParts.push(marginViewOverlays);
marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context));
let margin = new Margin(this._context);
margin.getDomNode().appendChild(this.viewZones.marginDomNode);
margin.getDomNode().appendChild(marginViewOverlays.getDomNode());
this.viewParts.push(margin);
// Content widgets
this.contentWidgets = new ViewContentWidgets(this._context, this.domNode);
this.viewParts.push(this.contentWidgets);
this.viewCursors = new ViewCursors(this._context);
this.viewParts.push(this.viewCursors);
// Overlay widgets
this.overlayWidgets = new ViewOverlayWidgets(this._context);
this.viewParts.push(this.overlayWidgets);
let rulers = new Rulers(this._context);
this.viewParts.push(rulers);
let minimap = new Minimap(this._context);
this.viewParts.push(minimap);
// -------------- Wire dom nodes up
if (decorationsOverviewRuler) {
let overviewRulerData = this._scrollbar.getOverviewRulerLayoutInfo();
overviewRulerData.parent.insertBefore(decorationsOverviewRuler.getDomNode(), overviewRulerData.insertBefore);
}
this.linesContent.appendChild(contentViewOverlays.getDomNode());
this.linesContent.appendChild(rulers.domNode);
this.linesContent.appendChild(this.viewZones.domNode);
this.linesContent.appendChild(this.viewLines.getDomNode());
this.linesContent.appendChild(this.contentWidgets.domNode);
this.linesContent.appendChild(this.viewCursors.getDomNode());
this.overflowGuardContainer.appendChild(margin.getDomNode());
this.overflowGuardContainer.appendChild(this._scrollbar.getDomNode());
this.overflowGuardContainer.appendChild(scrollDecoration.getDomNode());
this.overflowGuardContainer.appendChild(this._textAreaHandler.textArea);
this.overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover);
this.overflowGuardContainer.appendChild(this.overlayWidgets.getDomNode());
this.overflowGuardContainer.appendChild(minimap.getDomNode());
this.domNode.appendChild(this.overflowGuardContainer);
this.domNode.appendChild(this.contentWidgets.overflowingContentWidgetsDomNode);
}
private _flushAccumulatedAndRenderNow(): void {
this._renderNow();
}
private createPointerHandlerHelper(): IPointerHandlerHelper {
return {
viewDomNode: this.domNode.domNode,
linesContentDomNode: this.linesContent.domNode,
focusTextArea: () => {
this.focus();
},
getLastViewCursorsRenderData: () => {
return this.viewCursors.getLastRenderData() || [];
},
shouldSuppressMouseDownOnViewZone: (viewZoneId: number) => {
return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId);
},
shouldSuppressMouseDownOnWidget: (widgetId: string) => {
return this.contentWidgets.shouldSuppressMouseDownOnWidget(widgetId);
},
getPositionFromDOMInfo: (spanNode: HTMLElement, offset: number) => {
this._flushAccumulatedAndRenderNow();
return this.viewLines.getPositionFromDOMInfo(spanNode, offset);
},
visibleRangeForPosition2: (lineNumber: number, column: number) => {
this._flushAccumulatedAndRenderNow();
let visibleRanges = this.viewLines.visibleRangesForRange2(new Range(lineNumber, column, lineNumber, column));
if (!visibleRanges) {
return null;
}
return visibleRanges[0];
},
getLineWidth: (lineNumber: number) => {
this._flushAccumulatedAndRenderNow();
return this.viewLines.getLineWidth(lineNumber);
}
};
}
private createTextAreaHandlerHelper(): ITextAreaHandlerHelper {
return {
visibleRangeForPositionRelativeToEditor: (lineNumber: number, column: number) => {
this._flushAccumulatedAndRenderNow();
let visibleRanges = this.viewLines.visibleRangesForRange2(new Range(lineNumber, column, lineNumber, column));
if (!visibleRanges) {
return null;
}
return visibleRanges[0];
}
};
}
private _setLayout(): void {
const layoutInfo = this._context.configuration.editor.layoutInfo;
this.domNode.setWidth(layoutInfo.width);
this.domNode.setHeight(layoutInfo.height);
this.overflowGuardContainer.setWidth(layoutInfo.width);
this.overflowGuardContainer.setHeight(layoutInfo.height);
this.linesContent.setWidth(1000000);
this.linesContent.setHeight(1000000);
}
private getEditorClassName() {
return this._context.configuration.editor.editorClassName + ' ' + getThemeTypeSelector(this._context.theme.type);
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.editorClassName) {
this.domNode.setClassName(this.getEditorClassName());
}
if (e.layoutInfo) {
this._setLayout();
}
return false;
}
public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
this.domNode.toggleClassName('focused', e.isFocused);
if (e.isFocused) {
this.outgoingEvents.emitViewFocusGained();
} else {
this.outgoingEvents.emitViewFocusLost();
}
return false;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this.outgoingEvents.emitScrollChanged(e);
return false;
}
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
this.domNode.setClassName(this.getEditorClassName());
return false;
}
// --- end event handlers
public dispose(): void {
this._isDisposed = true;
if (this._renderAnimationFrame !== null) {
this._renderAnimationFrame.dispose();
this._renderAnimationFrame = null;
}
this.eventDispatcher.removeEventHandler(this);
this.outgoingEvents.dispose();
this.pointerHandler.dispose();
this.viewLines.dispose();
// Destroy view parts
for (let i = 0, len = this.viewParts.length; i < len; i++) {
this.viewParts[i].dispose();
}
this.viewParts = [];
super.dispose();
}
private _renderOnce(callback: () => any): any {
let r = safeInvokeNoArg(callback);
this._scheduleRender();
return r;
}
private _scheduleRender(): void {
if (this._renderAnimationFrame === null) {
this._renderAnimationFrame = dom.runAtThisOrScheduleAtNextAnimationFrame(this._onRenderScheduled.bind(this), 100);
}
}
private _onRenderScheduled(): void {
this._renderAnimationFrame = null;
this._flushAccumulatedAndRenderNow();
}
private _renderNow(): void {
safeInvokeNoArg(() => this._actualRender());
}
private _getViewPartsToRender(): ViewPart[] {
let result: ViewPart[] = [], resultLen = 0;
for (let i = 0, len = this.viewParts.length; i < len; i++) {
let viewPart = this.viewParts[i];
if (viewPart.shouldRender()) {
result[resultLen++] = viewPart;
}
}
return result;
}
private _actualRender(): void {
if (!dom.isInDOM(this.domNode.domNode)) {
return;
}
let viewPartsToRender = this._getViewPartsToRender();
if (!this.viewLines.shouldRender() && viewPartsToRender.length === 0) {
// Nothing to render
return;
}
const partialViewportData = this._context.viewLayout.getLinesViewportData();
this._context.model.setViewport(partialViewportData.startLineNumber, partialViewportData.endLineNumber, partialViewportData.centeredLineNumber);
let viewportData = new ViewportData(
this._cursor.getViewSelections(),
partialViewportData,
this._context.viewLayout.getWhitespaceViewportData(),
this._context.model
);
if (this.viewLines.shouldRender()) {
this.viewLines.renderText(viewportData);
this.viewLines.onDidRender();
// Rendering of viewLines might cause scroll events to occur, so collect view parts to render again
viewPartsToRender = this._getViewPartsToRender();
}
let renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this.viewLines);
// Render the rest of the parts
for (let i = 0, len = viewPartsToRender.length; i < len; i++) {
let viewPart = viewPartsToRender[i];
viewPart.prepareRender(renderingContext);
}
for (let i = 0, len = viewPartsToRender.length; i < len; i++) {
let viewPart = viewPartsToRender[i];
viewPart.render(renderingContext);
viewPart.onDidRender();
}
}
// --- BEGIN CodeEditor helpers
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
this._scrollbar.delegateVerticalScrollbarMouseDown(browserEvent);
}
public getOffsetForColumn(modelLineNumber: number, modelColumn: number): number {
let modelPosition = this._context.model.validateModelPosition({
lineNumber: modelLineNumber,
column: modelColumn
});
let viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
this._flushAccumulatedAndRenderNow();
let visibleRanges = this.viewLines.visibleRangesForRange2(new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column));
if (!visibleRanges) {
return -1;
}
return visibleRanges[0].left;
}
public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget {
return this.pointerHandler.getTargetAtClientPoint(clientX, clientY);
}
public getInternalEventBus(): ViewOutgoingEvents {
return this.outgoingEvents;
}
public createOverviewRuler(cssClassName: string, minimumHeight: number, maximumHeight: number): OverviewRuler {
return new OverviewRuler(this._context, cssClassName, minimumHeight, maximumHeight);
}
public change(callback: (changeAccessor: editorBrowser.IViewZoneChangeAccessor) => any): boolean {
let zonesHaveChanged = false;
this._renderOnce(() => {
let changeAccessor: editorBrowser.IViewZoneChangeAccessor = {
addZone: (zone: editorBrowser.IViewZone): number => {
zonesHaveChanged = true;
return this.viewZones.addZone(zone);
},
removeZone: (id: number): void => {
if (!id) {
return;
}
zonesHaveChanged = this.viewZones.removeZone(id) || zonesHaveChanged;
},
layoutZone: (id: number): void => {
if (!id) {
return;
}
zonesHaveChanged = this.viewZones.layoutZone(id) || zonesHaveChanged;
}
};
safeInvoke1Arg(callback, changeAccessor);
// Invalidate changeAccessor
changeAccessor.addZone = null;
changeAccessor.removeZone = null;
if (zonesHaveChanged) {
this._context.viewLayout.onHeightMaybeChanged();
this._context.privateViewEventBus.emit(new viewEvents.ViewZonesChangedEvent());
}
});
return zonesHaveChanged;
}
public render(now: boolean, everything: boolean): void {
if (everything) {
// Force everything to render...
this.viewLines.forceShouldRender();
for (let i = 0, len = this.viewParts.length; i < len; i++) {
let viewPart = this.viewParts[i];
viewPart.forceShouldRender();
}
}
if (now) {
this._flushAccumulatedAndRenderNow();
} else {
this._scheduleRender();
}
}
public setAriaActiveDescendant(id: string): void {
this._textAreaHandler.setAriaActiveDescendant(id);
}
public focus(): void {
this._textAreaHandler.focusTextArea();
}
public isFocused(): boolean {
return this._textAreaHandler.isFocused();
}
public addContentWidget(widgetData: IContentWidgetData): void {
this.contentWidgets.addWidget(widgetData.widget);
this.layoutContentWidget(widgetData);
this._scheduleRender();
}
public layoutContentWidget(widgetData: IContentWidgetData): void {
let newPosition = widgetData.position ? widgetData.position.position : null;
let newPreference = widgetData.position ? widgetData.position.preference : null;
this.contentWidgets.setWidgetPosition(widgetData.widget, newPosition, newPreference);
this._scheduleRender();
}
public removeContentWidget(widgetData: IContentWidgetData): void {
this.contentWidgets.removeWidget(widgetData.widget);
this._scheduleRender();
}
public addOverlayWidget(widgetData: IOverlayWidgetData): void {
this.overlayWidgets.addWidget(widgetData.widget);
this.layoutOverlayWidget(widgetData);
this._scheduleRender();
}
public layoutOverlayWidget(widgetData: IOverlayWidgetData): void {
let newPreference = widgetData.position ? widgetData.position.preference : null;
let shouldRender = this.overlayWidgets.setWidgetPosition(widgetData.widget, newPreference);
if (shouldRender) {
this._scheduleRender();
}
}
public removeOverlayWidget(widgetData: IOverlayWidgetData): void {
this.overlayWidgets.removeWidget(widgetData.widget);
this._scheduleRender();
}
// --- END CodeEditor helpers
}
function safeInvokeNoArg(func: Function): any {
try {
return func();
} catch (e) {
onUnexpectedError(e);
}
}
function safeInvoke1Arg(func: Function, arg1: any): any {
try {
return func(arg1);
} catch (e) {
onUnexpectedError(e);
}
}

View File

@@ -0,0 +1,610 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { createStringBuilder, IStringBuilder } from 'vs/editor/common/core/stringBuilder';
/**
* Represents a visible line
*/
export interface IVisibleLine {
getDomNode(): HTMLElement;
setDomNode(domNode: HTMLElement): void;
onContentChanged(): void;
onTokensChanged(): void;
/**
* Return null if the HTML should not be touched.
* Return the new HTML otherwise.
*/
renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: IStringBuilder): boolean;
/**
* Layout the line.
*/
layoutLine(lineNumber: number, deltaTop: number): void;
}
export interface ILine {
onContentChanged(): void;
onTokensChanged(): void;
}
export class RenderedLinesCollection<T extends ILine> {
private readonly _createLine: () => T;
private _lines: T[];
private _rendLineNumberStart: number;
constructor(createLine: () => T) {
this._createLine = createLine;
this._set(1, []);
}
public flush(): void {
this._set(1, []);
}
_set(rendLineNumberStart: number, lines: T[]): void {
this._lines = lines;
this._rendLineNumberStart = rendLineNumberStart;
}
_get(): { rendLineNumberStart: number; lines: T[]; } {
return {
rendLineNumberStart: this._rendLineNumberStart,
lines: this._lines
};
}
/**
* @returns Inclusive line number that is inside this collection
*/
public getStartLineNumber(): number {
return this._rendLineNumberStart;
}
/**
* @returns Inclusive line number that is inside this collection
*/
public getEndLineNumber(): number {
return this._rendLineNumberStart + this._lines.length - 1;
}
public getCount(): number {
return this._lines.length;
}
public getLine(lineNumber: number): T {
let lineIndex = lineNumber - this._rendLineNumberStart;
if (lineIndex < 0 || lineIndex >= this._lines.length) {
throw new Error('Illegal value for lineNumber: ' + lineNumber);
}
return this._lines[lineIndex];
}
/**
* @returns Lines that were removed from this collection
*/
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): T[] {
if (this.getCount() === 0) {
// no lines
return null;
}
let startLineNumber = this.getStartLineNumber();
let endLineNumber = this.getEndLineNumber();
if (deleteToLineNumber < startLineNumber) {
// deleting above the viewport
let deleteCnt = deleteToLineNumber - deleteFromLineNumber + 1;
this._rendLineNumberStart -= deleteCnt;
return null;
}
if (deleteFromLineNumber > endLineNumber) {
// deleted below the viewport
return null;
}
// Record what needs to be deleted
let deleteStartIndex = 0;
let deleteCount = 0;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
let lineIndex = lineNumber - this._rendLineNumberStart;
if (deleteFromLineNumber <= lineNumber && lineNumber <= deleteToLineNumber) {
// this is a line to be deleted
if (deleteCount === 0) {
// this is the first line to be deleted
deleteStartIndex = lineIndex;
deleteCount = 1;
} else {
deleteCount++;
}
}
}
// Adjust this._rendLineNumberStart for lines deleted above
if (deleteFromLineNumber < startLineNumber) {
// Something was deleted above
let deleteAboveCount = 0;
if (deleteToLineNumber < startLineNumber) {
// the entire deleted lines are above
deleteAboveCount = deleteToLineNumber - deleteFromLineNumber + 1;
} else {
deleteAboveCount = startLineNumber - deleteFromLineNumber;
}
this._rendLineNumberStart -= deleteAboveCount;
}
let deleted = this._lines.splice(deleteStartIndex, deleteCount);
return deleted;
}
public onLinesChanged(changeFromLineNumber: number, changeToLineNumber: number): boolean {
if (this.getCount() === 0) {
// no lines
return false;
}
let startLineNumber = this.getStartLineNumber();
let endLineNumber = this.getEndLineNumber();
let someoneNotified = false;
for (let changedLineNumber = changeFromLineNumber; changedLineNumber <= changeToLineNumber; changedLineNumber++) {
if (changedLineNumber >= startLineNumber && changedLineNumber <= endLineNumber) {
// Notify the line
this._lines[changedLineNumber - this._rendLineNumberStart].onContentChanged();
someoneNotified = true;
}
}
return someoneNotified;
}
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): T[] {
if (this.getCount() === 0) {
// no lines
return null;
}
let insertCnt = insertToLineNumber - insertFromLineNumber + 1;
let startLineNumber = this.getStartLineNumber();
let endLineNumber = this.getEndLineNumber();
if (insertFromLineNumber <= startLineNumber) {
// inserting above the viewport
this._rendLineNumberStart += insertCnt;
return null;
}
if (insertFromLineNumber > endLineNumber) {
// inserting below the viewport
return null;
}
if (insertCnt + insertFromLineNumber > endLineNumber) {
// insert inside the viewport in such a way that all remaining lines are pushed outside
let deleted = this._lines.splice(insertFromLineNumber - this._rendLineNumberStart, endLineNumber - insertFromLineNumber + 1);
return deleted;
}
// insert inside the viewport, push out some lines, but not all remaining lines
let newLines: T[] = [];
for (let i = 0; i < insertCnt; i++) {
newLines[i] = this._createLine();
}
let insertIndex = insertFromLineNumber - this._rendLineNumberStart;
let beforeLines = this._lines.slice(0, insertIndex);
let afterLines = this._lines.slice(insertIndex, this._lines.length - insertCnt);
let deletedLines = this._lines.slice(this._lines.length - insertCnt, this._lines.length);
this._lines = beforeLines.concat(newLines).concat(afterLines);
return deletedLines;
}
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number; }[]): boolean {
if (this.getCount() === 0) {
// no lines
return false;
}
let startLineNumber = this.getStartLineNumber();
let endLineNumber = this.getEndLineNumber();
let notifiedSomeone = false;
for (let i = 0, len = ranges.length; i < len; i++) {
let rng = ranges[i];
if (rng.toLineNumber < startLineNumber || rng.fromLineNumber > endLineNumber) {
// range outside viewport
continue;
}
let from = Math.max(startLineNumber, rng.fromLineNumber);
let to = Math.min(endLineNumber, rng.toLineNumber);
for (let lineNumber = from; lineNumber <= to; lineNumber++) {
let lineIndex = lineNumber - this._rendLineNumberStart;
this._lines[lineIndex].onTokensChanged();
notifiedSomeone = true;
}
}
return notifiedSomeone;
}
}
export interface IVisibleLinesHost<T extends IVisibleLine> {
createVisibleLine(): T;
}
export class VisibleLinesCollection<T extends IVisibleLine> {
private readonly _host: IVisibleLinesHost<T>;
public readonly domNode: FastDomNode<HTMLElement>;
private readonly _linesCollection: RenderedLinesCollection<T>;
constructor(host: IVisibleLinesHost<T>) {
this._host = host;
this.domNode = this._createDomNode();
this._linesCollection = new RenderedLinesCollection<T>(() => this._host.createVisibleLine());
}
private _createDomNode(): FastDomNode<HTMLElement> {
let domNode = createFastDomNode(document.createElement('div'));
domNode.setClassName('view-layer');
domNode.setPosition('absolute');
domNode.domNode.setAttribute('role', 'presentation');
domNode.domNode.setAttribute('aria-hidden', 'true');
return domNode;
}
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
return e.layoutInfo;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
this._linesCollection.flush();
// No need to clear the dom node because a full .innerHTML will occur in ViewLayerRenderer._render
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return this._linesCollection.onLinesChanged(e.fromLineNumber, e.toLineNumber);
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
let deleted = this._linesCollection.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
if (deleted) {
// Remove from DOM
for (let i = 0, len = deleted.length; i < len; i++) {
let lineDomNode = deleted[i].getDomNode();
if (lineDomNode) {
this.domNode.domNode.removeChild(lineDomNode);
}
}
}
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
let deleted = this._linesCollection.onLinesInserted(e.fromLineNumber, e.toLineNumber);
if (deleted) {
// Remove from DOM
for (let i = 0, len = deleted.length; i < len; i++) {
let lineDomNode = deleted[i].getDomNode();
if (lineDomNode) {
this.domNode.domNode.removeChild(lineDomNode);
}
}
}
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
return this._linesCollection.onTokensChanged(e.ranges);
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// ---- end view event handlers
public getStartLineNumber(): number {
return this._linesCollection.getStartLineNumber();
}
public getEndLineNumber(): number {
return this._linesCollection.getEndLineNumber();
}
public getVisibleLine(lineNumber: number): T {
return this._linesCollection.getLine(lineNumber);
}
public renderLines(viewportData: ViewportData): void {
let inp = this._linesCollection._get();
let renderer = new ViewLayerRenderer<T>(this.domNode.domNode, this._host, viewportData);
let ctx: IRendererContext<T> = {
rendLineNumberStart: inp.rendLineNumberStart,
lines: inp.lines,
linesLength: inp.lines.length
};
// Decide if this render will do a single update (single large .innerHTML) or many updates (inserting/removing dom nodes)
let resCtx = renderer.render(ctx, viewportData.startLineNumber, viewportData.endLineNumber, viewportData.relativeVerticalOffset);
this._linesCollection._set(resCtx.rendLineNumberStart, resCtx.lines);
}
}
interface IRendererContext<T extends IVisibleLine> {
rendLineNumberStart: number;
lines: T[];
linesLength: number;
}
class ViewLayerRenderer<T extends IVisibleLine> {
readonly domNode: HTMLElement;
readonly host: IVisibleLinesHost<T>;
readonly viewportData: ViewportData;
constructor(domNode: HTMLElement, host: IVisibleLinesHost<T>, viewportData: ViewportData) {
this.domNode = domNode;
this.host = host;
this.viewportData = viewportData;
}
public render(inContext: IRendererContext<T>, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext<T> {
let ctx: IRendererContext<T> = {
rendLineNumberStart: inContext.rendLineNumberStart,
lines: inContext.lines.slice(0),
linesLength: inContext.linesLength
};
if ((ctx.rendLineNumberStart + ctx.linesLength - 1 < startLineNumber) || (stopLineNumber < ctx.rendLineNumberStart)) {
// There is no overlap whatsoever
ctx.rendLineNumberStart = startLineNumber;
ctx.linesLength = stopLineNumber - startLineNumber + 1;
ctx.lines = [];
for (let x = startLineNumber; x <= stopLineNumber; x++) {
ctx.lines[x - startLineNumber] = this.host.createVisibleLine();
}
this._finishRendering(ctx, true, deltaTop);
return ctx;
}
// Update lines which will remain untouched
this._renderUntouchedLines(
ctx,
Math.max(startLineNumber - ctx.rendLineNumberStart, 0),
Math.min(stopLineNumber - ctx.rendLineNumberStart, ctx.linesLength - 1),
deltaTop,
startLineNumber
);
if (ctx.rendLineNumberStart > startLineNumber) {
// Insert lines before
let fromLineNumber = startLineNumber;
let toLineNumber = Math.min(stopLineNumber, ctx.rendLineNumberStart - 1);
if (fromLineNumber <= toLineNumber) {
this._insertLinesBefore(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
ctx.linesLength += toLineNumber - fromLineNumber + 1;
}
} else if (ctx.rendLineNumberStart < startLineNumber) {
// Remove lines before
let removeCnt = Math.min(ctx.linesLength, startLineNumber - ctx.rendLineNumberStart);
if (removeCnt > 0) {
this._removeLinesBefore(ctx, removeCnt);
ctx.linesLength -= removeCnt;
}
}
ctx.rendLineNumberStart = startLineNumber;
if (ctx.rendLineNumberStart + ctx.linesLength - 1 < stopLineNumber) {
// Insert lines after
let fromLineNumber = ctx.rendLineNumberStart + ctx.linesLength;
let toLineNumber = stopLineNumber;
if (fromLineNumber <= toLineNumber) {
this._insertLinesAfter(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
ctx.linesLength += toLineNumber - fromLineNumber + 1;
}
} else if (ctx.rendLineNumberStart + ctx.linesLength - 1 > stopLineNumber) {
// Remove lines after
let fromLineNumber = Math.max(0, stopLineNumber - ctx.rendLineNumberStart + 1);
let toLineNumber = ctx.linesLength - 1;
let removeCnt = toLineNumber - fromLineNumber + 1;
if (removeCnt > 0) {
this._removeLinesAfter(ctx, removeCnt);
ctx.linesLength -= removeCnt;
}
}
this._finishRendering(ctx, false, deltaTop);
return ctx;
}
private _renderUntouchedLines(ctx: IRendererContext<T>, startIndex: number, endIndex: number, deltaTop: number[], deltaLN: number): void {
const rendLineNumberStart = ctx.rendLineNumberStart;
const lines = ctx.lines;
for (let i = startIndex; i <= endIndex; i++) {
let lineNumber = rendLineNumberStart + i;
lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN]);
}
}
private _insertLinesBefore(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {
let newLines: T[] = [];
let newLinesLen = 0;
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
newLines[newLinesLen++] = this.host.createVisibleLine();
}
ctx.lines = newLines.concat(ctx.lines);
}
private _removeLinesBefore(ctx: IRendererContext<T>, removeCount: number): void {
for (let i = 0; i < removeCount; i++) {
let lineDomNode = ctx.lines[i].getDomNode();
if (lineDomNode) {
this.domNode.removeChild(lineDomNode);
}
}
ctx.lines.splice(0, removeCount);
}
private _insertLinesAfter(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {
let newLines: T[] = [];
let newLinesLen = 0;
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
newLines[newLinesLen++] = this.host.createVisibleLine();
}
ctx.lines = ctx.lines.concat(newLines);
}
private _removeLinesAfter(ctx: IRendererContext<T>, removeCount: number): void {
let removeIndex = ctx.linesLength - removeCount;
for (let i = 0; i < removeCount; i++) {
let lineDomNode = ctx.lines[removeIndex + i].getDomNode();
if (lineDomNode) {
this.domNode.removeChild(lineDomNode);
}
}
ctx.lines.splice(removeIndex, removeCount);
}
private _finishRenderingNewLines(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, newLinesHTML: string, wasNew: boolean[]): void {
let lastChild = <HTMLElement>this.domNode.lastChild;
if (domNodeIsEmpty || !lastChild) {
this.domNode.innerHTML = newLinesHTML;
} else {
lastChild.insertAdjacentHTML('afterend', newLinesHTML);
}
let currChild = <HTMLElement>this.domNode.lastChild;
for (let i = ctx.linesLength - 1; i >= 0; i--) {
let line = ctx.lines[i];
if (wasNew[i]) {
line.setDomNode(currChild);
currChild = <HTMLElement>currChild.previousSibling;
}
}
}
private _finishRenderingInvalidLines(ctx: IRendererContext<T>, invalidLinesHTML: string, wasInvalid: boolean[]): void {
let hugeDomNode = document.createElement('div');
hugeDomNode.innerHTML = invalidLinesHTML;
for (let i = 0; i < ctx.linesLength; i++) {
let line = ctx.lines[i];
if (wasInvalid[i]) {
let source = <HTMLElement>hugeDomNode.firstChild;
let lineDomNode = line.getDomNode();
lineDomNode.parentNode.replaceChild(source, lineDomNode);
line.setDomNode(source);
}
}
}
private static _sb = createStringBuilder(100000);
private _finishRendering(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, deltaTop: number[]): void {
const sb = ViewLayerRenderer._sb;
const linesLength = ctx.linesLength;
const lines = ctx.lines;
const rendLineNumberStart = ctx.rendLineNumberStart;
let wasNew: boolean[] = [];
{
sb.reset();
let hadNewLine = false;
for (let i = 0; i < linesLength; i++) {
const line = lines[i];
wasNew[i] = false;
const lineDomNode = line.getDomNode();
if (lineDomNode) {
// line is not new
continue;
}
const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb);
if (!renderResult) {
// line does not need rendering
continue;
}
wasNew[i] = true;
hadNewLine = true;
}
if (hadNewLine) {
this._finishRenderingNewLines(ctx, domNodeIsEmpty, sb.build(), wasNew);
}
}
{
sb.reset();
let hadInvalidLine = false;
let wasInvalid: boolean[] = [];
for (let i = 0; i < linesLength; i++) {
let line = lines[i];
wasInvalid[i] = false;
if (wasNew[i]) {
// line was new
continue;
}
const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this.viewportData, sb);
if (!renderResult) {
// line does not need rendering
continue;
}
wasInvalid[i] = true;
hadInvalidLine = true;
}
if (hadInvalidLine) {
this._finishRenderingInvalidLines(ctx, sb.build(), wasInvalid);
}
}
}
}

View File

@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
import { IScrollEvent } from 'vs/editor/common/editorCommon';
import { IEditorMouseEvent, IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { MouseTarget } from 'vs/editor/browser/controller/mouseTarget';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export interface EventCallback<T> {
(event: T): void;
}
export class ViewOutgoingEvents extends Disposable {
public onDidScroll: EventCallback<IScrollEvent> = null;
public onDidGainFocus: EventCallback<void> = null;
public onDidLoseFocus: EventCallback<void> = null;
public onKeyDown: EventCallback<IKeyboardEvent> = null;
public onKeyUp: EventCallback<IKeyboardEvent> = null;
public onContextMenu: EventCallback<IEditorMouseEvent> = null;
public onMouseMove: EventCallback<IEditorMouseEvent> = null;
public onMouseLeave: EventCallback<IEditorMouseEvent> = null;
public onMouseUp: EventCallback<IEditorMouseEvent> = null;
public onMouseDown: EventCallback<IEditorMouseEvent> = null;
public onMouseDrag: EventCallback<IEditorMouseEvent> = null;
public onMouseDrop: EventCallback<IEditorMouseEvent> = null;
private _viewModel: IViewModel;
constructor(viewModel: IViewModel) {
super();
this._viewModel = viewModel;
}
public emitScrollChanged(e: viewEvents.ViewScrollChangedEvent): void {
if (this.onDidScroll) {
this.onDidScroll(e);
}
}
public emitViewFocusGained(): void {
if (this.onDidGainFocus) {
this.onDidGainFocus(void 0);
}
}
public emitViewFocusLost(): void {
if (this.onDidLoseFocus) {
this.onDidLoseFocus(void 0);
}
}
public emitKeyDown(e: IKeyboardEvent): void {
if (this.onKeyDown) {
this.onKeyDown(e);
}
}
public emitKeyUp(e: IKeyboardEvent): void {
if (this.onKeyUp) {
this.onKeyUp(e);
}
}
public emitContextMenu(e: IEditorMouseEvent): void {
if (this.onContextMenu) {
this.onContextMenu(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseMove(e: IEditorMouseEvent): void {
if (this.onMouseMove) {
this.onMouseMove(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseLeave(e: IEditorMouseEvent): void {
if (this.onMouseLeave) {
this.onMouseLeave(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseUp(e: IEditorMouseEvent): void {
if (this.onMouseUp) {
this.onMouseUp(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseDown(e: IEditorMouseEvent): void {
if (this.onMouseDown) {
this.onMouseDown(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseDrag(e: IEditorMouseEvent): void {
if (this.onMouseDrag) {
this.onMouseDrag(this._convertViewToModelMouseEvent(e));
}
}
public emitMouseDrop(e: IEditorMouseEvent): void {
if (this.onMouseDrop) {
this.onMouseDrop(this._convertViewToModelMouseEvent(e));
}
}
private _convertViewToModelMouseEvent(e: IEditorMouseEvent): IEditorMouseEvent {
if (e.target) {
return {
event: e.event,
target: this._convertViewToModelMouseTarget(e.target)
};
}
return e;
}
private _convertViewToModelMouseTarget(target: IMouseTarget): IMouseTarget {
return new ExternalMouseTarget(
target.element,
target.type,
target.mouseColumn,
target.position ? this._convertViewToModelPosition(target.position) : null,
target.range ? this._convertViewToModelRange(target.range) : null,
target.detail
);
}
private _convertViewToModelPosition(viewPosition: Position): Position {
return this._viewModel.coordinatesConverter.convertViewPositionToModelPosition(viewPosition);
}
private _convertViewToModelRange(viewRange: Range): Range {
return this._viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange);
}
}
class ExternalMouseTarget implements IMouseTarget {
public readonly element: Element;
public readonly type: MouseTargetType;
public readonly mouseColumn: number;
public readonly position: Position;
public readonly range: Range;
public readonly detail: any;
constructor(element: Element, type: MouseTargetType, mouseColumn: number, position: Position, range: Range, detail: any) {
this.element = element;
this.type = type;
this.mouseColumn = mouseColumn;
this.position = position;
this.range = range;
this.detail = detail;
}
public toString(): string {
return MouseTarget.toString(this);
}
}

View File

@@ -0,0 +1,286 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IConfiguration } from 'vs/editor/common/editorCommon';
import { IVisibleLine, VisibleLinesCollection, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { IStringBuilder } from 'vs/editor/common/core/stringBuilder';
export class ViewOverlays extends ViewPart implements IVisibleLinesHost<ViewOverlayLine> {
private readonly _visibleLines: VisibleLinesCollection<ViewOverlayLine>;
protected readonly domNode: FastDomNode<HTMLElement>;
private _dynamicOverlays: DynamicViewOverlay[];
private _isFocused: boolean;
constructor(context: ViewContext) {
super(context);
this._visibleLines = new VisibleLinesCollection<ViewOverlayLine>(this);
this.domNode = this._visibleLines.domNode;
this._dynamicOverlays = [];
this._isFocused = false;
this.domNode.setClassName('view-overlays');
}
public shouldRender(): boolean {
if (super.shouldRender()) {
return true;
}
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
let dynamicOverlay = this._dynamicOverlays[i];
if (dynamicOverlay.shouldRender()) {
return true;
}
}
return false;
}
public dispose(): void {
super.dispose();
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
let dynamicOverlay = this._dynamicOverlays[i];
dynamicOverlay.dispose();
}
this._dynamicOverlays = null;
}
public getDomNode(): FastDomNode<HTMLElement> {
return this.domNode;
}
// ---- begin IVisibleLinesHost
public createVisibleLine(): ViewOverlayLine {
return new ViewOverlayLine(this._context.configuration, this._dynamicOverlays);
}
// ---- end IVisibleLinesHost
public addDynamicOverlay(overlay: DynamicViewOverlay): void {
this._dynamicOverlays.push(overlay);
}
// ----- event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
this._visibleLines.onConfigurationChanged(e);
let startLineNumber = this._visibleLines.getStartLineNumber();
let endLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
let line = this._visibleLines.getVisibleLine(lineNumber);
line.onConfigurationChanged(e);
}
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return this._visibleLines.onFlushed(e);
}
public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
this._isFocused = e.isFocused;
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return this._visibleLines.onLinesChanged(e);
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return this._visibleLines.onLinesDeleted(e);
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return this._visibleLines.onLinesInserted(e);
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return this._visibleLines.onScrollChanged(e) || true;
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
return this._visibleLines.onTokensChanged(e);
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return this._visibleLines.onZonesChanged(e);
}
// ----- end event handlers
public prepareRender(ctx: RenderingContext): void {
let toRender = this._dynamicOverlays.filter(overlay => overlay.shouldRender());
for (let i = 0, len = toRender.length; i < len; i++) {
let dynamicOverlay = toRender[i];
dynamicOverlay.prepareRender(ctx);
dynamicOverlay.onDidRender();
}
return null;
}
public render(ctx: RestrictedRenderingContext): void {
// Overwriting to bypass `shouldRender` flag
this._viewOverlaysRender(ctx);
this.domNode.toggleClassName('focused', this._isFocused);
}
_viewOverlaysRender(ctx: RestrictedRenderingContext): void {
this._visibleLines.renderLines(ctx.viewportData);
}
}
export class ViewOverlayLine implements IVisibleLine {
private _configuration: IConfiguration;
private _dynamicOverlays: DynamicViewOverlay[];
private _domNode: FastDomNode<HTMLElement>;
private _renderedContent: string;
private _lineHeight: number;
constructor(configuration: IConfiguration, dynamicOverlays: DynamicViewOverlay[]) {
this._configuration = configuration;
this._lineHeight = this._configuration.editor.lineHeight;
this._dynamicOverlays = dynamicOverlays;
this._domNode = null;
this._renderedContent = null;
}
public getDomNode(): HTMLElement {
if (!this._domNode) {
return null;
}
return this._domNode.domNode;
}
public setDomNode(domNode: HTMLElement): void {
this._domNode = createFastDomNode(domNode);
}
public onContentChanged(): void {
// Nothing
}
public onTokensChanged(): void {
// Nothing
}
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void {
if (e.lineHeight) {
this._lineHeight = this._configuration.editor.lineHeight;
}
}
public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: IStringBuilder): boolean {
let result = '';
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
let dynamicOverlay = this._dynamicOverlays[i];
result += dynamicOverlay.render(viewportData.startLineNumber, lineNumber);
}
if (this._renderedContent === result) {
// No rendering needed
return false;
}
this._renderedContent = result;
sb.appendASCIIString('<div style="position:absolute;top:');
sb.appendASCIIString(String(deltaTop));
sb.appendASCIIString('px;width:100%;height:');
sb.appendASCIIString(String(this._lineHeight));
sb.appendASCIIString('px;">');
sb.appendASCIIString(result);
sb.appendASCIIString('</div>');
return true;
}
public layoutLine(lineNumber: number, deltaTop: number): void {
if (this._domNode) {
this._domNode.setTop(deltaTop);
this._domNode.setHeight(this._lineHeight);
}
}
}
export class ContentViewOverlays extends ViewOverlays {
private _contentWidth: number;
constructor(context: ViewContext) {
super(context);
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this.domNode.setHeight(0);
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.layoutInfo) {
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
}
return super.onConfigurationChanged(e);
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return super.onScrollChanged(e) || e.scrollWidthChanged;
}
// --- end event handlers
_viewOverlaysRender(ctx: RestrictedRenderingContext): void {
super._viewOverlaysRender(ctx);
this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));
}
}
export class MarginViewOverlays extends ViewOverlays {
private _contentLeft: number;
constructor(context: ViewContext) {
super(context);
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this.domNode.setClassName('margin-view-overlays');
this.domNode.setWidth(1);
Configuration.applyFontInfo(this.domNode, this._context.configuration.editor.fontInfo);
}
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
let shouldRender = false;
if (e.fontInfo) {
Configuration.applyFontInfo(this.domNode, this._context.configuration.editor.fontInfo);
shouldRender = true;
}
if (e.layoutInfo) {
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
shouldRender = true;
}
return super.onConfigurationChanged(e) || shouldRender;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return super.onScrollChanged(e) || e.scrollHeightChanged;
}
_viewOverlaysRender(ctx: RestrictedRenderingContext): void {
super._viewOverlaysRender(ctx);
let height = Math.min(ctx.scrollHeight, 1000000);
this.domNode.setHeight(height);
this.domNode.setWidth(this._contentLeft);
}
}

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { FastDomNode } from 'vs/base/browser/fastDomNode';
export abstract class ViewPart extends ViewEventHandler {
_context: ViewContext;
constructor(context: ViewContext) {
super();
this._context = context;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
super.dispose();
}
public abstract prepareRender(ctx: RenderingContext): void;
public abstract render(ctx: RestrictedRenderingContext): void;
}
export const enum PartFingerprint {
None,
ContentWidgets,
OverflowingContentWidgets,
OverflowGuard,
OverlayWidgets,
ScrollableElement,
TextArea,
ViewLines,
Minimap
}
export class PartFingerprints {
public static write(target: Element | FastDomNode<HTMLElement>, partId: PartFingerprint) {
if (target instanceof FastDomNode) {
target.setAttribute('data-mprt', String(partId));
} else {
target.setAttribute('data-mprt', String(partId));
}
}
public static read(target: Element): PartFingerprint {
let r = target.getAttribute('data-mprt');
if (r === null) {
return PartFingerprint.None;
}
return parseInt(r, 10);
}
public static collect(child: Element, stopAt: Element): Uint8Array {
let result: PartFingerprint[] = [], resultLen = 0;
while (child && child !== document.body) {
if (child === stopAt) {
break;
}
if (child.nodeType === child.ELEMENT_NODE) {
result[resultLen++] = this.read(child);
}
child = child.parentElement;
}
let r = new Uint8Array(resultLen);
for (let i = 0; i < resultLen; i++) {
r[i] = result[resultLen - i - 1];
}
return r;
}
}

View File

@@ -0,0 +1,431 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ContentWidgetPositionPreference, IContentWidget } from 'vs/editor/browser/editorBrowser';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { Position, IPosition } from 'vs/editor/common/core/position';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
class Coordinate {
_coordinateBrand: void;
public readonly top: number;
public readonly left: number;
constructor(top: number, left: number) {
this.top = top;
this.left = left;
}
}
export class ViewContentWidgets extends ViewPart {
private _viewDomNode: FastDomNode<HTMLElement>;
private _widgets: { [key: string]: Widget; };
public domNode: FastDomNode<HTMLElement>;
public overflowingContentWidgetsDomNode: FastDomNode<HTMLElement>;
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>) {
super(context);
this._viewDomNode = viewDomNode;
this._widgets = {};
this.domNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this.domNode, PartFingerprint.ContentWidgets);
this.domNode.setClassName('contentWidgets');
this.domNode.setPosition('absolute');
this.domNode.setTop(0);
this.overflowingContentWidgetsDomNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this.overflowingContentWidgetsDomNode, PartFingerprint.OverflowingContentWidgets);
this.overflowingContentWidgetsDomNode.setClassName('overflowingContentWidgets');
}
public dispose(): void {
super.dispose();
this._widgets = null;
this.domNode = null;
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
let keys = Object.keys(this._widgets);
for (let i = 0, len = keys.length; i < len; i++) {
const widgetId = keys[i];
this._widgets[widgetId].onConfigurationChanged(e);
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return true;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// ---- end view event handlers
public addWidget(_widget: IContentWidget): void {
const myWidget = new Widget(this._context, this._viewDomNode, _widget);
this._widgets[myWidget.id] = myWidget;
if (myWidget.allowEditorOverflow) {
this.overflowingContentWidgetsDomNode.appendChild(myWidget.domNode);
} else {
this.domNode.appendChild(myWidget.domNode);
}
this.setShouldRender();
}
public setWidgetPosition(widget: IContentWidget, position: IPosition, preference: ContentWidgetPositionPreference[]): void {
const myWidget = this._widgets[widget.getId()];
myWidget.setPosition(position, preference);
this.setShouldRender();
}
public removeWidget(widget: IContentWidget): void {
const widgetId = widget.getId();
if (this._widgets.hasOwnProperty(widgetId)) {
const myWidget = this._widgets[widgetId];
delete this._widgets[widgetId];
const domNode = myWidget.domNode.domNode;
domNode.parentNode.removeChild(domNode);
domNode.removeAttribute('monaco-visible-content-widget');
this.setShouldRender();
}
}
public shouldSuppressMouseDownOnWidget(widgetId: string): boolean {
if (this._widgets.hasOwnProperty(widgetId)) {
return this._widgets[widgetId].suppressMouseDown;
}
return false;
}
public prepareRender(ctx: RenderingContext): void {
let keys = Object.keys(this._widgets);
for (let i = 0, len = keys.length; i < len; i++) {
const widgetId = keys[i];
this._widgets[widgetId].prepareRender(ctx);
}
}
public render(ctx: RestrictedRenderingContext): void {
let keys = Object.keys(this._widgets);
for (let i = 0, len = keys.length; i < len; i++) {
const widgetId = keys[i];
this._widgets[widgetId].render(ctx);
}
}
}
interface IBoxLayoutResult {
aboveTop: number;
fitsAbove: boolean;
belowTop: number;
fitsBelow: boolean;
left: number;
}
class Widget {
private readonly _context: ViewContext;
private readonly _viewDomNode: FastDomNode<HTMLElement>;
private readonly _actual: IContentWidget;
public readonly domNode: FastDomNode<HTMLElement>;
public readonly id: string;
public readonly allowEditorOverflow: boolean;
public readonly suppressMouseDown: boolean;
private _fixedOverflowWidgets: boolean;
private _contentWidth: number;
private _contentLeft: number;
private _lineHeight: number;
private _position: IPosition;
private _preference: ContentWidgetPositionPreference[];
private _isVisible: boolean;
private _renderData: Coordinate;
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>, actual: IContentWidget) {
this._context = context;
this._viewDomNode = viewDomNode;
this._actual = actual;
this.domNode = createFastDomNode(this._actual.getDomNode());
this.id = this._actual.getId();
this.allowEditorOverflow = this._actual.allowEditorOverflow || false;
this.suppressMouseDown = this._actual.suppressMouseDown || false;
this._fixedOverflowWidgets = this._context.configuration.editor.viewInfo.fixedOverflowWidgets;
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._position = null;
this._preference = null;
this._isVisible = false;
this._renderData = null;
this.domNode.setPosition((this._fixedOverflowWidgets && this.allowEditorOverflow) ? 'fixed' : 'absolute');
this._updateMaxWidth();
this.domNode.setVisibility('hidden');
this.domNode.setAttribute('widgetId', this.id);
}
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.layoutInfo) {
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this._updateMaxWidth();
}
}
private _updateMaxWidth(): void {
const maxWidth = this.allowEditorOverflow
? window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
: this._contentWidth;
this.domNode.setMaxWidth(maxWidth);
}
public setPosition(position: IPosition, preference: ContentWidgetPositionPreference[]): void {
this._position = position;
this._preference = preference;
}
private _layoutBoxInViewport(topLeft: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
// Our visible box is split horizontally by the current line => 2 boxes
// a) the box above the line
let aboveLineTop = topLeft.top;
let heightAboveLine = aboveLineTop;
// b) the box under the line
let underLineTop = topLeft.top + this._lineHeight;
let heightUnderLine = ctx.viewportHeight - underLineTop;
let aboveTop = aboveLineTop - height;
let fitsAbove = (heightAboveLine >= height);
let belowTop = underLineTop;
let fitsBelow = (heightUnderLine >= height);
// And its left
let actualLeft = topLeft.left;
if (actualLeft + width > ctx.scrollLeft + ctx.viewportWidth) {
actualLeft = ctx.scrollLeft + ctx.viewportWidth - width;
}
if (actualLeft < ctx.scrollLeft) {
actualLeft = ctx.scrollLeft;
}
return {
aboveTop: aboveTop,
fitsAbove: fitsAbove,
belowTop: belowTop,
fitsBelow: fitsBelow,
left: actualLeft
};
}
private _layoutBoxInPage(topLeft: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
let left0 = topLeft.left - ctx.scrollLeft;
if (left0 + width < 0 || left0 > this._contentWidth) {
return null;
}
let aboveTop = topLeft.top - height;
let belowTop = topLeft.top + this._lineHeight;
let left = left0 + this._contentLeft;
let domNodePosition = dom.getDomNodePagePosition(this._viewDomNode.domNode);
let absoluteAboveTop = domNodePosition.top + aboveTop - dom.StandardWindow.scrollY;
let absoluteBelowTop = domNodePosition.top + belowTop - dom.StandardWindow.scrollY;
let absoluteLeft = domNodePosition.left + left - dom.StandardWindow.scrollX;
let INNER_WIDTH = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
let INNER_HEIGHT = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
// Leave some clearance to the bottom
let TOP_PADDING = 22;
let BOTTOM_PADDING = 22;
let fitsAbove = (absoluteAboveTop >= TOP_PADDING),
fitsBelow = (absoluteBelowTop + height <= INNER_HEIGHT - BOTTOM_PADDING);
if (absoluteLeft + width + 20 > INNER_WIDTH) {
let delta = absoluteLeft - (INNER_WIDTH - width - 20);
absoluteLeft -= delta;
left -= delta;
}
if (absoluteLeft < 0) {
let delta = absoluteLeft;
absoluteLeft -= delta;
left -= delta;
}
if (this._fixedOverflowWidgets) {
aboveTop = absoluteAboveTop;
belowTop = absoluteBelowTop;
left = absoluteLeft;
}
return { aboveTop, fitsAbove, belowTop, fitsBelow, left };
}
private _prepareRenderWidgetAtExactPositionOverflowing(topLeft: Coordinate): Coordinate {
return new Coordinate(topLeft.top, topLeft.left + this._contentLeft);
}
private _getTopLeft(ctx: RenderingContext, position: Position): Coordinate {
const visibleRange = ctx.visibleRangeForPosition(position);
if (!visibleRange) {
return null;
}
const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop;
return new Coordinate(top, visibleRange.left);
}
private _prepareRenderWidget(ctx: RenderingContext): Coordinate {
if (!this._position || !this._preference) {
return null;
}
// Do not trust that widgets have a valid position
let validModelPosition = this._context.model.validateModelPosition(this._position);
if (!this._context.model.coordinatesConverter.modelPositionIsVisible(validModelPosition)) {
// this position is hidden by the view model
return null;
}
let position = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(validModelPosition);
let placement: IBoxLayoutResult = null;
let fetchPlacement = (): void => {
if (placement) {
return;
}
const topLeft = this._getTopLeft(ctx, position);
if (!topLeft) {
return;
}
const domNode = this.domNode.domNode;
const width = domNode.clientWidth;
const height = domNode.clientHeight;
if (this.allowEditorOverflow) {
placement = this._layoutBoxInPage(topLeft, width, height, ctx);
} else {
placement = this._layoutBoxInViewport(topLeft, width, height, ctx);
}
};
// Do two passes, first for perfect fit, second picks first option
for (let pass = 1; pass <= 2; pass++) {
for (let i = 0; i < this._preference.length; i++) {
let pref = this._preference[i];
if (pref === ContentWidgetPositionPreference.ABOVE) {
fetchPlacement();
if (!placement) {
// Widget outside of viewport
return null;
}
if (pass === 2 || placement.fitsAbove) {
return new Coordinate(placement.aboveTop, placement.left);
}
} else if (pref === ContentWidgetPositionPreference.BELOW) {
fetchPlacement();
if (!placement) {
// Widget outside of viewport
return null;
}
if (pass === 2 || placement.fitsBelow) {
return new Coordinate(placement.belowTop, placement.left);
}
} else {
const topLeft = this._getTopLeft(ctx, position);
if (!topLeft) {
// Widget outside of viewport
return null;
}
if (this.allowEditorOverflow) {
return this._prepareRenderWidgetAtExactPositionOverflowing(topLeft);
} else {
return topLeft;
}
}
}
}
return null;
}
public prepareRender(ctx: RenderingContext): void {
this._renderData = this._prepareRenderWidget(ctx);
}
public render(ctx: RestrictedRenderingContext): void {
if (!this._renderData) {
// This widget should be invisible
if (this._isVisible) {
this.domNode.removeAttribute('monaco-visible-content-widget');
this._isVisible = false;
this.domNode.setVisibility('hidden');
}
return;
}
// This widget should be visible
if (this.allowEditorOverflow) {
this.domNode.setTop(this._renderData.top);
this.domNode.setLeft(this._renderData.left);
} else {
this.domNode.setTop(this._renderData.top + ctx.scrollTop - ctx.bigNumbersDelta);
this.domNode.setLeft(this._renderData.left);
}
if (!this._isVisible) {
this.domNode.setVisibility('inherit');
this.domNode.setAttribute('monaco-visible-content-widget', 'true');
this._isVisible = true;
}
}
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .view-overlays .current-line {
display: block;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
}

View File

@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./currentLineHighlight';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
export class CurrentLineHighlightOverlay extends DynamicViewOverlay {
private _context: ViewContext;
private _lineHeight: number;
private _readOnly: boolean;
private _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all';
private _selectionIsEmpty: boolean;
private _primaryCursorIsInEditableRange: boolean;
private _primaryCursorLineNumber: number;
private _scrollWidth: number;
private _contentWidth: number;
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._readOnly = this._context.configuration.editor.readOnly;
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
this._selectionIsEmpty = true;
this._primaryCursorIsInEditableRange = true;
this._primaryCursorLineNumber = 1;
this._scrollWidth = 0;
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.readOnly) {
this._readOnly = this._context.configuration.editor.readOnly;
}
if (e.viewInfo) {
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
}
if (e.layoutInfo) {
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
}
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
let hasChanged = false;
if (this._primaryCursorIsInEditableRange !== e.isInEditableRange) {
this._primaryCursorIsInEditableRange = e.isInEditableRange;
hasChanged = true;
}
const primaryCursorLineNumber = e.selections[0].positionLineNumber;
if (this._primaryCursorLineNumber !== primaryCursorLineNumber) {
this._primaryCursorLineNumber = primaryCursorLineNumber;
hasChanged = true;
}
const selectionIsEmpty = e.selections[0].isEmpty();
if (this._selectionIsEmpty !== selectionIsEmpty) {
this._selectionIsEmpty = selectionIsEmpty;
hasChanged = true;
return true;
}
return hasChanged;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollWidthChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
this._scrollWidth = ctx.scrollWidth;
}
public render(startLineNumber: number, lineNumber: number): string {
if (lineNumber === this._primaryCursorLineNumber) {
if (this._shouldShowCurrentLine()) {
return (
'<div class="current-line" style="width:'
+ String(Math.max(this._scrollWidth, this._contentWidth))
+ 'px; height:'
+ String(this._lineHeight)
+ 'px;"></div>'
);
} else {
return '';
}
}
return '';
}
private _shouldShowCurrentLine(): boolean {
return (this._renderLineHighlight === 'line' || this._renderLineHighlight === 'all') &&
this._selectionIsEmpty &&
this._primaryCursorIsInEditableRange;
}
}
registerThemingParticipant((theme, collector) => {
let lineHighlight = theme.getColor(editorLineHighlight);
if (lineHighlight) {
collector.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${lineHighlight}; }`);
}
if (!lineHighlight || lineHighlight.isTransparent() || theme.defines(editorLineHighlightBorder)) {
let lineHighlightBorder = theme.getColor(editorLineHighlightBorder);
if (lineHighlightBorder) {
collector.addRule(`.monaco-editor .view-overlays .current-line { border: 2px solid ${lineHighlightBorder}; }`);
if (theme.type === 'hc') {
collector.addRule(`.monaco-editor .view-overlays .current-line { border-width: 1px; }`);
}
}
}
});

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .margin-view-overlays .current-line-margin {
display: block;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./currentLineMarginHighlight';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
export class CurrentLineMarginHighlightOverlay extends DynamicViewOverlay {
private _context: ViewContext;
private _lineHeight: number;
private _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all';
private _primaryCursorIsInEditableRange: boolean;
private _primaryCursorLineNumber: number;
private _contentLeft: number;
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
this._primaryCursorIsInEditableRange = true;
this._primaryCursorLineNumber = 1;
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.viewInfo) {
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
}
if (e.layoutInfo) {
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
}
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
let hasChanged = false;
if (this._primaryCursorIsInEditableRange !== e.isInEditableRange) {
this._primaryCursorIsInEditableRange = e.isInEditableRange;
hasChanged = true;
}
const primaryCursorLineNumber = e.selections[0].positionLineNumber;
if (this._primaryCursorLineNumber !== primaryCursorLineNumber) {
this._primaryCursorLineNumber = primaryCursorLineNumber;
hasChanged = true;
}
return hasChanged;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
}
public render(startLineNumber: number, lineNumber: number): string {
if (lineNumber === this._primaryCursorLineNumber) {
if (this._shouldShowCurrentLine()) {
return (
'<div class="current-line-margin" style="width:'
+ String(this._contentLeft)
+ 'px; height:'
+ String(this._lineHeight)
+ 'px;"></div>'
);
} else {
return '';
}
}
return '';
}
private _shouldShowCurrentLine(): boolean {
return (this._renderLineHighlight === 'gutter' || this._renderLineHighlight === 'all') && this._primaryCursorIsInEditableRange;
}
}
registerThemingParticipant((theme, collector) => {
let lineHighlight = theme.getColor(editorLineHighlight);
if (lineHighlight) {
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`);
} else {
let lineHighlightBorder = theme.getColor(editorLineHighlightBorder);
if (lineHighlightBorder) {
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { border: 2px solid ${lineHighlightBorder}; }`);
}
if (theme.type === 'hc') {
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { border-width: 1px; }`);
}
}
});

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cdr = core decorations rendering (div)
*/
.monaco-editor .lines-content .cdr {
position: absolute;
}

View File

@@ -0,0 +1,209 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./decorations';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { Range } from 'vs/editor/common/core/range';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, HorizontalRange } from 'vs/editor/common/view/renderingContext';
import { ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export class DecorationsOverlay extends DynamicViewOverlay {
private _context: ViewContext;
private _lineHeight: number;
private _typicalHalfwidthCharacterWidth: number;
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.fontInfo) {
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged || e.scrollWidthChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
let _decorations = ctx.getDecorationsInViewport();
// Keep only decorations with `className`
let decorations: ViewModelDecoration[] = [], decorationsLen = 0;
for (let i = 0, len = _decorations.length; i < len; i++) {
let d = _decorations[i];
if (d.source.options.className) {
decorations[decorationsLen++] = d;
}
}
// Sort decorations for consistent render output
decorations = decorations.sort((a, b) => {
let aClassName = a.source.options.className;
let bClassName = b.source.options.className;
if (aClassName < bClassName) {
return -1;
}
if (aClassName > bClassName) {
return 1;
}
return Range.compareRangesUsingStarts(a.range, b.range);
});
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = '';
}
// Render first whole line decorations and then regular decorations
this._renderWholeLineDecorations(ctx, decorations, output);
this._renderNormalDecorations(ctx, decorations, output);
this._renderResult = output;
}
private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void {
let lineHeight = String(this._lineHeight);
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
let d = decorations[i];
if (!d.source.options.isWholeLine) {
continue;
}
let decorationOutput = (
'<div class="cdr '
+ d.source.options.className
+ '" style="left:0;width:100%;height:'
+ lineHeight
+ 'px;"></div>'
);
let startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);
let endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);
for (let j = startLineNumber; j <= endLineNumber; j++) {
let lineIndex = j - visibleStartLineNumber;
output[lineIndex] += decorationOutput;
}
}
}
private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void {
let lineHeight = String(this._lineHeight);
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
const d = decorations[i];
if (d.source.options.isWholeLine) {
continue;
}
const className = d.source.options.className;
const showIfCollapsed = d.source.options.showIfCollapsed;
let range = d.range;
if (showIfCollapsed && range.endColumn === 1 && range.endLineNumber !== range.startLineNumber) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber - 1, this._context.model.getLineMaxColumn(range.endLineNumber - 1));
}
let linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch');
if (!linesVisibleRanges) {
continue;
}
for (let j = 0, lenJ = linesVisibleRanges.length; j < lenJ; j++) {
let lineVisibleRanges = linesVisibleRanges[j];
const lineIndex = lineVisibleRanges.lineNumber - visibleStartLineNumber;
if (showIfCollapsed && lineVisibleRanges.ranges.length === 1) {
const singleVisibleRange = lineVisibleRanges.ranges[0];
if (singleVisibleRange.width === 0) {
// collapsed range case => make the decoration visible by faking its width
lineVisibleRanges.ranges[0] = new HorizontalRange(singleVisibleRange.left, this._typicalHalfwidthCharacterWidth);
}
}
for (let k = 0, lenK = lineVisibleRanges.ranges.length; k < lenK; k++) {
const visibleRange = lineVisibleRanges.ranges[k];
const decorationOutput = (
'<div class="cdr '
+ className
+ '" style="left:'
+ String(visibleRange.left)
+ 'px;width:'
+ String(visibleRange.width)
+ 'px;height:'
+ lineHeight
+ 'px;"></div>'
);
output[lineIndex] += decorationOutput;
}
}
}
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
let lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
throw new Error('Unexpected render request');
}
return this._renderResult[lineIndex];
}
}

View File

@@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as dom from 'vs/base/browser/dom';
import { ScrollableElementCreationOptions, ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { IOverviewRulerLayoutInfo, SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { INewScrollPosition } from 'vs/editor/common/editorCommon';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { getThemeTypeSelector } from 'vs/platform/theme/common/themeService';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { ISimplifiedMouseEvent } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
export class EditorScrollbar extends ViewPart {
private scrollbar: SmoothScrollableElement;
private scrollbarDomNode: FastDomNode<HTMLElement>;
constructor(
context: ViewContext,
linesContent: FastDomNode<HTMLElement>,
viewDomNode: FastDomNode<HTMLElement>,
overflowGuardDomNode: FastDomNode<HTMLElement>
) {
super(context);
const editor = this._context.configuration.editor;
const configScrollbarOpts = editor.viewInfo.scrollbar;
let scrollbarOptions: ScrollableElementCreationOptions = {
listenOnDomNode: viewDomNode.domNode,
className: 'editor-scrollable' + ' ' + getThemeTypeSelector(context.theme.type),
useShadows: false,
lazyRender: true,
vertical: configScrollbarOpts.vertical,
horizontal: configScrollbarOpts.horizontal,
verticalHasArrows: configScrollbarOpts.verticalHasArrows,
horizontalHasArrows: configScrollbarOpts.horizontalHasArrows,
verticalScrollbarSize: configScrollbarOpts.verticalScrollbarSize,
verticalSliderSize: configScrollbarOpts.verticalSliderSize,
horizontalScrollbarSize: configScrollbarOpts.horizontalScrollbarSize,
horizontalSliderSize: configScrollbarOpts.horizontalSliderSize,
handleMouseWheel: configScrollbarOpts.handleMouseWheel,
arrowSize: configScrollbarOpts.arrowSize,
mouseWheelScrollSensitivity: configScrollbarOpts.mouseWheelScrollSensitivity,
};
this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.scrollable));
PartFingerprints.write(this.scrollbar.getDomNode(), PartFingerprint.ScrollableElement);
this.scrollbarDomNode = createFastDomNode(this.scrollbar.getDomNode());
this.scrollbarDomNode.setPosition('absolute');
this._setLayout();
// When having a zone widget that calls .focus() on one of its dom elements,
// the browser will try desperately to reveal that dom node, unexpectedly
// changing the .scrollTop of this.linesContent
let onBrowserDesperateReveal = (domNode: HTMLElement, lookAtScrollTop: boolean, lookAtScrollLeft: boolean) => {
let newScrollPosition: INewScrollPosition = {};
if (lookAtScrollTop) {
let deltaTop = domNode.scrollTop;
if (deltaTop) {
newScrollPosition.scrollTop = this._context.viewLayout.getCurrentScrollTop() + deltaTop;
domNode.scrollTop = 0;
}
}
if (lookAtScrollLeft) {
let deltaLeft = domNode.scrollLeft;
if (deltaLeft) {
newScrollPosition.scrollLeft = this._context.viewLayout.getCurrentScrollLeft() + deltaLeft;
domNode.scrollLeft = 0;
}
}
this._context.viewLayout.setScrollPositionNow(newScrollPosition);
};
// I've seen this happen both on the view dom node & on the lines content dom node.
this._register(dom.addDisposableListener(viewDomNode.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(viewDomNode.domNode, true, true)));
this._register(dom.addDisposableListener(linesContent.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(linesContent.domNode, true, false)));
this._register(dom.addDisposableListener(overflowGuardDomNode.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(overflowGuardDomNode.domNode, true, false)));
}
public dispose(): void {
super.dispose();
}
private _setLayout(): void {
const layoutInfo = this._context.configuration.editor.layoutInfo;
this.scrollbarDomNode.setLeft(layoutInfo.contentLeft);
this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimapWidth);
this.scrollbarDomNode.setHeight(layoutInfo.contentHeight);
}
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
return this.scrollbar.getOverviewRulerLayoutInfo();
}
public getDomNode(): FastDomNode<HTMLElement> {
return this.scrollbarDomNode;
}
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
this.scrollbar.delegateVerticalScrollbarMouseDown(browserEvent);
}
public delegateSliderMouseDown(e: ISimplifiedMouseEvent, onDragFinished: () => void): void {
this.scrollbar.delegateSliderMouseDown(e, onDragFinished);
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.viewInfo) {
const editor = this._context.configuration.editor;
let newOpts: ScrollableElementChangeOptions = {
handleMouseWheel: editor.viewInfo.scrollbar.handleMouseWheel,
mouseWheelScrollSensitivity: editor.viewInfo.scrollbar.mouseWheelScrollSensitivity
};
this.scrollbar.updateOptions(newOpts);
}
if (e.layoutInfo) {
this._setLayout();
}
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return true;
}
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
this.scrollbar.updateClassName('editor-scrollable' + ' ' + getThemeTypeSelector(this._context.theme.type));
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
// Nothing to do
}
public render(ctx: RestrictedRenderingContext): void {
this.scrollbar.renderNow();
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .glyph-margin {
position: absolute;
top: 0;
}
/*
Keeping name short for faster parsing.
cgmr = core glyph margin rendering (div)
*/
.monaco-editor .margin-view-overlays .cgmr {
position: absolute;
}

View File

@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./glyphMargin';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export class DecorationToRender {
_decorationToRenderBrand: void;
public startLineNumber: number;
public endLineNumber: number;
public className: string;
constructor(startLineNumber: number, endLineNumber: number, className: string) {
this.startLineNumber = +startLineNumber;
this.endLineNumber = +endLineNumber;
this.className = String(className);
}
}
export abstract class DedupOverlay extends DynamicViewOverlay {
protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): string[][] {
let output: string[][] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = [];
}
if (decorations.length === 0) {
return output;
}
decorations.sort((a, b) => {
if (a.className === b.className) {
if (a.startLineNumber === b.startLineNumber) {
return a.endLineNumber - b.endLineNumber;
}
return a.startLineNumber - b.startLineNumber;
}
return (a.className < b.className ? -1 : 1);
});
let prevClassName: string = null;
let prevEndLineIndex = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
let d = decorations[i];
let className = d.className;
let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;
let endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;
if (prevClassName === className) {
startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex);
prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex);
} else {
prevClassName = className;
prevEndLineIndex = endLineIndex;
}
for (let i = startLineIndex; i <= prevEndLineIndex; i++) {
output[i].push(prevClassName);
}
}
return output;
}
}
export class GlyphMarginOverlay extends DedupOverlay {
private _context: ViewContext;
private _lineHeight: number;
private _glyphMargin: boolean;
private _glyphMarginLeft: number;
private _glyphMarginWidth: number;
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._glyphMargin = this._context.configuration.editor.viewInfo.glyphMargin;
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.viewInfo) {
this._glyphMargin = this._context.configuration.editor.viewInfo.glyphMargin;
}
if (e.layoutInfo) {
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
let decorations = ctx.getDecorationsInViewport();
let r: DecorationToRender[] = [], rLen = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
let d = decorations[i];
let glyphMarginClassName = d.source.options.glyphMarginClassName;
if (glyphMarginClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName);
}
}
return r;
}
public prepareRender(ctx: RenderingContext): void {
if (!this._glyphMargin) {
this._renderResult = null;
return;
}
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
let lineHeight = this._lineHeight.toString();
let left = this._glyphMarginLeft.toString();
let width = this._glyphMarginWidth.toString();
let common = '" style="left:' + left + 'px;width:' + width + 'px' + ';height:' + lineHeight + 'px;"></div>';
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
let classNames = toRender[lineIndex];
if (classNames.length === 0) {
output[lineIndex] = '';
} else {
output[lineIndex] = (
'<div class="cgmr '
+ classNames.join(' ')
+ common
);
}
}
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
let lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
throw new Error('Unexpected render request');
}
return this._renderResult[lineIndex];
}
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cigr = core ident guides rendering (div)
*/
.monaco-editor .lines-content .cigr {
position: absolute;
}

View File

@@ -0,0 +1,131 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./indentGuides';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry';
import * as dom from 'vs/base/browser/dom';
import { Position } from 'vs/editor/common/core/position';
export class IndentGuidesOverlay extends DynamicViewOverlay {
private _context: ViewContext;
private _lineHeight: number;
private _spaceWidth: number;
private _renderResult: string[];
private _enabled: boolean;
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides;
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.fontInfo) {
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
}
if (e.viewInfo) {
this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides;
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;// || e.scrollWidthChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
if (!this._enabled) {
this._renderResult = null;
return;
}
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const tabSize = this._context.model.getTabSize();
const tabWidth = tabSize * this._spaceWidth;
const lineHeight = this._lineHeight;
const indentGuideWidth = dom.computeScreenAwareSize(1);
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
let indent = this._context.model.getLineIndentGuide(lineNumber);
let result = '';
let leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1));
let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0;
for (let i = 0; i < indent; i++) {
result += `<div class="cigr" style="left:${left}px;height:${lineHeight}px;width:${indentGuideWidth}px"></div>`;
left += tabWidth;
}
output[lineIndex] = result;
}
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
let lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
throw new Error('Unexpected render request');
}
return this._renderResult[lineIndex];
}
}
registerThemingParticipant((theme, collector) => {
let editorGuideColor = theme.getColor(editorIndentGuides);
if (editorGuideColor) {
collector.addRule(`.monaco-editor .lines-content .cigr { background-color: ${editorGuideColor}; }`);
}
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="30" height="42" viewBox="0 0 30 42" style="enable-background:new 0 0 30 42;"><polygon style="fill:#FFFFFF;stroke:#000000;stroke-width:2;" points="29,2.4 3.8,27.6 14.3,27.6 9,38.1 15.4,40.2 20.6,29.7 29,36"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="36" viewBox="0 0 24 36.1"><defs><style>.a{fill:#fff;}</style></defs><title>flipped-cursor-mac-2x</title><polygon points="8.6 33.1 11.8 23.9 2.2 23.9 23 2.5 23 31.3 17.4 26.1 14.2 35.1 8.6 33.1"/><path class="a" d="M22,29.1l-5-4.6-3.062,8.938-4.062-1.5L13,23H5L22,5M0,25H10.4l-3,8.3L15,36.1l3.125-7.662L24,33V0Z"/></svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="18" viewBox="0 0 12 18"><style>.st0{fill:#fff}</style><title>flipped-cursor-mac</title><path d="M4.3 16.5l1.6-4.6H1.1L11.5 1.2v14.4L8.7 13l-1.6 4.5z"/><path class="st0" d="M11 14.5l-2.5-2.3L7 16.7 5 16l1.6-4.5h-4l8.5-9M0 12.5h5.2l-1.5 4.1L7.5 18 9 14.2l2.9 2.3V0L0 12.5z"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="21" x="0px" y="0px" viewBox="0 0 15 21" style="enable-background:new 0 0 15 21;"><polygon style="fill:#FFFFFF;stroke:#000000" points="14.5,1.2 1.9,13.8 7.1,13.8 4.5,19.1 7.7,20.1 10.3,14.9 14.5,18"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .margin-view-overlays .line-numbers {
position: absolute;
text-align: right;
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
cursor: default;
height: 100%;
}
.monaco-editor .relative-current-line-number {
text-align: left;
display: inline-block;
width: 100%;
}
.monaco-editor .margin-view-overlays .line-numbers {
cursor: -webkit-image-set(
url('flipped-cursor.svg') 1x,
url('flipped-cursor-2x.svg') 2x
) 30 0, default;
}
.monaco-editor.mac .margin-view-overlays .line-numbers {
cursor: -webkit-image-set(
url('flipped-cursor-mac.svg') 1x,
url('flipped-cursor-mac-2x.svg') 2x
) 24 3, default;
}
.monaco-editor .margin-view-overlays .line-numbers.lh-odd {
margin-top: 1px;
}

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./lineNumbers';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import * as platform from 'vs/base/common/platform';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { Position } from 'vs/editor/common/core/position';
export class LineNumbersOverlay extends DynamicViewOverlay {
public static CLASS_NAME = 'line-numbers';
private _context: ViewContext;
private _lineHeight: number;
private _renderLineNumbers: boolean;
private _renderCustomLineNumbers: (lineNumber: number) => string;
private _renderRelativeLineNumbers: boolean;
private _lineNumbersLeft: number;
private _lineNumbersWidth: number;
private _lastCursorModelPosition: Position;
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._readConfig();
this._lastCursorModelPosition = new Position(1, 1);
this._renderResult = null;
this._context.addEventHandler(this);
}
private _readConfig(): void {
const config = this._context.configuration.editor;
this._lineHeight = config.lineHeight;
this._renderLineNumbers = config.viewInfo.renderLineNumbers;
this._renderCustomLineNumbers = config.viewInfo.renderCustomLineNumbers;
this._renderRelativeLineNumbers = config.viewInfo.renderRelativeLineNumbers;
this._lineNumbersLeft = config.layoutInfo.lineNumbersLeft;
this._lineNumbersWidth = config.layoutInfo.lineNumbersWidth;
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
this._readConfig();
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
const primaryViewPosition = e.selections[0].getPosition();
this._lastCursorModelPosition = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(primaryViewPosition);
if (this._renderRelativeLineNumbers) {
return true;
}
return false;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
private _getLineRenderLineNumber(viewLineNumber: number): string {
const modelPosition = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(viewLineNumber, 1));
if (modelPosition.column !== 1) {
return '';
}
let modelLineNumber = modelPosition.lineNumber;
if (this._renderCustomLineNumbers) {
return this._renderCustomLineNumbers(modelLineNumber);
}
if (this._renderRelativeLineNumbers) {
let diff = Math.abs(this._lastCursorModelPosition.lineNumber - modelLineNumber);
if (diff === 0) {
return '<span class="relative-current-line-number">' + modelLineNumber + '</span>';
}
return String(diff);
}
return String(modelLineNumber);
}
public prepareRender(ctx: RenderingContext): void {
if (!this._renderLineNumbers) {
this._renderResult = null;
return;
}
let lineHeightClassName = (platform.isLinux ? (this._lineHeight % 2 === 0 ? ' lh-even' : ' lh-odd') : '');
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
let common = '<div class="' + LineNumbersOverlay.CLASS_NAME + lineHeightClassName + '" style="left:' + this._lineNumbersLeft.toString() + 'px;width:' + this._lineNumbersWidth.toString() + 'px;">';
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
let renderLineNumber = this._getLineRenderLineNumber(lineNumber);
if (renderLineNumber) {
output[lineIndex] = (
common
+ renderLineNumber
+ '</div>'
);
} else {
output[lineIndex] = '';
}
}
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
let lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
throw new Error('Unexpected render request');
}
return this._renderResult[lineIndex];
}
}
// theming
registerThemingParticipant((theme, collector) => {
let lineNumbers = theme.getColor(editorLineNumbers);
if (lineNumbers) {
collector.addRule(`.monaco-editor .line-numbers { color: ${lineNumbers}; }`);
}
});

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
class FloatHorizontalRange {
_floatHorizontalRangeBrand: void;
public readonly left: number;
public readonly width: number;
constructor(left: number, width: number) {
this.left = left;
this.width = width;
}
public toString(): string {
return `[${this.left},${this.width}]`;
}
public static compare(a: FloatHorizontalRange, b: FloatHorizontalRange): number {
return a.left - b.left;
}
}
export class RangeUtil {
/**
* Reusing the same range here
* because IE is buggy and constantly freezes when using a large number
* of ranges and calling .detach on them
*/
private static _handyReadyRange: Range;
private static _createRange(): Range {
if (!this._handyReadyRange) {
this._handyReadyRange = document.createRange();
}
return this._handyReadyRange;
}
private static _detachRange(range: Range, endNode: HTMLElement): void {
// Move range out of the span node, IE doesn't like having many ranges in
// the same spot and will act badly for lines containing dashes ('-')
range.selectNodeContents(endNode);
}
private static _readClientRects(startElement: Node, startOffset: number, endElement: Node, endOffset: number, endNode: HTMLElement): ClientRectList {
let range = this._createRange();
try {
range.setStart(startElement, startOffset);
range.setEnd(endElement, endOffset);
return range.getClientRects();
} catch (e) {
// This is life ...
return null;
} finally {
this._detachRange(range, endNode);
}
}
private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): HorizontalRange[] {
if (ranges.length === 1) {
// There is nothing to merge
return [new HorizontalRange(ranges[0].left, ranges[0].width)];
}
ranges.sort(FloatHorizontalRange.compare);
let result: HorizontalRange[] = [], resultLen = 0;
let prevLeft = ranges[0].left;
let prevWidth = ranges[0].width;
for (let i = 1, len = ranges.length; i < len; i++) {
const range = ranges[i];
const myLeft = range.left;
const myWidth = range.width;
if (prevLeft + prevWidth + 0.9 /* account for browser's rounding errors*/ >= myLeft) {
prevWidth = Math.max(prevWidth, myLeft + myWidth - prevLeft);
} else {
result[resultLen++] = new HorizontalRange(prevLeft, prevWidth);
prevLeft = myLeft;
prevWidth = myWidth;
}
}
result[resultLen++] = new HorizontalRange(prevLeft, prevWidth);
return result;
}
private static _createHorizontalRangesFromClientRects(clientRects: ClientRectList, clientRectDeltaLeft: number): HorizontalRange[] {
if (!clientRects || clientRects.length === 0) {
return null;
}
// We go through FloatHorizontalRange because it has been observed in bi-di text
// that the clientRects are not coming in sorted from the browser
let result: FloatHorizontalRange[] = [];
for (let i = 0, len = clientRects.length; i < len; i++) {
const clientRect = clientRects[i];
result[i] = new FloatHorizontalRange(Math.max(0, clientRect.left - clientRectDeltaLeft), clientRect.width);
}
return this._mergeAdjacentRanges(result);
}
public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
// Panic check
let min = 0;
let max = domNode.children.length - 1;
if (min > max) {
return null;
}
startChildIndex = Math.min(max, Math.max(min, startChildIndex));
endChildIndex = Math.min(max, Math.max(min, endChildIndex));
// If crossing over to a span only to select offset 0, then use the previous span's maximum offset
// Chrome is buggy and doesn't handle 0 offsets well sometimes.
if (startChildIndex !== endChildIndex) {
if (endChildIndex > 0 && endOffset === 0) {
endChildIndex--;
endOffset = Number.MAX_VALUE;
}
}
let startElement = domNode.children[startChildIndex].firstChild;
let endElement = domNode.children[endChildIndex].firstChild;
if (!startElement || !endElement) {
return null;
}
startOffset = Math.min(startElement.textContent.length, Math.max(0, startOffset));
endOffset = Math.min(endElement.textContent.length, Math.max(0, endOffset));
let clientRects = this._readClientRects(startElement, startOffset, endElement, endOffset, endNode);
return this._createHorizontalRangesFromClientRects(clientRects, clientRectDeltaLeft);
}
}

View File

@@ -0,0 +1,613 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as browser from 'vs/base/browser/browser';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IConfiguration } from 'vs/editor/common/editorCommon';
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { renderViewLine, RenderLineInput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { IVisibleLine } from 'vs/editor/browser/view/viewLayer';
import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil';
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import { ThemeType, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { IStringBuilder } from 'vs/editor/common/core/stringBuilder';
const canUseFastRenderedViewLine = (function () {
if (platform.isNative) {
// In VSCode we know very well when the zoom level changes
return true;
}
if (platform.isLinux || browser.isFirefox || browser.isSafari) {
// On Linux, it appears that zooming affects char widths (in pixels), which is unexpected.
// --
// Even though we read character widths correctly, having read them at a specific zoom level
// does not mean they are the same at the current zoom level.
// --
// This could be improved if we ever figure out how to get an event when browsers zoom,
// but until then we have to stick with reading client rects.
// --
// The same has been observed with Firefox on Windows7
// --
// The same has been oversved with Safari
return false;
}
return true;
})();
const alwaysRenderInlineSelection = (browser.isEdgeOrIE);
export class DomReadingContext {
private readonly _domNode: HTMLElement;
private _clientRectDeltaLeft: number;
private _clientRectDeltaLeftRead: boolean;
public get clientRectDeltaLeft(): number {
if (!this._clientRectDeltaLeftRead) {
this._clientRectDeltaLeftRead = true;
this._clientRectDeltaLeft = this._domNode.getBoundingClientRect().left;
}
return this._clientRectDeltaLeft;
}
public readonly endNode: HTMLElement;
constructor(domNode: HTMLElement, endNode: HTMLElement) {
this._domNode = domNode;
this._clientRectDeltaLeft = 0;
this._clientRectDeltaLeftRead = false;
this.endNode = endNode;
}
}
export class ViewLineOptions {
public readonly themeType: ThemeType;
public readonly renderWhitespace: 'none' | 'boundary' | 'all';
public readonly renderControlCharacters: boolean;
public readonly spaceWidth: number;
public readonly useMonospaceOptimizations: boolean;
public readonly lineHeight: number;
public readonly stopRenderingLineAfter: number;
public readonly fontLigatures: boolean;
constructor(config: IConfiguration, themeType: ThemeType) {
this.themeType = themeType;
this.renderWhitespace = config.editor.viewInfo.renderWhitespace;
this.renderControlCharacters = config.editor.viewInfo.renderControlCharacters;
this.spaceWidth = config.editor.fontInfo.spaceWidth;
this.useMonospaceOptimizations = (
config.editor.fontInfo.isMonospace
&& !config.editor.viewInfo.disableMonospaceOptimizations
);
this.lineHeight = config.editor.lineHeight;
this.stopRenderingLineAfter = config.editor.viewInfo.stopRenderingLineAfter;
this.fontLigatures = config.editor.viewInfo.fontLigatures;
}
public equals(other: ViewLineOptions): boolean {
return (
this.themeType === other.themeType
&& this.renderWhitespace === other.renderWhitespace
&& this.renderControlCharacters === other.renderControlCharacters
&& this.spaceWidth === other.spaceWidth
&& this.useMonospaceOptimizations === other.useMonospaceOptimizations
&& this.lineHeight === other.lineHeight
&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
&& this.fontLigatures === other.fontLigatures
);
}
}
export class ViewLine implements IVisibleLine {
public static CLASS_NAME = 'view-line';
private _options: ViewLineOptions;
private _isMaybeInvalid: boolean;
private _renderedViewLine: IRenderedViewLine;
constructor(options: ViewLineOptions) {
this._options = options;
this._isMaybeInvalid = true;
this._renderedViewLine = null;
}
// --- begin IVisibleLineData
public getDomNode(): HTMLElement {
if (this._renderedViewLine && this._renderedViewLine.domNode) {
return this._renderedViewLine.domNode.domNode;
}
return null;
}
public setDomNode(domNode: HTMLElement): void {
if (this._renderedViewLine) {
this._renderedViewLine.domNode = createFastDomNode(domNode);
} else {
throw new Error('I have no rendered view line to set the dom node to...');
}
}
public onContentChanged(): void {
this._isMaybeInvalid = true;
}
public onTokensChanged(): void {
this._isMaybeInvalid = true;
}
public onDecorationsChanged(): void {
this._isMaybeInvalid = true;
}
public onOptionsChanged(newOptions: ViewLineOptions): void {
this._isMaybeInvalid = true;
this._options = newOptions;
}
public onSelectionChanged(): boolean {
if (alwaysRenderInlineSelection || this._options.themeType === HIGH_CONTRAST) {
this._isMaybeInvalid = true;
return true;
}
return false;
}
public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: IStringBuilder): boolean {
if (this._isMaybeInvalid === false) {
// it appears that nothing relevant has changed
return false;
}
this._isMaybeInvalid = false;
const lineData = viewportData.getViewLineRenderingData(lineNumber);
const options = this._options;
const actualInlineDecorations = LineDecoration.filter(lineData.inlineDecorations, lineNumber, lineData.minColumn, lineData.maxColumn);
if (alwaysRenderInlineSelection || options.themeType === HIGH_CONTRAST) {
const selections = viewportData.selections;
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i];
if (selection.endLineNumber < lineNumber || selection.startLineNumber > lineNumber) {
// Selection does not intersect line
continue;
}
let startColumn = (selection.startLineNumber === lineNumber ? selection.startColumn : lineData.minColumn);
let endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn);
if (startColumn < endColumn) {
actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', false));
}
}
}
let renderLineInput = new RenderLineInput(
options.useMonospaceOptimizations,
lineData.content,
lineData.mightContainRTL,
lineData.minColumn - 1,
lineData.tokens,
actualInlineDecorations,
lineData.tabSize,
options.spaceWidth,
options.stopRenderingLineAfter,
options.renderWhitespace,
options.renderControlCharacters,
options.fontLigatures
);
if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) {
// no need to do anything, we have the same render input
return false;
}
sb.appendASCIIString('<div style="top:');
sb.appendASCIIString(String(deltaTop));
sb.appendASCIIString('px;height:');
sb.appendASCIIString(String(this._options.lineHeight));
sb.appendASCIIString('px;" class="');
sb.appendASCIIString(ViewLine.CLASS_NAME);
sb.appendASCIIString('">');
const output = renderViewLine(renderLineInput, sb);
sb.appendASCIIString('</div>');
let renderedViewLine: IRenderedViewLine = null;
if (canUseFastRenderedViewLine && options.useMonospaceOptimizations && !output.containsForeignElements) {
let isRegularASCII = true;
if (lineData.mightContainNonBasicASCII) {
isRegularASCII = strings.isBasicASCII(lineData.content);
}
if (isRegularASCII && lineData.content.length < 1000) {
// Browser rounding errors have been observed in Chrome and IE, so using the fast
// view line only for short lines. Please test before removing the length check...
renderedViewLine = new FastRenderedViewLine(
this._renderedViewLine ? this._renderedViewLine.domNode : null,
renderLineInput,
output.characterMapping
);
}
}
if (!renderedViewLine) {
renderedViewLine = createRenderedLine(
this._renderedViewLine ? this._renderedViewLine.domNode : null,
renderLineInput,
output.characterMapping,
output.containsRTL,
output.containsForeignElements
);
}
this._renderedViewLine = renderedViewLine;
return true;
}
public layoutLine(lineNumber: number, deltaTop: number): void {
if (this._renderedViewLine && this._renderedViewLine.domNode) {
this._renderedViewLine.domNode.setTop(deltaTop);
this._renderedViewLine.domNode.setHeight(this._options.lineHeight);
}
}
// --- end IVisibleLineData
public getWidth(): number {
if (!this._renderedViewLine) {
return 0;
}
return this._renderedViewLine.getWidth();
}
public getWidthIsFast(): boolean {
if (!this._renderedViewLine) {
return true;
}
return this._renderedViewLine.getWidthIsFast();
}
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
startColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, startColumn));
endColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, endColumn));
return this._renderedViewLine.getVisibleRangesForRange(startColumn, endColumn, context);
}
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
return this._renderedViewLine.getColumnOfNodeOffset(lineNumber, spanNode, offset);
}
}
interface IRenderedViewLine {
domNode: FastDomNode<HTMLElement>;
readonly input: RenderLineInput;
getWidth(): number;
getWidthIsFast(): boolean;
getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[];
getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number;
}
/**
* A rendered line which is guaranteed to contain only regular ASCII and is rendered with a monospace font.
*/
class FastRenderedViewLine implements IRenderedViewLine {
public domNode: FastDomNode<HTMLElement>;
public readonly input: RenderLineInput;
private readonly _characterMapping: CharacterMapping;
private readonly _charWidth: number;
constructor(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping) {
this.domNode = domNode;
this.input = renderLineInput;
this._characterMapping = characterMapping;
this._charWidth = renderLineInput.spaceWidth;
}
public getWidth(): number {
return this._getCharPosition(this._characterMapping.length);
}
public getWidthIsFast(): boolean {
return true;
}
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
startColumn = startColumn | 0; // @perf
endColumn = endColumn | 0; // @perf
const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) {
// This range is obviously not visible
return null;
}
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) {
startColumn = stopRenderingLineAfter;
}
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) {
endColumn = stopRenderingLineAfter;
}
const startPosition = this._getCharPosition(startColumn);
const endPosition = this._getCharPosition(endColumn);
return [new HorizontalRange(startPosition, endPosition - startPosition)];
}
private _getCharPosition(column: number): number {
const charOffset = this._characterMapping.getAbsoluteOffsets();
if (charOffset.length === 0) {
// No characters on this line
return 0;
}
return Math.round(this._charWidth * charOffset[column - 1]);
}
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
let spanNodeTextContentLength = spanNode.textContent.length;
let spanIndex = -1;
while (spanNode) {
spanNode = <HTMLElement>spanNode.previousSibling;
spanIndex++;
}
let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset);
return charOffset + 1;
}
}
/**
* Every time we render a line, we save what we have rendered in an instance of this class.
*/
class RenderedViewLine implements IRenderedViewLine {
public domNode: FastDomNode<HTMLElement>;
public readonly input: RenderLineInput;
protected readonly _characterMapping: CharacterMapping;
private readonly _isWhitespaceOnly: boolean;
private readonly _containsForeignElements: boolean;
private _cachedWidth: number;
/**
* This is a map that is used only when the line is guaranteed to have no RTL text.
*/
private _pixelOffsetCache: Int32Array;
constructor(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) {
this.domNode = domNode;
this.input = renderLineInput;
this._characterMapping = characterMapping;
this._isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent);
this._containsForeignElements = containsForeignElements;
this._cachedWidth = -1;
this._pixelOffsetCache = null;
if (!containsRTL || this._characterMapping.length === 0 /* the line is empty */) {
this._pixelOffsetCache = new Int32Array(this._characterMapping.length + 1);
for (let column = 0, len = this._characterMapping.length; column <= len; column++) {
this._pixelOffsetCache[column] = -1;
}
}
}
// --- Reading from the DOM methods
protected _getReadingTarget(): HTMLElement {
return <HTMLSpanElement>this.domNode.domNode.firstChild;
}
/**
* Width of the line in pixels
*/
public getWidth(): number {
if (this._cachedWidth === -1) {
this._cachedWidth = this._getReadingTarget().offsetWidth;
}
return this._cachedWidth;
}
public getWidthIsFast(): boolean {
if (this._cachedWidth === -1) {
return false;
}
return true;
}
/**
* Visible ranges for a model range
*/
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
startColumn = startColumn | 0; // @perf
endColumn = endColumn | 0; // @perf
const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) {
// This range is obviously not visible
return null;
}
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) {
startColumn = stopRenderingLineAfter;
}
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) {
endColumn = stopRenderingLineAfter;
}
if (this._pixelOffsetCache !== null) {
// the text is LTR
let startOffset = this._readPixelOffset(startColumn, context);
if (startOffset === -1) {
return null;
}
let endOffset = this._readPixelOffset(endColumn, context);
if (endOffset === -1) {
return null;
}
return [new HorizontalRange(startOffset, endOffset - startOffset)];
}
return this._readVisibleRangesForRange(startColumn, endColumn, context);
}
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
if (startColumn === endColumn) {
let pixelOffset = this._readPixelOffset(startColumn, context);
if (pixelOffset === -1) {
return null;
} else {
return [new HorizontalRange(pixelOffset, 0)];
}
} else {
return this._readRawVisibleRangesForRange(startColumn, endColumn, context);
}
}
protected _readPixelOffset(column: number, context: DomReadingContext): number {
if (this._characterMapping.length === 0) {
// This line has no content
if (!this._containsForeignElements) {
// We can assume the line is really empty
return 0;
}
}
if (this._pixelOffsetCache !== null) {
// the text is LTR
let cachedPixelOffset = this._pixelOffsetCache[column];
if (cachedPixelOffset !== -1) {
return cachedPixelOffset;
}
let result = this._actualReadPixelOffset(column, context);
this._pixelOffsetCache[column] = result;
return result;
}
return this._actualReadPixelOffset(column, context);
}
private _actualReadPixelOffset(column: number, context: DomReadingContext): number {
if (this._characterMapping.length === 0) {
// This line has no content
let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), 0, 0, 0, 0, context.clientRectDeltaLeft, context.endNode);
if (!r || r.length === 0) {
return -1;
}
return r[0].left;
}
if (column === this._characterMapping.length && this._isWhitespaceOnly && !this._containsForeignElements) {
// This branch helps in the case of whitespace only lines which have a width set
return this.getWidth();
}
let partData = this._characterMapping.charOffsetToPartData(column - 1);
let partIndex = CharacterMapping.getPartIndex(partData);
let charOffsetInPart = CharacterMapping.getCharIndex(partData);
let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, context.clientRectDeltaLeft, context.endNode);
if (!r || r.length === 0) {
return -1;
}
return r[0].left;
}
private _readRawVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
if (startColumn === 1 && endColumn === this._characterMapping.length) {
// This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line
return [new HorizontalRange(0, this.getWidth())];
}
let startPartData = this._characterMapping.charOffsetToPartData(startColumn - 1);
let startPartIndex = CharacterMapping.getPartIndex(startPartData);
let startCharOffsetInPart = CharacterMapping.getCharIndex(startPartData);
let endPartData = this._characterMapping.charOffsetToPartData(endColumn - 1);
let endPartIndex = CharacterMapping.getPartIndex(endPartData);
let endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData);
return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, context.clientRectDeltaLeft, context.endNode);
}
/**
* Returns the column for the text found at a specific offset inside a rendered dom node
*/
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
let spanNodeTextContentLength = spanNode.textContent.length;
let spanIndex = -1;
while (spanNode) {
spanNode = <HTMLElement>spanNode.previousSibling;
spanIndex++;
}
let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset);
return charOffset + 1;
}
}
class WebKitRenderedViewLine extends RenderedViewLine {
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
let output = super._readVisibleRangesForRange(startColumn, endColumn, context);
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) {
return output;
}
// WebKit is buggy and returns an expanded range (to contain words in some cases)
// The last client rect is enlarged (I think)
// This is an attempt to patch things up
// Find position of previous column
let beforeEndPixelOffset = this._readPixelOffset(endColumn - 1, context);
// Find position of last column
let endPixelOffset = this._readPixelOffset(endColumn, context);
if (beforeEndPixelOffset !== -1 && endPixelOffset !== -1) {
let isLTR = (beforeEndPixelOffset <= endPixelOffset);
let lastRange = output[output.length - 1];
if (isLTR && lastRange.left < endPixelOffset) {
// Trim down the width of the last visible range to not go after the last column's position
lastRange.width = endPixelOffset - lastRange.left;
}
}
return output;
}
}
const createRenderedLine: (domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) => RenderedViewLine = (function () {
if (browser.isWebKit) {
return createWebKitRenderedLine;
}
return createNormalRenderedLine;
})();
function createWebKitRenderedLine(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine {
return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements);
}
function createNormalRenderedLine(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine {
return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements);
}

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Uncomment to see lines flashing when they're painted */
/*.monaco-editor .view-lines > .view-line {
background-color: none;
animation-name: flash-background;
animation-duration: 800ms;
}
@keyframes flash-background {
0% { background-color: lightgreen; }
100% { background-color: none }
}*/
.monaco-editor .lines-content,
.monaco-editor .view-line,
.monaco-editor .view-lines {
-webkit-user-select: text;
-ms-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-o-user-select: text;
user-select: text;
}
.monaco-editor.ie .lines-content,
.monaco-editor.ie .view-line,
.monaco-editor.ie .view-lines {
-ms-user-select: none;
user-select: none;
}
.monaco-editor .view-lines {
cursor: text;
white-space: nowrap;
}
.monaco-editor.vs-dark.mac .view-lines,
.monaco-editor.hc-black.mac .view-lines {
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
}
.monaco-editor .view-line {
position: absolute;
width: 100%;
}
/* TODO@tokenization bootstrap fix */
/*.monaco-editor .view-line > span > span {
float: none;
min-height: inherit;
margin-left: inherit;
}*/

View File

@@ -0,0 +1,689 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./viewLines';
import { RunOnceScheduler } from 'vs/base/common/async';
import { FastDomNode } from 'vs/base/browser/fastDomNode';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { VisibleLinesCollection, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer';
import { ViewLineOptions, DomReadingContext, ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import { IViewLines, HorizontalRange, LineVisibleRanges } from 'vs/editor/common/view/renderingContext';
import { Viewport } from 'vs/editor/common/viewModel/viewModel';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { ScrollType } from 'vs/editor/common/editorCommon';
class LastRenderedData {
private _currentVisibleRange: Range;
constructor() {
this._currentVisibleRange = new Range(1, 1, 1, 1);
}
public getCurrentVisibleRange(): Range {
return this._currentVisibleRange;
}
public setCurrentVisibleRange(currentVisibleRange: Range): void {
this._currentVisibleRange = currentVisibleRange;
}
}
class HorizontalRevealRequest {
public readonly lineNumber: number;
public readonly startColumn: number;
public readonly endColumn: number;
public readonly startScrollTop: number;
public readonly stopScrollTop: number;
public readonly scrollType: ScrollType;
constructor(lineNumber: number, startColumn: number, endColumn: number, startScrollTop: number, stopScrollTop: number, scrollType: ScrollType) {
this.lineNumber = lineNumber;
this.startColumn = startColumn;
this.endColumn = endColumn;
this.startScrollTop = startScrollTop;
this.stopScrollTop = stopScrollTop;
this.scrollType = scrollType;
}
}
export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>, IViewLines {
/**
* Adds this ammount of pixels to the right of lines (no-one wants to type near the edge of the viewport)
*/
private static HORIZONTAL_EXTRA_PX = 30;
private readonly _linesContent: FastDomNode<HTMLElement>;
private readonly _textRangeRestingSpot: HTMLElement;
private readonly _visibleLines: VisibleLinesCollection<ViewLine>;
private readonly domNode: FastDomNode<HTMLElement>;
// --- config
private _lineHeight: number;
private _typicalHalfwidthCharacterWidth: number;
private _isViewportWrapping: boolean;
private _revealHorizontalRightPadding: number;
private _canUseLayerHinting: boolean;
private _viewLineOptions: ViewLineOptions;
// --- width
private _maxLineWidth: number;
private _asyncUpdateLineWidths: RunOnceScheduler;
private _horizontalRevealRequest: HorizontalRevealRequest;
private _lastRenderedData: LastRenderedData;
constructor(context: ViewContext, linesContent: FastDomNode<HTMLElement>) {
super(context);
this._linesContent = linesContent;
this._textRangeRestingSpot = document.createElement('div');
this._visibleLines = new VisibleLinesCollection(this);
this.domNode = this._visibleLines.domNode;
const conf = this._context.configuration;
this._lineHeight = conf.editor.lineHeight;
this._typicalHalfwidthCharacterWidth = conf.editor.fontInfo.typicalHalfwidthCharacterWidth;
this._isViewportWrapping = conf.editor.wrappingInfo.isViewportWrapping;
this._revealHorizontalRightPadding = conf.editor.viewInfo.revealHorizontalRightPadding;
this._canUseLayerHinting = conf.editor.canUseLayerHinting;
this._viewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
PartFingerprints.write(this.domNode, PartFingerprint.ViewLines);
this.domNode.setClassName('view-lines');
Configuration.applyFontInfo(this.domNode, conf.editor.fontInfo);
// --- width & height
this._maxLineWidth = 0;
this._asyncUpdateLineWidths = new RunOnceScheduler(() => {
this._updateLineWidthsSlow();
}, 200);
this._lastRenderedData = new LastRenderedData();
this._horizontalRevealRequest = null;
}
public dispose(): void {
this._asyncUpdateLineWidths.dispose();
super.dispose();
}
public getDomNode(): FastDomNode<HTMLElement> {
return this.domNode;
}
// ---- begin IVisibleLinesHost
public createVisibleLine(): ViewLine {
return new ViewLine(this._viewLineOptions);
}
// ---- end IVisibleLinesHost
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
this._visibleLines.onConfigurationChanged(e);
if (e.wrappingInfo) {
this._maxLineWidth = 0;
}
const conf = this._context.configuration;
if (e.lineHeight) {
this._lineHeight = conf.editor.lineHeight;
}
if (e.fontInfo) {
this._typicalHalfwidthCharacterWidth = conf.editor.fontInfo.typicalHalfwidthCharacterWidth;
}
if (e.wrappingInfo) {
this._isViewportWrapping = conf.editor.wrappingInfo.isViewportWrapping;
}
if (e.viewInfo) {
this._revealHorizontalRightPadding = conf.editor.viewInfo.revealHorizontalRightPadding;
}
if (e.canUseLayerHinting) {
this._canUseLayerHinting = conf.editor.canUseLayerHinting;
}
if (e.fontInfo) {
Configuration.applyFontInfo(this.domNode, conf.editor.fontInfo);
}
this._onOptionsMaybeChanged();
if (e.layoutInfo) {
this._maxLineWidth = 0;
}
return true;
}
private _onOptionsMaybeChanged(): boolean {
const conf = this._context.configuration;
let newViewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
if (!this._viewLineOptions.equals(newViewLineOptions)) {
this._viewLineOptions = newViewLineOptions;
let startLineNumber = this._visibleLines.getStartLineNumber();
let endLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
let line = this._visibleLines.getVisibleLine(lineNumber);
line.onOptionsChanged(this._viewLineOptions);
}
return true;
}
return false;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
let r = false;
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
r = this._visibleLines.getVisibleLine(lineNumber).onSelectionChanged() || r;
}
return r;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
if (true/*e.inlineDecorationsChanged*/) {
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged();
}
}
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
let shouldRender = this._visibleLines.onFlushed(e);
this._maxLineWidth = 0;
return shouldRender;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return this._visibleLines.onLinesChanged(e);
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return this._visibleLines.onLinesDeleted(e);
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return this._visibleLines.onLinesInserted(e);
}
public onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean {
// Using the future viewport here in order to handle multiple
// incoming reveal range requests that might all desire to be animated
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.range, e.verticalType);
// validate the new desired scroll top
let newScrollPosition = this._context.viewLayout.validateScrollPosition({ scrollTop: desiredScrollTop });
if (e.revealHorizontal) {
if (e.range.startLineNumber !== e.range.endLineNumber) {
// Two or more lines? => scroll to base (That's how you see most of the two lines)
newScrollPosition = {
scrollTop: newScrollPosition.scrollTop,
scrollLeft: 0
};
} else {
// We don't necessarily know the horizontal offset of this range since the line might not be in the view...
this._horizontalRevealRequest = new HorizontalRevealRequest(e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
}
} else {
this._horizontalRevealRequest = null;
}
const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop);
if (e.scrollType === ScrollType.Smooth && scrollTopDelta > this._lineHeight) {
this._context.viewLayout.setScrollPositionSmooth(newScrollPosition);
} else {
this._context.viewLayout.setScrollPositionNow(newScrollPosition);
}
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
if (this._horizontalRevealRequest && e.scrollLeftChanged) {
// cancel any outstanding horizontal reveal request if someone else scrolls horizontally.
this._horizontalRevealRequest = null;
}
if (this._horizontalRevealRequest && e.scrollTopChanged) {
const min = Math.min(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
const max = Math.max(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
if (e.scrollTop < min || e.scrollTop > max) {
// cancel any outstanding horizontal reveal request if someone else scrolls vertically.
this._horizontalRevealRequest = null;
}
}
this.domNode.setWidth(e.scrollWidth);
return this._visibleLines.onScrollChanged(e) || true;
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
return this._visibleLines.onTokensChanged(e);
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return this._visibleLines.onZonesChanged(e);
}
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
return this._onOptionsMaybeChanged();
}
// ---- end view event handlers
// ----------- HELPERS FOR OTHERS
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position {
let viewLineDomNode = this._getViewLineDomNode(spanNode);
if (viewLineDomNode === null) {
// Couldn't find view line node
return null;
}
let lineNumber = this._getLineNumberFor(viewLineDomNode);
if (lineNumber === -1) {
// Couldn't find view line node
return null;
}
if (lineNumber < 1 || lineNumber > this._context.model.getLineCount()) {
// lineNumber is outside range
return null;
}
if (this._context.model.getLineMaxColumn(lineNumber) === 1) {
// Line is empty
return new Position(lineNumber, 1);
}
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
// Couldn't find line
return null;
}
let column = this._visibleLines.getVisibleLine(lineNumber).getColumnOfNodeOffset(lineNumber, spanNode, offset);
let minColumn = this._context.model.getLineMinColumn(lineNumber);
if (column < minColumn) {
column = minColumn;
}
return new Position(lineNumber, column);
}
private _getViewLineDomNode(node: HTMLElement): HTMLElement {
while (node && node.nodeType === 1) {
if (node.className === ViewLine.CLASS_NAME) {
return node;
}
node = node.parentElement;
}
return null;
}
/**
* @returns the line number of this view line dom node.
*/
private _getLineNumberFor(domNode: HTMLElement): number {
let startLineNumber = this._visibleLines.getStartLineNumber();
let endLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
let line = this._visibleLines.getVisibleLine(lineNumber);
if (domNode === line.getDomNode()) {
return lineNumber;
}
}
return -1;
}
public getLineWidth(lineNumber: number): number {
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
// Couldn't find line
return -1;
}
return this._visibleLines.getVisibleLine(lineNumber).getWidth();
}
public linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] {
if (this.shouldRender()) {
// Cannot read from the DOM because it is dirty
// i.e. the model & the dom are out of sync, so I'd be reading something stale
return null;
}
let originalEndLineNumber = range.endLineNumber;
range = Range.intersectRanges(range, this._lastRenderedData.getCurrentVisibleRange());
if (!range) {
return null;
}
let visibleRanges: LineVisibleRanges[] = [], visibleRangesLen = 0;
let domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
let nextLineModelLineNumber: number;
if (includeNewLines) {
nextLineModelLineNumber = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
}
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
continue;
}
let startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
let endColumn = lineNumber === range.endLineNumber ? range.endColumn : this._context.model.getLineMaxColumn(lineNumber);
let visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, domReadingContext);
if (!visibleRangesForLine || visibleRangesForLine.length === 0) {
continue;
}
if (includeNewLines && lineNumber < originalEndLineNumber) {
let currentLineModelLineNumber = nextLineModelLineNumber;
nextLineModelLineNumber = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;
if (currentLineModelLineNumber !== nextLineModelLineNumber) {
visibleRangesForLine[visibleRangesForLine.length - 1].width += this._typicalHalfwidthCharacterWidth;
}
}
visibleRanges[visibleRangesLen++] = new LineVisibleRanges(lineNumber, visibleRangesForLine);
}
if (visibleRangesLen === 0) {
return null;
}
return visibleRanges;
}
public visibleRangesForRange2(range: Range): HorizontalRange[] {
if (this.shouldRender()) {
// Cannot read from the DOM because it is dirty
// i.e. the model & the dom are out of sync, so I'd be reading something stale
return null;
}
range = Range.intersectRanges(range, this._lastRenderedData.getCurrentVisibleRange());
if (!range) {
return null;
}
let result: HorizontalRange[] = [];
let domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
continue;
}
let startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
let endColumn = lineNumber === range.endLineNumber ? range.endColumn : this._context.model.getLineMaxColumn(lineNumber);
let visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, domReadingContext);
if (!visibleRangesForLine || visibleRangesForLine.length === 0) {
continue;
}
result = result.concat(visibleRangesForLine);
}
if (result.length === 0) {
return null;
}
return result;
}
// --- implementation
/**
* Updates the max line width if it is fast to compute.
* Returns true if all lines were taken into account.
* Returns false if some lines need to be reevaluated (in a slow fashion).
*/
private _updateLineWidthsFast(): boolean {
return this._updateLineWidths(true);
}
private _updateLineWidthsSlow(): void {
this._updateLineWidths(false);
}
private _updateLineWidths(fast: boolean): boolean {
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
let localMaxLineWidth = 1;
let allWidthsComputed = true;
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
if (fast && !visibleLine.getWidthIsFast()) {
// Cannot compute width in a fast way for this line
allWidthsComputed = false;
continue;
}
localMaxLineWidth = Math.max(localMaxLineWidth, visibleLine.getWidth());
}
if (allWidthsComputed && rendStartLineNumber === 1 && rendEndLineNumber === this._context.model.getLineCount()) {
// we know the max line width for all the lines
this._maxLineWidth = 0;
}
this._ensureMaxLineWidth(localMaxLineWidth);
return allWidthsComputed;
}
public prepareRender(): void {
throw new Error('Not supported');
}
public render(): void {
throw new Error('Not supported');
}
public renderText(viewportData: ViewportData): void {
// (1) render lines - ensures lines are in the DOM
this._visibleLines.renderLines(viewportData);
this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange);
this.domNode.setWidth(this._context.viewLayout.getScrollWidth());
this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000));
// (2) compute horizontal scroll position:
// - this must happen after the lines are in the DOM since it might need a line that rendered just now
// - it might change `scrollWidth` and `scrollLeft`
if (this._horizontalRevealRequest) {
const revealLineNumber = this._horizontalRevealRequest.lineNumber;
const revealStartColumn = this._horizontalRevealRequest.startColumn;
const revealEndColumn = this._horizontalRevealRequest.endColumn;
const scrollType = this._horizontalRevealRequest.scrollType;
// Check that we have the line that contains the horizontal range in the viewport
if (viewportData.startLineNumber <= revealLineNumber && revealLineNumber <= viewportData.endLineNumber) {
this._horizontalRevealRequest = null;
// allow `visibleRangesForRange2` to work
this.onDidRender();
// compute new scroll position
let newScrollLeft = this._computeScrollLeftToRevealRange(revealLineNumber, revealStartColumn, revealEndColumn);
let isViewportWrapping = this._isViewportWrapping;
if (!isViewportWrapping) {
// ensure `scrollWidth` is large enough
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
}
// set `scrollLeft`
if (scrollType === ScrollType.Smooth) {
this._context.viewLayout.setScrollPositionSmooth({
scrollLeft: newScrollLeft.scrollLeft
});
} else {
this._context.viewLayout.setScrollPositionNow({
scrollLeft: newScrollLeft.scrollLeft
});
}
}
}
// (3) handle scrolling
this._linesContent.setLayerHinting(this._canUseLayerHinting);
const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta;
this._linesContent.setTop(-adjustedScrollTop);
this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft());
// Update max line width (not so important, it is just so the horizontal scrollbar doesn't get too small)
if (!this._updateLineWidthsFast()) {
// Computing the width of some lines would be slow => delay it
this._asyncUpdateLineWidths.schedule();
}
}
// --- width
private _ensureMaxLineWidth(lineWidth: number): void {
let iLineWidth = Math.ceil(lineWidth);
if (this._maxLineWidth < iLineWidth) {
this._maxLineWidth = iLineWidth;
this._context.viewLayout.onMaxLineWidthChanged(this._maxLineWidth);
}
}
private _computeScrollTopToRevealRange(viewport: Viewport, range: Range, verticalType: viewEvents.VerticalRevealType): number {
let viewportStartY = viewport.top;
let viewportHeight = viewport.height;
let viewportEndY = viewportStartY + viewportHeight;
let boxStartY: number;
let boxEndY: number;
// Have a box that includes one extra line height (for the horizontal scrollbar)
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) {
// Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom
boxEndY += this._lineHeight;
}
let newScrollTop: number;
if (verticalType === viewEvents.VerticalRevealType.Center || verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport) {
if (verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
// Box is already in the viewport... do nothing
newScrollTop = viewportStartY;
} else {
// Box is outside the viewport... center it
let boxMiddleY = (boxStartY + boxEndY) / 2;
newScrollTop = Math.max(0, boxMiddleY - viewportHeight / 2);
}
} else {
newScrollTop = this._computeMinimumScrolling(viewportStartY, viewportEndY, boxStartY, boxEndY, verticalType === viewEvents.VerticalRevealType.Top, verticalType === viewEvents.VerticalRevealType.Bottom);
}
return newScrollTop;
}
private _computeScrollLeftToRevealRange(lineNumber: number, startColumn: number, endColumn: number): { scrollLeft: number; maxHorizontalOffset: number; } {
let maxHorizontalOffset = 0;
let viewport = this._context.viewLayout.getCurrentViewport();
let viewportStartX = viewport.left;
let viewportEndX = viewportStartX + viewport.width;
let visibleRanges = this.visibleRangesForRange2(new Range(lineNumber, startColumn, lineNumber, endColumn));
let boxStartX = Number.MAX_VALUE;
let boxEndX = 0;
if (!visibleRanges) {
// Unknown
return {
scrollLeft: viewportStartX,
maxHorizontalOffset: maxHorizontalOffset
};
}
for (let i = 0; i < visibleRanges.length; i++) {
let visibleRange = visibleRanges[i];
if (visibleRange.left < boxStartX) {
boxStartX = visibleRange.left;
}
if (visibleRange.left + visibleRange.width > boxEndX) {
boxEndX = visibleRange.left + visibleRange.width;
}
}
maxHorizontalOffset = boxEndX;
boxStartX = Math.max(0, boxStartX - ViewLines.HORIZONTAL_EXTRA_PX);
boxEndX += this._revealHorizontalRightPadding;
let newScrollLeft = this._computeMinimumScrolling(viewportStartX, viewportEndX, boxStartX, boxEndX);
return {
scrollLeft: newScrollLeft,
maxHorizontalOffset: maxHorizontalOffset
};
}
private _computeMinimumScrolling(viewportStart: number, viewportEnd: number, boxStart: number, boxEnd: number, revealAtStart?: boolean, revealAtEnd?: boolean): number {
viewportStart = viewportStart | 0;
viewportEnd = viewportEnd | 0;
boxStart = boxStart | 0;
boxEnd = boxEnd | 0;
revealAtStart = !!revealAtStart;
revealAtEnd = !!revealAtEnd;
let viewportLength = viewportEnd - viewportStart;
let boxLength = boxEnd - boxStart;
if (boxLength < viewportLength) {
// The box would fit in the viewport
if (revealAtStart) {
return boxStart;
}
if (revealAtEnd) {
return Math.max(0, boxEnd - viewportLength);
}
if (boxStart < viewportStart) {
// The box is above the viewport
return boxStart;
} else if (boxEnd > viewportEnd) {
// The box is below the viewport
return Math.max(0, boxEnd - viewportLength);
}
} else {
// The box would not fit in the viewport
// Reveal the beginning of the box
return boxStart;
}
return viewportStart;
}
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .lines-decorations {
position: absolute;
top: 0;
background: white;
}
/*
Keeping name short for faster parsing.
cldr = core lines decorations rendering (div)
*/
.monaco-editor .margin-view-overlays .cldr {
position: absolute;
height: 100%;
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./linesDecorations';
import { DecorationToRender, DedupOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export class LinesDecorationsOverlay extends DedupOverlay {
private _context: ViewContext;
private _decorationsLeft: number;
private _decorationsWidth: number;
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._decorationsLeft = this._context.configuration.editor.layoutInfo.decorationsLeft;
this._decorationsWidth = this._context.configuration.editor.layoutInfo.decorationsWidth;
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.layoutInfo) {
this._decorationsLeft = this._context.configuration.editor.layoutInfo.decorationsLeft;
this._decorationsWidth = this._context.configuration.editor.layoutInfo.decorationsWidth;
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
let decorations = ctx.getDecorationsInViewport();
let r: DecorationToRender[] = [], rLen = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
let d = decorations[i];
let linesDecorationsClassName = d.source.options.linesDecorationsClassName;
if (linesDecorationsClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName);
}
}
return r;
}
public prepareRender(ctx: RenderingContext): void {
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
let left = this._decorationsLeft.toString();
let width = this._decorationsWidth.toString();
let common = '" style="left:' + left + 'px;width:' + width + 'px;"></div>';
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
let classNames = toRender[lineIndex];
let lineOutput = '';
for (let i = 0, len = classNames.length; i < len; i++) {
lineOutput += '<div class="cldr ' + classNames[i] + common;
}
output[lineIndex] = lineOutput;
}
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
return this._renderResult[lineNumber - startLineNumber];
}
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export class Margin extends ViewPart {
public static CLASS_NAME = 'glyph-margin';
private _domNode: FastDomNode<HTMLElement>;
private _canUseLayerHinting: boolean;
private _contentLeft: number;
private _glyphMarginLeft: number;
private _glyphMarginWidth: number;
private _glyphMarginBackgroundDomNode: FastDomNode<HTMLElement>;
constructor(context: ViewContext) {
super(context);
this._canUseLayerHinting = this._context.configuration.editor.canUseLayerHinting;
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
this._domNode = this._createDomNode();
}
public dispose(): void {
super.dispose();
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
private _createDomNode(): FastDomNode<HTMLElement> {
let domNode = createFastDomNode(document.createElement('div'));
domNode.setClassName('margin');
domNode.setPosition('absolute');
domNode.setAttribute('role', 'presentation');
domNode.setAttribute('aria-hidden', 'true');
this._glyphMarginBackgroundDomNode = createFastDomNode(document.createElement('div'));
this._glyphMarginBackgroundDomNode.setClassName(Margin.CLASS_NAME);
domNode.appendChild(this._glyphMarginBackgroundDomNode);
return domNode;
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.canUseLayerHinting) {
this._canUseLayerHinting = this._context.configuration.editor.canUseLayerHinting;
}
if (e.layoutInfo) {
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
}
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return super.onScrollChanged(e) || e.scrollTopChanged;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(ctx: RestrictedRenderingContext): void {
this._domNode.setLayerHinting(this._canUseLayerHinting);
const adjustedScrollTop = ctx.scrollTop - ctx.bigNumbersDelta;
this._domNode.setTop(-adjustedScrollTop);
let height = Math.min(ctx.scrollHeight, 1000000);
this._domNode.setHeight(height);
this._domNode.setWidth(this._contentLeft);
this._glyphMarginBackgroundDomNode.setLeft(this._glyphMarginLeft);
this._glyphMarginBackgroundDomNode.setWidth(this._glyphMarginWidth);
this._glyphMarginBackgroundDomNode.setHeight(height);
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cmdr = core margin decorations rendering (div)
*/
.monaco-editor .margin-view-overlays .cmdr {
position: absolute;
left: 0;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./marginDecorations';
import { DecorationToRender, DedupOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export class MarginViewLineDecorationsOverlay extends DedupOverlay {
private _context: ViewContext;
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
let decorations = ctx.getDecorationsInViewport();
let r: DecorationToRender[] = [], rLen = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
let d = decorations[i];
let marginClassName = d.source.options.marginClassName;
if (marginClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName);
}
}
return r;
}
public prepareRender(ctx: RenderingContext): void {
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
let output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
let classNames = toRender[lineIndex];
let lineOutput = '';
for (let i = 0, len = classNames.length; i < len; i++) {
lineOutput += '<div class="cmdr ' + classNames[i] + '" style=""></div>';
}
output[lineIndex] = lineOutput;
}
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
return this._renderResult[lineNumber - startLineNumber];
}
}

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* START cover the case that slider is visible on mouseover */
.monaco-editor .minimap.slider-mouseover .minimap-slider {
opacity: 0;
transition: opacity 100ms linear;
}
.monaco-editor .minimap.slider-mouseover:hover .minimap-slider {
opacity: 1;
}
.monaco-editor .minimap.slider-mouseover .minimap-slider.active {
opacity: 1;
}
/* END cover the case that slider is visible on mouseover */
.monaco-editor .minimap-shadow-hidden {
position: absolute;
width: 0;
}
.monaco-editor .minimap-shadow-visible {
position: absolute;
left: -6px;
width: 6px;
}

View File

@@ -0,0 +1,911 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./minimap';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer';
import * as dom from 'vs/base/browser/dom';
import { MinimapCharRenderer, MinimapTokensColorTracker, Constants } from 'vs/editor/common/view/minimapCharRenderer';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { CharCode } from 'vs/base/common/charCode';
import { ViewLineData } from 'vs/editor/common/viewModel/viewModel';
import { ColorId } from 'vs/editor/common/modes';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IDisposable } from 'vs/base/common/lifecycle';
import { RenderedLinesCollection, ILine } from 'vs/editor/browser/view/viewLayer';
import { Range } from 'vs/editor/common/core/range';
import { RGBA8 } from 'vs/editor/common/core/rgba';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import * as platform from 'vs/base/common/platform';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
const enum RenderMinimap {
None = 0,
Small = 1,
Large = 2,
SmallBlocks = 3,
LargeBlocks = 4,
}
function getMinimapLineHeight(renderMinimap: RenderMinimap): number {
if (renderMinimap === RenderMinimap.Large) {
return Constants.x2_CHAR_HEIGHT;
}
if (renderMinimap === RenderMinimap.LargeBlocks) {
return Constants.x2_CHAR_HEIGHT + 2;
}
if (renderMinimap === RenderMinimap.Small) {
return Constants.x1_CHAR_HEIGHT;
}
// RenderMinimap.SmallBlocks
return Constants.x1_CHAR_HEIGHT + 1;
}
function getMinimapCharWidth(renderMinimap: RenderMinimap): number {
if (renderMinimap === RenderMinimap.Large) {
return Constants.x2_CHAR_WIDTH;
}
if (renderMinimap === RenderMinimap.LargeBlocks) {
return Constants.x2_CHAR_WIDTH;
}
if (renderMinimap === RenderMinimap.Small) {
return Constants.x1_CHAR_WIDTH;
}
// RenderMinimap.SmallBlocks
return Constants.x1_CHAR_WIDTH;
}
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const MOUSE_DRAG_RESET_DISTANCE = 140;
class MinimapOptions {
public readonly renderMinimap: RenderMinimap;
public readonly scrollBeyondLastLine: boolean;
public readonly showSlider: 'always' | 'mouseover';
public readonly pixelRatio: number;
public readonly typicalHalfwidthCharacterWidth: number;
public readonly lineHeight: number;
/**
* container dom node width (in CSS px)
*/
public readonly minimapWidth: number;
/**
* container dom node height (in CSS px)
*/
public readonly minimapHeight: number;
/**
* canvas backing store width (in device px)
*/
public readonly canvasInnerWidth: number;
/**
* canvas backing store height (in device px)
*/
public readonly canvasInnerHeight: number;
/**
* canvas width (in CSS px)
*/
public readonly canvasOuterWidth: number;
/**
* canvas height (in CSS px)
*/
public readonly canvasOuterHeight: number;
constructor(configuration: editorCommon.IConfiguration) {
const pixelRatio = configuration.editor.pixelRatio;
const layoutInfo = configuration.editor.layoutInfo;
const viewInfo = configuration.editor.viewInfo;
const fontInfo = configuration.editor.fontInfo;
this.renderMinimap = layoutInfo.renderMinimap | 0;
this.scrollBeyondLastLine = viewInfo.scrollBeyondLastLine;
this.showSlider = viewInfo.minimap.showSlider;
this.pixelRatio = pixelRatio;
this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
this.lineHeight = configuration.editor.lineHeight;
this.minimapWidth = layoutInfo.minimapWidth;
this.minimapHeight = layoutInfo.height;
this.canvasInnerWidth = Math.max(1, Math.floor(pixelRatio * this.minimapWidth));
this.canvasInnerHeight = Math.max(1, Math.floor(pixelRatio * this.minimapHeight));
this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio;
this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio;
}
public equals(other: MinimapOptions): boolean {
return (this.renderMinimap === other.renderMinimap
&& this.scrollBeyondLastLine === other.scrollBeyondLastLine
&& this.showSlider === other.showSlider
&& this.pixelRatio === other.pixelRatio
&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
&& this.lineHeight === other.lineHeight
&& this.minimapWidth === other.minimapWidth
&& this.minimapHeight === other.minimapHeight
&& this.canvasInnerWidth === other.canvasInnerWidth
&& this.canvasInnerHeight === other.canvasInnerHeight
&& this.canvasOuterWidth === other.canvasOuterWidth
&& this.canvasOuterHeight === other.canvasOuterHeight
);
}
}
class MinimapLayout {
/**
* The given editor scrollTop (input).
*/
public readonly scrollTop: number;
/**
* The given editor scrollHeight (input).
*/
public readonly scrollHeight: number;
private readonly _computedSliderRatio: number;
/**
* slider dom node top (in CSS px)
*/
public readonly sliderTop: number;
/**
* slider dom node height (in CSS px)
*/
public readonly sliderHeight: number;
/**
* minimap render start line number.
*/
public readonly startLineNumber: number;
/**
* minimap render end line number.
*/
public readonly endLineNumber: number;
constructor(
scrollTop: number,
scrollHeight: number,
computedSliderRatio: number,
sliderTop: number,
sliderHeight: number,
startLineNumber: number,
endLineNumber: number
) {
this.scrollTop = scrollTop;
this.scrollHeight = scrollHeight;
this._computedSliderRatio = computedSliderRatio;
this.sliderTop = sliderTop;
this.sliderHeight = sliderHeight;
this.startLineNumber = startLineNumber;
this.endLineNumber = endLineNumber;
}
/**
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
*/
public getDesiredScrollTopFromDelta(delta: number): number {
let desiredSliderPosition = this.sliderTop + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
public static create(
options: MinimapOptions,
viewportStartLineNumber: number,
viewportEndLineNumber: number,
viewportHeight: number,
viewportContainsWhitespaceGaps: boolean,
lineCount: number,
scrollTop: number,
scrollHeight: number,
previousLayout: MinimapLayout
): MinimapLayout {
const pixelRatio = options.pixelRatio;
const minimapLineHeight = getMinimapLineHeight(options.renderMinimap);
const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
const lineHeight = options.lineHeight;
// The visible line count in a viewport can change due to a number of reasons:
// a) with the same viewport width, different scroll positions can result in partial lines being visible:
// e.g. for a line height of 20, and a viewport height of 600
// * scrollTop = 0 => visible lines are [1, 30]
// * scrollTop = 10 => visible lines are [1, 31] (with lines 1 and 31 partially visible)
// * scrollTop = 20 => visible lines are [2, 31]
// b) whitespace gaps might make their way in the viewport (which results in a decrease in the visible line count)
// c) we could be in the scroll beyond last line case (which also results in a decrease in the visible line count, down to possibly only one line being visible)
// We must first establish a desirable slider height.
let sliderHeight: number;
if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {
// case b) from above: there are whitespace gaps in the viewport.
// In this case, the height of the slider directly reflects the visible line count.
const viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;
sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
} else {
// The slider has a stable height
const expectedViewportLineCount = viewportHeight / lineHeight;
sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);
}
let maxMinimapSliderTop: number;
if (options.scrollBeyondLastLine) {
// The minimap slider, when dragged all the way down, will contain the last line at its top
maxMinimapSliderTop = (lineCount - 1) * minimapLineHeight / pixelRatio;
} else {
// The minimap slider, when dragged all the way down, will contain the last line at its bottom
maxMinimapSliderTop = Math.max(0, lineCount * minimapLineHeight / pixelRatio - sliderHeight);
}
maxMinimapSliderTop = Math.min(options.minimapHeight - sliderHeight, maxMinimapSliderTop);
// The slider can move from 0 to `maxMinimapSliderTop`
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
const sliderTop = (scrollTop * computedSliderRatio);
if (minimapLinesFitting >= lineCount) {
// All lines fit in the minimap
const startLineNumber = 1;
const endLineNumber = lineCount;
return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
} else {
let startLineNumber = Math.max(1, Math.floor(viewportStartLineNumber - sliderTop * pixelRatio / minimapLineHeight));
// Avoid flickering caused by a partial viewport start line
// by being consistent w.r.t. the previous layout decision
if (previousLayout && previousLayout.scrollHeight === scrollHeight) {
if (previousLayout.scrollTop > scrollTop) {
// Scrolling up => never increase `startLineNumber`
startLineNumber = Math.min(startLineNumber, previousLayout.startLineNumber);
}
if (previousLayout.scrollTop < scrollTop) {
// Scrolling down => never decrease `startLineNumber`
startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber);
}
}
const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1);
return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
}
}
}
class MinimapLine implements ILine {
public static INVALID = new MinimapLine(-1);
dy: number;
constructor(dy: number) {
this.dy = dy;
}
public onContentChanged(): void {
this.dy = -1;
}
public onTokensChanged(): void {
this.dy = -1;
}
}
class RenderData {
/**
* last rendered layout.
*/
public readonly renderedLayout: MinimapLayout;
private readonly _imageData: ImageData;
private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;
constructor(
renderedLayout: MinimapLayout,
imageData: ImageData,
lines: MinimapLine[]
) {
this.renderedLayout = renderedLayout;
this._imageData = imageData;
this._renderedLines = new RenderedLinesCollection(
() => MinimapLine.INVALID
);
this._renderedLines._set(renderedLayout.startLineNumber, lines);
}
/**
* Check if the current RenderData matches accurately the new desired layout and no painting is needed.
*/
public linesEquals(layout: MinimapLayout): boolean {
if (this.renderedLayout.startLineNumber !== layout.startLineNumber) {
return false;
}
if (this.renderedLayout.endLineNumber !== layout.endLineNumber) {
return false;
}
const tmp = this._renderedLines._get();
const lines = tmp.lines;
for (let i = 0, len = lines.length; i < len; i++) {
if (lines[i].dy === -1) {
// This line is invalid
return false;
}
}
return true;
}
_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[]; } {
let tmp = this._renderedLines._get();
return {
imageData: this._imageData,
rendLineNumberStart: tmp.rendLineNumberStart,
lines: tmp.lines
};
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return this._renderedLines.onLinesChanged(e.fromLineNumber, e.toLineNumber);
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void {
this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber);
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
return this._renderedLines.onTokensChanged(e.ranges);
}
}
/**
* Some sort of double buffering.
*
* Keeps two buffers around that will be rotated for painting.
* Always gives a buffer that is filled with the background color.
*/
class MinimapBuffers {
private readonly _backgroundFillData: Uint8ClampedArray;
private readonly _buffers: [ImageData, ImageData];
private _lastUsedBuffer: number;
constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {
this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);
this._buffers = [
ctx.createImageData(WIDTH, HEIGHT),
ctx.createImageData(WIDTH, HEIGHT)
];
this._lastUsedBuffer = 0;
}
public getBuffer(): ImageData {
// rotate buffers
this._lastUsedBuffer = 1 - this._lastUsedBuffer;
let result = this._buffers[this._lastUsedBuffer];
// fill with background color
result.data.set(this._backgroundFillData);
return result;
}
private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {
const backgroundR = background.r;
const backgroundG = background.g;
const backgroundB = background.b;
let result = new Uint8ClampedArray(WIDTH * HEIGHT * 4);
let offset = 0;
for (let i = 0; i < HEIGHT; i++) {
for (let j = 0; j < WIDTH; j++) {
result[offset] = backgroundR;
result[offset + 1] = backgroundG;
result[offset + 2] = backgroundB;
result[offset + 3] = 255;
offset += 4;
}
}
return result;
}
}
export class Minimap extends ViewPart {
private readonly _domNode: FastDomNode<HTMLElement>;
private readonly _shadow: FastDomNode<HTMLElement>;
private readonly _canvas: FastDomNode<HTMLCanvasElement>;
private readonly _slider: FastDomNode<HTMLElement>;
private readonly _sliderHorizontal: FastDomNode<HTMLElement>;
private readonly _tokensColorTracker: MinimapTokensColorTracker;
private readonly _mouseDownListener: IDisposable;
private readonly _sliderMouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
private readonly _sliderMouseDownListener: IDisposable;
private readonly _minimapCharRenderer: MinimapCharRenderer;
private _options: MinimapOptions;
private _lastRenderData: RenderData;
private _buffers: MinimapBuffers;
constructor(context: ViewContext) {
super(context);
this._options = new MinimapOptions(this._context.configuration);
this._lastRenderData = null;
this._buffers = null;
this._domNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this._domNode, PartFingerprint.Minimap);
this._domNode.setClassName(this._getMinimapDomNodeClassName());
this._domNode.setPosition('absolute');
this._domNode.setAttribute('role', 'presentation');
this._domNode.setAttribute('aria-hidden', 'true');
this._domNode.setRight(this._context.configuration.editor.layoutInfo.verticalScrollbarWidth);
this._shadow = createFastDomNode(document.createElement('div'));
this._shadow.setClassName('minimap-shadow-hidden');
this._domNode.appendChild(this._shadow);
this._canvas = createFastDomNode(document.createElement('canvas'));
this._canvas.setPosition('absolute');
this._canvas.setLeft(0);
this._domNode.appendChild(this._canvas);
this._slider = createFastDomNode(document.createElement('div'));
this._slider.setPosition('absolute');
this._slider.setClassName('minimap-slider');
this._slider.setLayerHinting(true);
this._domNode.appendChild(this._slider);
this._sliderHorizontal = createFastDomNode(document.createElement('div'));
this._sliderHorizontal.setPosition('absolute');
this._sliderHorizontal.setClassName('minimap-slider-horizontal');
this._slider.appendChild(this._sliderHorizontal);
this._tokensColorTracker = MinimapTokensColorTracker.getInstance();
this._minimapCharRenderer = getOrCreateMinimapCharRenderer();
this._applyLayout();
this._mouseDownListener = dom.addStandardDisposableListener(this._canvas.domNode, 'mousedown', (e) => {
e.preventDefault();
const renderMinimap = this._options.renderMinimap;
if (renderMinimap === RenderMinimap.None) {
return;
}
if (!this._lastRenderData) {
return;
}
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY;
const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);
let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber;
lineNumber = Math.min(lineNumber, this._context.model.getLineCount());
this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
new Range(lineNumber, 1, lineNumber, 1),
viewEvents.VerticalRevealType.Center,
false,
editorCommon.ScrollType.Smooth
));
});
this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>();
this._sliderMouseDownListener = dom.addStandardDisposableListener(this._slider.domNode, 'mousedown', (e) => {
e.preventDefault();
if (e.leftButton && this._lastRenderData) {
const initialMousePosition = e.posy;
const initialMouseOrthogonalPosition = e.posx;
const initialSliderState = this._lastRenderData.renderedLayout;
this._slider.toggleClassName('active', true);
this._sliderMouseMoveMonitor.startMonitoring(
standardMouseMoveMerger,
(mouseMoveData: IStandardMouseMoveEventData) => {
const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition);
if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
// The mouse has wondered away from the scrollbar => reset dragging
this._context.viewLayout.setScrollPositionNow({
scrollTop: initialSliderState.scrollTop
});
return;
}
const mouseDelta = mouseMoveData.posy - initialMousePosition;
this._context.viewLayout.setScrollPositionNow({
scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)
});
},
() => {
this._slider.toggleClassName('active', false);
}
);
}
});
}
public dispose(): void {
this._mouseDownListener.dispose();
this._sliderMouseMoveMonitor.dispose();
this._sliderMouseDownListener.dispose();
super.dispose();
}
private _getMinimapDomNodeClassName(): string {
if (this._options.showSlider === 'always') {
return 'minimap slider-always';
}
return 'minimap slider-mouseover';
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
private _applyLayout(): void {
this._domNode.setWidth(this._options.minimapWidth);
this._domNode.setHeight(this._options.minimapHeight);
this._shadow.setHeight(this._options.minimapHeight);
this._canvas.setWidth(this._options.canvasOuterWidth);
this._canvas.setHeight(this._options.canvasOuterHeight);
this._canvas.domNode.width = this._options.canvasInnerWidth;
this._canvas.domNode.height = this._options.canvasInnerHeight;
this._slider.setWidth(this._options.minimapWidth);
}
private _getBuffer(): ImageData {
if (!this._buffers) {
this._buffers = new MinimapBuffers(
this._canvas.domNode.getContext('2d'),
this._options.canvasInnerWidth,
this._options.canvasInnerHeight,
this._tokensColorTracker.getColor(ColorId.DefaultBackground)
);
}
return this._buffers.getBuffer();
}
private _onOptionsMaybeChanged(): boolean {
let opts = new MinimapOptions(this._context.configuration);
if (this._options.equals(opts)) {
return false;
}
this._options = opts;
this._lastRenderData = null;
this._buffers = null;
this._applyLayout();
this._domNode.setClassName(this._getMinimapDomNodeClassName());
return true;
}
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
return this._onOptionsMaybeChanged();
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
this._lastRenderData = null;
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
if (this._lastRenderData) {
return this._lastRenderData.onLinesChanged(e);
}
return false;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
if (this._lastRenderData) {
this._lastRenderData.onLinesDeleted(e);
}
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
if (this._lastRenderData) {
this._lastRenderData.onLinesInserted(e);
}
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return true;
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
if (this._lastRenderData) {
return this._lastRenderData.onTokensChanged(e);
}
return false;
}
public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
this._lastRenderData = null;
this._buffers = null;
return true;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
this._lastRenderData = null;
return true;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(renderingCtx: RestrictedRenderingContext): void {
const renderMinimap = this._options.renderMinimap;
if (renderMinimap === RenderMinimap.None) {
this._shadow.setClassName('minimap-shadow-hidden');
return;
}
if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {
this._shadow.setClassName('minimap-shadow-hidden');
} else {
this._shadow.setClassName('minimap-shadow-visible');
}
const layout = MinimapLayout.create(
this._options,
renderingCtx.visibleRange.startLineNumber,
renderingCtx.visibleRange.endLineNumber,
renderingCtx.viewportHeight,
(renderingCtx.viewportData.whitespaceViewportData.length > 0),
this._context.model.getLineCount(),
renderingCtx.scrollTop,
renderingCtx.scrollHeight,
this._lastRenderData ? this._lastRenderData.renderedLayout : null
);
this._slider.setTop(layout.sliderTop);
this._slider.setHeight(layout.sliderHeight);
// Compute horizontal slider coordinates
const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth;
const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap) / this._options.pixelRatio));
this._sliderHorizontal.setLeft(horizontalSliderLeft);
this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft);
this._sliderHorizontal.setTop(0);
this._sliderHorizontal.setHeight(layout.sliderHeight);
this._lastRenderData = this.renderLines(layout);
}
private renderLines(layout: MinimapLayout): RenderData {
const renderMinimap = this._options.renderMinimap;
const startLineNumber = layout.startLineNumber;
const endLineNumber = layout.endLineNumber;
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
// Check if nothing changed w.r.t. lines from last frame
if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
const _lastData = this._lastRenderData._get();
// Nice!! Nothing changed from last frame
return new RenderData(layout, _lastData.imageData, _lastData.lines);
}
// Oh well!! We need to repaint some lines...
const imageData = this._getBuffer();
// Render untouched lines by using last rendered data.
let needed = Minimap._renderUntouchedLines(
imageData,
startLineNumber,
endLineNumber,
minimapLineHeight,
this._lastRenderData
);
// Fetch rendering info from view model for rest of lines that need rendering.
const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
const tabSize = lineInfo.tabSize;
const background = this._tokensColorTracker.getColor(ColorId.DefaultBackground);
const useLighterFont = this._tokensColorTracker.backgroundIsLight();
// Render the rest of lines
let dy = 0;
let renderedLines: MinimapLine[] = [];
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
if (needed[lineIndex]) {
Minimap._renderLine(
imageData,
background,
useLighterFont,
renderMinimap,
this._tokensColorTracker,
this._minimapCharRenderer,
dy,
tabSize,
lineInfo.data[lineIndex]
);
}
renderedLines[lineIndex] = new MinimapLine(dy);
dy += minimapLineHeight;
}
// Finally, paint to the canvas
const ctx = this._canvas.domNode.getContext('2d');
ctx.putImageData(imageData, 0, 0);
// Save rendered data for reuse on next frame if possible
return new RenderData(
layout,
imageData,
renderedLines
);
}
private static _renderUntouchedLines(
target: ImageData,
startLineNumber: number,
endLineNumber: number,
minimapLineHeight: number,
lastRenderData: RenderData,
): boolean[] {
let needed: boolean[] = [];
if (!lastRenderData) {
for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
needed[i] = true;
}
return needed;
}
const _lastData = lastRenderData._get();
const lastTargetData = _lastData.imageData.data;
const lastStartLineNumber = _lastData.rendLineNumberStart;
const lastLines = _lastData.lines;
const lastLinesLength = lastLines.length;
const WIDTH = target.width;
const targetData = target.data;
let copySourceStart = -1;
let copySourceEnd = -1;
let copyDestStart = -1;
let copyDestEnd = -1;
let dest_dy = 0;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const lineIndex = lineNumber - startLineNumber;
const lastLineIndex = lineNumber - lastStartLineNumber;
const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);
if (source_dy === -1) {
needed[lineIndex] = true;
dest_dy += minimapLineHeight;
continue;
}
let sourceStart = source_dy * WIDTH * 4;
let sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;
let destStart = dest_dy * WIDTH * 4;
let destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;
if (copySourceEnd === sourceStart && copyDestEnd === destStart) {
// contiguous zone => extend copy request
copySourceEnd = sourceEnd;
copyDestEnd = destEnd;
} else {
if (copySourceStart !== -1) {
// flush existing copy request
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
}
copySourceStart = sourceStart;
copySourceEnd = sourceEnd;
copyDestStart = destStart;
copyDestEnd = destEnd;
}
needed[lineIndex] = false;
dest_dy += minimapLineHeight;
}
if (copySourceStart !== -1) {
// flush existing copy request
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
}
return needed;
}
private static _renderLine(
target: ImageData,
backgroundColor: RGBA8,
useLighterFont: boolean,
renderMinimap: RenderMinimap,
colorTracker: MinimapTokensColorTracker,
minimapCharRenderer: MinimapCharRenderer,
dy: number,
tabSize: number,
lineData: ViewLineData
): void {
const content = lineData.content;
const tokens = lineData.tokens;
const charWidth = getMinimapCharWidth(renderMinimap);
const maxDx = target.width - charWidth;
let dx = 0;
let charIndex = 0;
let tabsCharDelta = 0;
for (let tokenIndex = 0, tokensLen = tokens.length; tokenIndex < tokensLen; tokenIndex++) {
const token = tokens[tokenIndex];
const tokenEndIndex = token.endIndex;
const tokenColorId = token.getForeground();
const tokenColor = colorTracker.getColor(tokenColorId);
for (; charIndex < tokenEndIndex; charIndex++) {
if (dx > maxDx) {
// hit edge of minimap
return;
}
const charCode = content.charCodeAt(charIndex);
if (charCode === CharCode.Tab) {
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
// No need to render anything since tab is invisible
dx += insertSpacesCount * charWidth;
} else if (charCode === CharCode.Space) {
// No need to render anything since space is invisible
dx += charWidth;
} else {
if (renderMinimap === RenderMinimap.Large) {
minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
} else if (renderMinimap === RenderMinimap.Small) {
minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
} else if (renderMinimap === RenderMinimap.LargeBlocks) {
minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
} else {
// RenderMinimap.SmallBlocks
minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
}
dx += charWidth;
}
}
}
}
}
registerThemingParticipant((theme, collector) => {
const sliderBackground = theme.getColor(scrollbarSliderBackground);
if (sliderBackground) {
const halfSliderBackground = sliderBackground.transparent(0.5);
collector.addRule(`.monaco-editor .minimap-slider, .monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${halfSliderBackground}; }`);
}
const sliderHoverBackground = theme.getColor(scrollbarSliderHoverBackground);
if (sliderHoverBackground) {
const halfSliderHoverBackground = sliderHoverBackground.transparent(0.5);
collector.addRule(`.monaco-editor .minimap-slider:hover, .monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${halfSliderHoverBackground}; }`);
}
const sliderActiveBackground = theme.getColor(scrollbarSliderActiveBackground);
if (sliderActiveBackground) {
const halfSliderActiveBackground = sliderActiveBackground.transparent(0.5);
collector.addRule(`.monaco-editor .minimap-slider.active, .monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${halfSliderActiveBackground}; }`);
}
const shadow = theme.getColor(scrollbarShadow);
if (shadow) {
collector.addRule(`.monaco-editor .minimap-shadow-visible { box-shadow: ${shadow} -6px 0 6px -6px inset; }`);
}
});

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .overlayWidgets {
position: absolute;
top: 0;
left:0;
}

View File

@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./overlayWidgets';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IOverlayWidget, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
interface IWidgetData {
widget: IOverlayWidget;
preference: OverlayWidgetPositionPreference;
domNode: FastDomNode<HTMLElement>;
}
interface IWidgetMap {
[key: string]: IWidgetData;
}
export class ViewOverlayWidgets extends ViewPart {
private _widgets: IWidgetMap;
private _domNode: FastDomNode<HTMLElement>;
private _verticalScrollbarWidth: number;
private _minimapWidth: number;
private _horizontalScrollbarHeight: number;
private _editorHeight: number;
private _editorWidth: number;
constructor(context: ViewContext) {
super(context);
this._widgets = {};
this._verticalScrollbarWidth = this._context.configuration.editor.layoutInfo.verticalScrollbarWidth;
this._minimapWidth = this._context.configuration.editor.layoutInfo.minimapWidth;
this._horizontalScrollbarHeight = this._context.configuration.editor.layoutInfo.horizontalScrollbarHeight;
this._editorHeight = this._context.configuration.editor.layoutInfo.height;
this._editorWidth = this._context.configuration.editor.layoutInfo.width;
this._domNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this._domNode, PartFingerprint.OverlayWidgets);
this._domNode.setClassName('overlayWidgets');
}
public dispose(): void {
super.dispose();
this._widgets = null;
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.layoutInfo) {
this._verticalScrollbarWidth = this._context.configuration.editor.layoutInfo.verticalScrollbarWidth;
this._minimapWidth = this._context.configuration.editor.layoutInfo.minimapWidth;
this._horizontalScrollbarHeight = this._context.configuration.editor.layoutInfo.horizontalScrollbarHeight;
this._editorHeight = this._context.configuration.editor.layoutInfo.height;
this._editorWidth = this._context.configuration.editor.layoutInfo.width;
return true;
}
return false;
}
// ---- end view event handlers
public addWidget(widget: IOverlayWidget): void {
const domNode = createFastDomNode(widget.getDomNode());
this._widgets[widget.getId()] = {
widget: widget,
preference: null,
domNode: domNode
};
// This is sync because a widget wants to be in the dom
domNode.setPosition('absolute');
domNode.setAttribute('widgetId', widget.getId());
this._domNode.appendChild(domNode);
this.setShouldRender();
}
public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference): boolean {
let widgetData = this._widgets[widget.getId()];
if (widgetData.preference === preference) {
return false;
}
widgetData.preference = preference;
this.setShouldRender();
return true;
}
public removeWidget(widget: IOverlayWidget): void {
let widgetId = widget.getId();
if (this._widgets.hasOwnProperty(widgetId)) {
const widgetData = this._widgets[widgetId];
const domNode = widgetData.domNode.domNode;
delete this._widgets[widgetId];
domNode.parentNode.removeChild(domNode);
this.setShouldRender();
}
}
private _renderWidget(widgetData: IWidgetData): void {
const domNode = widgetData.domNode;
if (widgetData.preference === null) {
domNode.unsetTop();
return;
}
if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) {
domNode.setTop(0);
domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth);
} else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) {
let widgetHeight = domNode.domNode.clientHeight;
domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight));
domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth);
} else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) {
domNode.setTop(0);
domNode.domNode.style.right = '50%';
}
}
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(ctx: RestrictedRenderingContext): void {
this._domNode.setWidth(this._editorWidth);
let keys = Object.keys(this._widgets);
for (let i = 0, len = keys.length; i < len; i++) {
let widgetId = keys[i];
this._renderWidget(this._widgets[widgetId]);
}
}
}

View File

@@ -0,0 +1,267 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { Position } from 'vs/editor/common/core/position';
import { TokenizationRegistry } from 'vs/editor/common/modes';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry';
import { Color } from 'vs/base/common/color';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
export class DecorationsOverviewRuler extends ViewPart {
static MIN_DECORATION_HEIGHT = 6;
static MAX_DECORATION_HEIGHT = 60;
private readonly _tokensColorTrackerListener: IDisposable;
private _overviewRuler: OverviewRulerImpl;
private _renderBorder: boolean;
private _borderColor: string;
private _cursorColor: string;
private _shouldUpdateDecorations: boolean;
private _shouldUpdateCursorPosition: boolean;
private _hideCursor: boolean;
private _cursorPositions: Position[];
private _zonesFromDecorations: OverviewRulerZone[];
private _zonesFromCursors: OverviewRulerZone[];
constructor(context: ViewContext) {
super(context);
this._overviewRuler = new OverviewRulerImpl(
1,
'decorationsOverviewRuler',
this._context.viewLayout.getScrollHeight(),
this._context.configuration.editor.lineHeight,
this._context.configuration.editor.pixelRatio,
DecorationsOverviewRuler.MIN_DECORATION_HEIGHT,
DecorationsOverviewRuler.MAX_DECORATION_HEIGHT,
(lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber)
);
this._overviewRuler.setLanesCount(this._context.configuration.editor.viewInfo.overviewRulerLanes, false);
this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false);
this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder;
this._updateColors();
this._updateBackground(false);
this._tokensColorTrackerListener = TokenizationRegistry.onDidChange((e) => {
if (e.changedColorMap) {
this._updateBackground(true);
}
});
this._shouldUpdateDecorations = true;
this._zonesFromDecorations = [];
this._shouldUpdateCursorPosition = true;
this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler;
this._zonesFromCursors = [];
this._cursorPositions = [];
}
public dispose(): void {
super.dispose();
this._overviewRuler.dispose();
this._tokensColorTrackerListener.dispose();
}
private _updateBackground(render: boolean): void {
const minimapEnabled = this._context.configuration.editor.viewInfo.minimap.enabled;
this._overviewRuler.setUseBackground((minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null), render);
}
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
let prevLanesCount = this._overviewRuler.getLanesCount();
let newLanesCount = this._context.configuration.editor.viewInfo.overviewRulerLanes;
if (prevLanesCount !== newLanesCount) {
this._overviewRuler.setLanesCount(newLanesCount, false);
}
if (e.lineHeight) {
this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, false);
}
if (e.pixelRatio) {
this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, false);
}
if (e.viewInfo) {
this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder;
this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler;
this._shouldUpdateCursorPosition = true;
this._updateBackground(false);
}
if (e.layoutInfo) {
this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false);
}
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._shouldUpdateCursorPosition = true;
this._cursorPositions = [];
for (let i = 0, len = e.selections.length; i < len; i++) {
this._cursorPositions[i] = e.selections[i].getPosition();
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
this._shouldUpdateDecorations = true;
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
this._shouldUpdateCursorPosition = true;
this._shouldUpdateDecorations = true;
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this._overviewRuler.setScrollHeight(e.scrollHeight, false);
return super.onScrollChanged(e) || e.scrollHeightChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
this._updateColors();
this._shouldUpdateDecorations = true;
this._shouldUpdateCursorPosition = true;
return true;
}
// ---- end view event handlers
public getDomNode(): HTMLElement {
return this._overviewRuler.getDomNode();
}
private _updateColors() {
let borderColor = this._context.theme.getColor(editorOverviewRulerBorder);
this._borderColor = borderColor ? borderColor.toString() : null;
let cursorColor = this._context.theme.getColor(editorCursorForeground);
this._cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
this._overviewRuler.setThemeType(this._context.theme.type, false);
}
private _createZonesFromDecorations(): OverviewRulerZone[] {
let decorations = this._context.model.getAllOverviewRulerDecorations();
let zones: OverviewRulerZone[] = [];
for (let i = 0, len = decorations.length; i < len; i++) {
let dec = decorations[i];
let overviewRuler = dec.source.options.overviewRuler;
zones[i] = new OverviewRulerZone(
dec.range.startLineNumber,
dec.range.endLineNumber,
overviewRuler.position,
0,
this.resolveRulerColor(overviewRuler.color),
this.resolveRulerColor(overviewRuler.darkColor),
this.resolveRulerColor(overviewRuler.hcColor)
);
}
return zones;
}
private resolveRulerColor(color: string | ThemeColor): string {
if (editorCommon.isThemeColor(color)) {
let c = this._context.theme.getColor(color.id) || Color.transparent;
return c.toString();
}
return color;
}
private _createZonesFromCursors(): OverviewRulerZone[] {
let zones: OverviewRulerZone[] = [];
for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
let cursor = this._cursorPositions[i];
zones[i] = new OverviewRulerZone(
cursor.lineNumber,
cursor.lineNumber,
editorCommon.OverviewRulerLane.Full,
2,
this._cursorColor,
this._cursorColor,
this._cursorColor
);
}
return zones;
}
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(ctx: RestrictedRenderingContext): void {
if (this._shouldUpdateDecorations || this._shouldUpdateCursorPosition) {
if (this._shouldUpdateDecorations) {
this._shouldUpdateDecorations = false;
this._zonesFromDecorations = this._createZonesFromDecorations();
}
if (this._shouldUpdateCursorPosition) {
this._shouldUpdateCursorPosition = false;
if (this._hideCursor) {
this._zonesFromCursors = [];
} else {
this._zonesFromCursors = this._createZonesFromCursors();
}
}
let allZones: OverviewRulerZone[] = [];
allZones = allZones.concat(this._zonesFromCursors);
allZones = allZones.concat(this._zonesFromDecorations);
this._overviewRuler.setZones(allZones, false);
}
let hasRendered = this._overviewRuler.render(false);
if (hasRendered && this._renderBorder && this._borderColor && this._overviewRuler.getLanesCount() > 0 && (this._zonesFromDecorations.length > 0 || this._zonesFromCursors.length > 0)) {
let ctx2 = this._overviewRuler.getDomNode().getContext('2d');
ctx2.beginPath();
ctx2.lineWidth = 1;
ctx2.strokeStyle = this._borderColor;
ctx2.moveTo(0, 0);
ctx2.lineTo(0, this._overviewRuler.getPixelHeight());
ctx2.stroke();
ctx2.moveTo(0, 0);
ctx2.lineTo(this._overviewRuler.getPixelWidth(), 0);
ctx2.stroke();
}
}
}

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { IOverviewRuler } from 'vs/editor/browser/editorBrowser';
import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { OverviewRulerPosition } from 'vs/editor/common/config/editorOptions';
import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
export class OverviewRuler extends ViewEventHandler implements IOverviewRuler {
private _context: ViewContext;
private _overviewRuler: OverviewRulerImpl;
constructor(context: ViewContext, cssClassName: string, minimumHeight: number, maximumHeight: number) {
super();
this._context = context;
this._overviewRuler = new OverviewRulerImpl(
0,
cssClassName,
this._context.viewLayout.getScrollHeight(),
this._context.configuration.editor.lineHeight,
this._context.configuration.editor.pixelRatio,
minimumHeight,
maximumHeight,
(lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber)
);
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._overviewRuler.dispose();
super.dispose();
}
// ---- begin view event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, true);
}
if (e.pixelRatio) {
this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, true);
}
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this._overviewRuler.setScrollHeight(e.scrollHeight, true);
return super.onScrollChanged(e) || e.scrollHeightChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// ---- end view event handlers
public getDomNode(): HTMLElement {
return this._overviewRuler.getDomNode();
}
public setLayout(position: OverviewRulerPosition): void {
this._overviewRuler.setLayout(position, true);
}
public setZones(zones: OverviewRulerZone[]): void {
this._overviewRuler.setZones(zones, true);
}
}

View File

@@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { OverviewRulerLane } from 'vs/editor/common/editorCommon';
import { OverviewZoneManager, ColorZone, OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
import { Color } from 'vs/base/common/color';
import { OverviewRulerPosition } from 'vs/editor/common/config/editorOptions';
import { ThemeType, LIGHT } from 'vs/platform/theme/common/themeService';
export class OverviewRulerImpl {
private _canvasLeftOffset: number;
private _domNode: FastDomNode<HTMLCanvasElement>;
private _lanesCount: number;
private _zoneManager: OverviewZoneManager;
private _background: Color;
constructor(
canvasLeftOffset: number, cssClassName: string, scrollHeight: number, lineHeight: number,
pixelRatio: number, minimumHeight: number, maximumHeight: number,
getVerticalOffsetForLine: (lineNumber: number) => number
) {
this._canvasLeftOffset = canvasLeftOffset;
this._domNode = createFastDomNode(document.createElement('canvas'));
this._domNode.setClassName(cssClassName);
this._domNode.setPosition('absolute');
this._domNode.setLayerHinting(true);
this._lanesCount = 3;
this._background = null;
this._zoneManager = new OverviewZoneManager(getVerticalOffsetForLine);
this._zoneManager.setMinimumHeight(minimumHeight);
this._zoneManager.setMaximumHeight(maximumHeight);
this._zoneManager.setThemeType(LIGHT);
this._zoneManager.setDOMWidth(0);
this._zoneManager.setDOMHeight(0);
this._zoneManager.setOuterHeight(scrollHeight);
this._zoneManager.setLineHeight(lineHeight);
this._zoneManager.setPixelRatio(pixelRatio);
}
public dispose(): void {
this._zoneManager = null;
}
public setLayout(position: OverviewRulerPosition, render: boolean): void {
this._domNode.setTop(position.top);
this._domNode.setRight(position.right);
let hasChanged = false;
hasChanged = this._zoneManager.setDOMWidth(position.width) || hasChanged;
hasChanged = this._zoneManager.setDOMHeight(position.height) || hasChanged;
if (hasChanged) {
this._domNode.setWidth(this._zoneManager.getDOMWidth());
this._domNode.setHeight(this._zoneManager.getDOMHeight());
this._domNode.domNode.width = this._zoneManager.getCanvasWidth();
this._domNode.domNode.height = this._zoneManager.getCanvasHeight();
if (render) {
this.render(true);
}
}
}
public getLanesCount(): number {
return this._lanesCount;
}
public setLanesCount(newLanesCount: number, render: boolean): void {
this._lanesCount = newLanesCount;
if (render) {
this.render(true);
}
}
public setThemeType(themeType: ThemeType, render: boolean): void {
this._zoneManager.setThemeType(themeType);
if (render) {
this.render(true);
}
}
public setUseBackground(background: Color, render: boolean): void {
this._background = background;
if (render) {
this.render(true);
}
}
public getDomNode(): HTMLCanvasElement {
return this._domNode.domNode;
}
public getPixelWidth(): number {
return this._zoneManager.getCanvasWidth();
}
public getPixelHeight(): number {
return this._zoneManager.getCanvasHeight();
}
public setScrollHeight(scrollHeight: number, render: boolean): void {
this._zoneManager.setOuterHeight(scrollHeight);
if (render) {
this.render(true);
}
}
public setLineHeight(lineHeight: number, render: boolean): void {
this._zoneManager.setLineHeight(lineHeight);
if (render) {
this.render(true);
}
}
public setPixelRatio(pixelRatio: number, render: boolean): void {
this._zoneManager.setPixelRatio(pixelRatio);
this._domNode.setWidth(this._zoneManager.getDOMWidth());
this._domNode.setHeight(this._zoneManager.getDOMHeight());
this._domNode.domNode.width = this._zoneManager.getCanvasWidth();
this._domNode.domNode.height = this._zoneManager.getCanvasHeight();
if (render) {
this.render(true);
}
}
public setZones(zones: OverviewRulerZone[], render: boolean): void {
this._zoneManager.setZones(zones);
if (render) {
this.render(false);
}
}
public render(forceRender: boolean): boolean {
if (this._zoneManager.getOuterHeight() === 0) {
return false;
}
const width = this._zoneManager.getCanvasWidth();
const height = this._zoneManager.getCanvasHeight();
let colorZones = this._zoneManager.resolveColorZones();
let id2Color = this._zoneManager.getId2Color();
let ctx = this._domNode.domNode.getContext('2d');
if (this._background === null) {
ctx.clearRect(0, 0, width, height);
} else {
ctx.fillStyle = Color.Format.CSS.formatHex(this._background);
ctx.fillRect(0, 0, width, height);
}
if (colorZones.length > 0) {
let remainingWidth = width - this._canvasLeftOffset;
if (this._lanesCount >= 3) {
this._renderThreeLanes(ctx, colorZones, id2Color, remainingWidth);
} else if (this._lanesCount === 2) {
this._renderTwoLanes(ctx, colorZones, id2Color, remainingWidth);
} else if (this._lanesCount === 1) {
this._renderOneLane(ctx, colorZones, id2Color, remainingWidth);
}
}
return true;
}
private _renderOneLane(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left | OverviewRulerLane.Center | OverviewRulerLane.Right, this._canvasLeftOffset, w);
}
private _renderTwoLanes(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
let leftWidth = Math.floor(w / 2);
let rightWidth = w - leftWidth;
let leftOffset = this._canvasLeftOffset;
let rightOffset = this._canvasLeftOffset + leftWidth;
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left | OverviewRulerLane.Center, leftOffset, leftWidth);
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Right, rightOffset, rightWidth);
}
private _renderThreeLanes(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
let leftWidth = Math.floor(w / 3);
let rightWidth = Math.floor(w / 3);
let centerWidth = w - leftWidth - rightWidth;
let leftOffset = this._canvasLeftOffset;
let centerOffset = this._canvasLeftOffset + leftWidth;
let rightOffset = this._canvasLeftOffset + leftWidth + centerWidth;
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left, leftOffset, leftWidth);
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Center, centerOffset, centerWidth);
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Right, rightOffset, rightWidth);
}
private _renderVerticalPatch(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], laneMask: number, xpos: number, width: number): void {
let currentColorId = 0;
let currentFrom = 0;
let currentTo = 0;
for (let i = 0, len = colorZones.length; i < len; i++) {
let zone = colorZones[i];
if (!(zone.position & laneMask)) {
continue;
}
let zoneColorId = zone.colorId;
let zoneFrom = zone.from;
let zoneTo = zone.to;
if (zoneColorId !== currentColorId) {
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
currentColorId = zoneColorId;
ctx.fillStyle = id2Color[currentColorId];
currentFrom = zoneFrom;
currentTo = zoneTo;
} else {
if (currentTo >= zoneFrom) {
currentTo = Math.max(currentTo, zoneTo);
} else {
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
currentFrom = zoneFrom;
currentTo = zoneTo;
}
}
}
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
}
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .view-ruler {
position: absolute;
top: 0;
}

View File

@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./rulers';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorRuler } from 'vs/editor/common/view/editorColorRegistry';
import * as dom from 'vs/base/browser/dom';
export class Rulers extends ViewPart {
public domNode: FastDomNode<HTMLElement>;
private _renderedRulers: FastDomNode<HTMLElement>[];
private _rulers: number[];
private _height: number;
private _typicalHalfwidthCharacterWidth: number;
constructor(context: ViewContext) {
super(context);
this.domNode = createFastDomNode<HTMLElement>(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this.domNode.setClassName('view-rulers');
this._renderedRulers = [];
this._rulers = this._context.configuration.editor.viewInfo.rulers;
this._height = this._context.configuration.editor.layoutInfo.contentHeight;
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
}
public dispose(): void {
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.viewInfo || e.layoutInfo || e.fontInfo) {
this._rulers = this._context.configuration.editor.viewInfo.rulers;
this._height = this._context.configuration.editor.layoutInfo.contentHeight;
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
return true;
}
return false;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollHeightChanged;
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
private _ensureRulersCount(): void {
const currentCount = this._renderedRulers.length;
const desiredCount = this._rulers.length;
if (currentCount === desiredCount) {
// Nothing to do
return;
}
if (currentCount < desiredCount) {
const rulerWidth = dom.computeScreenAwareSize(1);
let addCount = desiredCount - currentCount;
while (addCount > 0) {
let node = createFastDomNode(document.createElement('div'));
node.setClassName('view-ruler');
node.setWidth(rulerWidth);
this.domNode.appendChild(node);
this._renderedRulers.push(node);
addCount--;
}
return;
}
let removeCount = currentCount - desiredCount;
while (removeCount > 0) {
let node = this._renderedRulers.pop();
this.domNode.removeChild(node);
removeCount--;
}
}
public render(ctx: RestrictedRenderingContext): void {
this._ensureRulersCount();
for (let i = 0, len = this._rulers.length; i < len; i++) {
let node = this._renderedRulers[i];
node.setHeight(Math.min(ctx.scrollHeight, 1000000));
node.setLeft(this._rulers[i] * this._typicalHalfwidthCharacterWidth);
}
}
}
registerThemingParticipant((theme, collector) => {
let rulerColor = theme.getColor(editorRuler);
if (rulerColor) {
collector.addRule(`.monaco-editor .view-ruler { background-color: ${rulerColor}; }`);
}
});

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .scroll-decoration {
position: absolute;
top: 0;
left: 0;
height: 6px;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./scrollDecoration';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
export class ScrollDecorationViewPart extends ViewPart {
private _domNode: FastDomNode<HTMLElement>;
private _scrollTop: number;
private _width: number;
private _shouldShow: boolean;
private _useShadows: boolean;
constructor(context: ViewContext) {
super(context);
this._scrollTop = 0;
this._width = 0;
this._updateWidth();
this._shouldShow = false;
this._useShadows = this._context.configuration.editor.viewInfo.scrollbar.useShadows;
this._domNode = createFastDomNode(document.createElement('div'));
this._domNode.setAttribute('role', 'presentation');
this._domNode.setAttribute('aria-hidden', 'true');
}
public dispose(): void {
super.dispose();
}
private _updateShouldShow(): boolean {
let newShouldShow = (this._useShadows && this._scrollTop > 0);
if (this._shouldShow !== newShouldShow) {
this._shouldShow = newShouldShow;
return true;
}
return false;
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
private _updateWidth(): boolean {
const layoutInfo = this._context.configuration.editor.layoutInfo;
let newWidth = layoutInfo.width - layoutInfo.minimapWidth;
if (this._width !== newWidth) {
this._width = newWidth;
return true;
}
return false;
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
let shouldRender = false;
if (e.viewInfo) {
this._useShadows = this._context.configuration.editor.viewInfo.scrollbar.useShadows;
}
if (e.layoutInfo) {
shouldRender = this._updateWidth();
}
return this._updateShouldShow() || shouldRender;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
this._scrollTop = e.scrollTop;
return this._updateShouldShow();
}
// --- end event handlers
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(ctx: RestrictedRenderingContext): void {
this._domNode.setWidth(this._width);
this._domNode.setClassName(this._shouldShow ? 'scroll-decoration' : '');
}
}
registerThemingParticipant((theme, collector) => {
let shadow = theme.getColor(scrollbarShadow);
if (shadow) {
collector.addRule(`.monaco-editor .scroll-decoration { box-shadow: ${shadow} 0 6px 6px -6px inset; }`);
}
});

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cslr = core selections layer rendering (div)
*/
.monaco-editor .lines-content .cslr {
position: absolute;
}
.monaco-editor .top-left-radius { border-top-left-radius: 3px; }
.monaco-editor .bottom-left-radius { border-bottom-left-radius: 3px; }
.monaco-editor .top-right-radius { border-top-right-radius: 3px; }
.monaco-editor .bottom-right-radius { border-bottom-right-radius: 3px; }
.monaco-editor.hc-black .top-left-radius { border-top-left-radius: 0; }
.monaco-editor.hc-black .bottom-left-radius { border-bottom-left-radius: 0; }
.monaco-editor.hc-black .top-right-radius { border-top-right-radius: 0; }
.monaco-editor.hc-black .bottom-right-radius { border-bottom-right-radius: 0; }

View File

@@ -0,0 +1,409 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./selections';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorSelectionBackground, editorInactiveSelection, editorSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { HorizontalRange, LineVisibleRanges, RenderingContext } from 'vs/editor/common/view/renderingContext';
import { Range } from 'vs/editor/common/core/range';
import * as browser from 'vs/base/browser/browser';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
const enum CornerStyle {
EXTERN,
INTERN,
FLAT
}
interface IVisibleRangeEndPointStyle {
top: CornerStyle;
bottom: CornerStyle;
}
class HorizontalRangeWithStyle {
public left: number;
public width: number;
public startStyle: IVisibleRangeEndPointStyle;
public endStyle: IVisibleRangeEndPointStyle;
constructor(other: HorizontalRange) {
this.left = other.left;
this.width = other.width;
this.startStyle = null;
this.endStyle = null;
}
}
class LineVisibleRangesWithStyle {
public lineNumber: number;
public ranges: HorizontalRangeWithStyle[];
constructor(lineNumber: number, ranges: HorizontalRangeWithStyle[]) {
this.lineNumber = lineNumber;
this.ranges = ranges;
}
}
function toStyledRange(item: HorizontalRange): HorizontalRangeWithStyle {
return new HorizontalRangeWithStyle(item);
}
function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle {
return new LineVisibleRangesWithStyle(item.lineNumber, item.ranges.map(toStyledRange));
}
// TODO@Alex: Remove this once IE11 fixes Bug #524217
// The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density.
// Unfortunately, this auto-zooming is buggy around dealing with rounded borders
const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeOrIE;
export class SelectionsOverlay extends DynamicViewOverlay {
private static SELECTION_CLASS_NAME = 'selected-text';
private static SELECTION_TOP_LEFT = 'top-left-radius';
private static SELECTION_BOTTOM_LEFT = 'bottom-left-radius';
private static SELECTION_TOP_RIGHT = 'top-right-radius';
private static SELECTION_BOTTOM_RIGHT = 'bottom-right-radius';
private static EDITOR_BACKGROUND_CLASS_NAME = 'monaco-editor-background';
private static ROUNDED_PIECE_WIDTH = 10;
private _context: ViewContext;
private _lineHeight: number;
private _roundedSelection: boolean;
private _selections: Range[];
private _renderResult: string[];
constructor(context: ViewContext) {
super();
this._context = context;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection;
this._selections = [];
this._renderResult = null;
this._context.addEventHandler(this);
}
public dispose(): void {
this._context.removeEventHandler(this);
this._context = null;
this._selections = null;
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.viewInfo) {
this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection;
}
return true;
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._selections = e.selections.slice(0);
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;//e.inlineDecorationsChanged;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
private _visibleRangesHaveGaps(linesVisibleRanges: LineVisibleRangesWithStyle[]): boolean {
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
let lineVisibleRanges = linesVisibleRanges[i];
if (lineVisibleRanges.ranges.length > 1) {
// There are two ranges on the same line
return true;
}
}
return false;
}
private _enrichVisibleRangesWithStyle(linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[]): void {
let previousFrameTop: HorizontalRangeWithStyle = null;
let previousFrameBottom: HorizontalRangeWithStyle = null;
if (previousFrame && previousFrame.length > 0 && linesVisibleRanges.length > 0) {
let topLineNumber = linesVisibleRanges[0].lineNumber;
for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) {
if (previousFrame[i].lineNumber === topLineNumber) {
previousFrameTop = previousFrame[i].ranges[0];
}
}
let bottomLineNumber = linesVisibleRanges[linesVisibleRanges.length - 1].lineNumber;
for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) {
if (previousFrame[i].lineNumber === bottomLineNumber) {
previousFrameBottom = previousFrame[i].ranges[0];
}
}
if (previousFrameTop && !previousFrameTop.startStyle) {
previousFrameTop = null;
}
if (previousFrameBottom && !previousFrameBottom.startStyle) {
previousFrameBottom = null;
}
}
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
// We know for a fact that there is precisely one range on each line
let curLineRange = linesVisibleRanges[i].ranges[0];
let curLeft = curLineRange.left;
let curRight = curLineRange.left + curLineRange.width;
let startStyle = {
top: CornerStyle.EXTERN,
bottom: CornerStyle.EXTERN
};
let endStyle = {
top: CornerStyle.EXTERN,
bottom: CornerStyle.EXTERN
};
if (i > 0) {
// Look above
let prevLeft = linesVisibleRanges[i - 1].ranges[0].left;
let prevRight = linesVisibleRanges[i - 1].ranges[0].left + linesVisibleRanges[i - 1].ranges[0].width;
if (curLeft === prevLeft) {
startStyle.top = CornerStyle.FLAT;
} else if (curLeft > prevLeft) {
startStyle.top = CornerStyle.INTERN;
}
if (curRight === prevRight) {
endStyle.top = CornerStyle.FLAT;
} else if (prevLeft < curRight && curRight < prevRight) {
endStyle.top = CornerStyle.INTERN;
}
} else if (previousFrameTop) {
// Accept some hick-ups near the viewport edges to save on repaints
startStyle.top = previousFrameTop.startStyle.top;
endStyle.top = previousFrameTop.endStyle.top;
}
if (i + 1 < len) {
// Look below
let nextLeft = linesVisibleRanges[i + 1].ranges[0].left;
let nextRight = linesVisibleRanges[i + 1].ranges[0].left + linesVisibleRanges[i + 1].ranges[0].width;
if (curLeft === nextLeft) {
startStyle.bottom = CornerStyle.FLAT;
} else if (nextLeft < curLeft && curLeft < nextRight) {
startStyle.bottom = CornerStyle.INTERN;
}
if (curRight === nextRight) {
endStyle.bottom = CornerStyle.FLAT;
} else if (curRight < nextRight) {
endStyle.bottom = CornerStyle.INTERN;
}
} else if (previousFrameBottom) {
// Accept some hick-ups near the viewport edges to save on repaints
startStyle.bottom = previousFrameBottom.startStyle.bottom;
endStyle.bottom = previousFrameBottom.endStyle.bottom;
}
curLineRange.startStyle = startStyle;
curLineRange.endStyle = endStyle;
}
}
private _getVisibleRangesWithStyle(selection: Range, ctx: RenderingContext, previousFrame: LineVisibleRangesWithStyle[]): LineVisibleRangesWithStyle[] {
let _linesVisibleRanges = ctx.linesVisibleRangesForRange(selection, true) || [];
let linesVisibleRanges = _linesVisibleRanges.map(toStyled);
let visibleRangesHaveGaps = this._visibleRangesHaveGaps(linesVisibleRanges);
if (!isIEWithZoomingIssuesNearRoundedBorders && !visibleRangesHaveGaps && this._roundedSelection) {
this._enrichVisibleRangesWithStyle(linesVisibleRanges, previousFrame);
}
// The visible ranges are sorted TOP-BOTTOM and LEFT-RIGHT
return linesVisibleRanges;
}
private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string {
return (
'<div class="cslr '
+ className
+ '" style="top:'
+ top.toString()
+ 'px;left:'
+ left.toString()
+ 'px;width:'
+ width.toString()
+ 'px;height:'
+ height
+ 'px;"></div>'
);
}
private _actualRenderOneSelection(output2: string[], visibleStartLineNumber: number, hasMultipleSelections: boolean, visibleRanges: LineVisibleRangesWithStyle[]): void {
let visibleRangesHaveStyle = (visibleRanges.length > 0 && visibleRanges[0].ranges[0].startStyle);
let fullLineHeight = (this._lineHeight).toString();
let reducedLineHeight = (this._lineHeight - 1).toString();
let firstLineNumber = (visibleRanges.length > 0 ? visibleRanges[0].lineNumber : 0);
let lastLineNumber = (visibleRanges.length > 0 ? visibleRanges[visibleRanges.length - 1].lineNumber : 0);
for (let i = 0, len = visibleRanges.length; i < len; i++) {
let lineVisibleRanges = visibleRanges[i];
let lineNumber = lineVisibleRanges.lineNumber;
let lineIndex = lineNumber - visibleStartLineNumber;
let lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight;
let top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0;
let lineOutput = '';
for (let j = 0, lenJ = lineVisibleRanges.ranges.length; j < lenJ; j++) {
let visibleRange = lineVisibleRanges.ranges[j];
if (visibleRangesHaveStyle) {
if (visibleRange.startStyle.top === CornerStyle.INTERN || visibleRange.startStyle.bottom === CornerStyle.INTERN) {
// Reverse rounded corner to the left
// First comes the selection (blue layer)
lineOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
// Second comes the background (white layer) with inverse border radius
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
if (visibleRange.startStyle.top === CornerStyle.INTERN) {
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
}
if (visibleRange.startStyle.bottom === CornerStyle.INTERN) {
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
}
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
}
if (visibleRange.endStyle.top === CornerStyle.INTERN || visibleRange.endStyle.bottom === CornerStyle.INTERN) {
// Reverse rounded corner to the right
// First comes the selection (blue layer)
lineOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
// Second comes the background (white layer) with inverse border radius
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
if (visibleRange.endStyle.top === CornerStyle.INTERN) {
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
}
if (visibleRange.endStyle.bottom === CornerStyle.INTERN) {
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
}
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
}
}
let className = SelectionsOverlay.SELECTION_CLASS_NAME;
if (visibleRangesHaveStyle) {
if (visibleRange.startStyle.top === CornerStyle.EXTERN) {
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
}
if (visibleRange.startStyle.bottom === CornerStyle.EXTERN) {
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
}
if (visibleRange.endStyle.top === CornerStyle.EXTERN) {
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
}
if (visibleRange.endStyle.bottom === CornerStyle.EXTERN) {
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
}
}
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width);
}
output2[lineIndex] += lineOutput;
}
}
private _previousFrameVisibleRangesWithStyle: LineVisibleRangesWithStyle[][] = [];
public prepareRender(ctx: RenderingContext): void {
let output: string[] = [];
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
let lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = '';
}
let thisFrameVisibleRangesWithStyle: LineVisibleRangesWithStyle[][] = [];
for (let i = 0, len = this._selections.length; i < len; i++) {
let selection = this._selections[i];
if (selection.isEmpty()) {
thisFrameVisibleRangesWithStyle[i] = null;
continue;
}
let visibleRangesWithStyle = this._getVisibleRangesWithStyle(selection, ctx, this._previousFrameVisibleRangesWithStyle[i]);
thisFrameVisibleRangesWithStyle[i] = visibleRangesWithStyle;
this._actualRenderOneSelection(output, visibleStartLineNumber, this._selections.length > 1, visibleRangesWithStyle);
}
this._previousFrameVisibleRangesWithStyle = thisFrameVisibleRangesWithStyle;
this._renderResult = output;
}
public render(startLineNumber: number, lineNumber: number): string {
if (!this._renderResult) {
return '';
}
let lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
throw new Error('Unexpected render request');
}
return this._renderResult[lineIndex];
}
}
registerThemingParticipant((theme, collector) => {
let editorSelectionColor = theme.getColor(editorSelectionBackground);
if (editorSelectionColor) {
collector.addRule(`.monaco-editor .focused .selected-text { background-color: ${editorSelectionColor}; }`);
}
let editorInactiveSelectionColor = theme.getColor(editorInactiveSelection);
if (editorInactiveSelectionColor) {
collector.addRule(`.monaco-editor .selected-text { background-color: ${editorInactiveSelectionColor}; }`);
}
let editorSelectionForegroundColor = theme.getColor(editorSelectionForeground);
if (editorSelectionForegroundColor) {
collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`);
}
});

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import * as dom from 'vs/base/browser/dom';
export interface IViewCursorRenderData {
domNode: HTMLElement;
position: Position;
contentLeft: number;
width: number;
height: number;
}
class ViewCursorRenderData {
public readonly top: number;
public readonly left: number;
public readonly width: number;
public readonly textContent: string;
constructor(top: number, left: number, width: number, textContent: string) {
this.top = top;
this.left = left;
this.width = width;
this.textContent = textContent;
}
}
export class ViewCursor {
private readonly _context: ViewContext;
private readonly _isSecondary: boolean;
private readonly _domNode: FastDomNode<HTMLElement>;
private _cursorStyle: TextEditorCursorStyle;
private _lineHeight: number;
private _typicalHalfwidthCharacterWidth: number;
private _isVisible: boolean;
private _position: Position;
private _isInEditableRange: boolean;
private _lastRenderedContent: string;
private _renderData: ViewCursorRenderData;
constructor(context: ViewContext, isSecondary: boolean) {
this._context = context;
this._isSecondary = isSecondary;
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
this._isVisible = true;
// Create the dom node
this._domNode = createFastDomNode(document.createElement('div'));
if (this._isSecondary) {
this._domNode.setClassName('cursor secondary');
} else {
this._domNode.setClassName('cursor');
}
this._domNode.setHeight(this._lineHeight);
this._domNode.setTop(0);
this._domNode.setLeft(0);
Configuration.applyFontInfo(this._domNode, this._context.configuration.editor.fontInfo);
this._domNode.setDisplay('none');
this.updatePosition(new Position(1, 1));
this._isInEditableRange = true;
this._lastRenderedContent = '';
this._renderData = null;
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
public getIsInEditableRange(): boolean {
return this._isInEditableRange;
}
public getPosition(): Position {
return this._position;
}
public show(): void {
if (!this._isVisible) {
this._domNode.setVisibility('inherit');
this._isVisible = true;
}
}
public hide(): void {
if (this._isVisible) {
this._domNode.setVisibility('hidden');
this._isVisible = false;
}
}
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
}
if (e.viewInfo) {
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
}
if (e.fontInfo) {
Configuration.applyFontInfo(this._domNode, this._context.configuration.editor.fontInfo);
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
}
return true;
}
public onCursorPositionChanged(position: Position, isInEditableRange: boolean): boolean {
this.updatePosition(position);
this._isInEditableRange = isInEditableRange;
return true;
}
private _prepareRender(ctx: RenderingContext): ViewCursorRenderData {
if (this._cursorStyle === TextEditorCursorStyle.Line || this._cursorStyle === TextEditorCursorStyle.LineThin) {
const visibleRange = ctx.visibleRangeForPosition(this._position);
if (!visibleRange) {
// Outside viewport
return null;
}
let width: number;
if (this._cursorStyle === TextEditorCursorStyle.Line) {
width = dom.computeScreenAwareSize(2);
} else {
width = dom.computeScreenAwareSize(1);
}
const top = ctx.getVerticalOffsetForLineNumber(this._position.lineNumber) - ctx.bigNumbersDelta;
return new ViewCursorRenderData(top, visibleRange.left, width, '');
}
const visibleRangeForCharacter = ctx.linesVisibleRangesForRange(new Range(this._position.lineNumber, this._position.column, this._position.lineNumber, this._position.column + 1), false);
if (!visibleRangeForCharacter || visibleRangeForCharacter.length === 0 || visibleRangeForCharacter[0].ranges.length === 0) {
// Outside viewport
return null;
}
const range = visibleRangeForCharacter[0].ranges[0];
const width = range.width < 1 ? this._typicalHalfwidthCharacterWidth : range.width;
let textContent = '';
if (this._cursorStyle === TextEditorCursorStyle.Block) {
const lineContent = this._context.model.getLineContent(this._position.lineNumber);
textContent = lineContent.charAt(this._position.column - 1);
}
const top = ctx.getVerticalOffsetForLineNumber(this._position.lineNumber) - ctx.bigNumbersDelta;
return new ViewCursorRenderData(top, range.left, width, textContent);
}
public prepareRender(ctx: RenderingContext): void {
this._renderData = this._prepareRender(ctx);
}
public render(ctx: RestrictedRenderingContext): IViewCursorRenderData {
if (!this._renderData) {
this._domNode.setDisplay('none');
return null;
}
if (this._lastRenderedContent !== this._renderData.textContent) {
this._lastRenderedContent = this._renderData.textContent;
this._domNode.domNode.textContent = this._lastRenderedContent;
}
this._domNode.setDisplay('block');
this._domNode.setTop(this._renderData.top);
this._domNode.setLeft(this._renderData.left);
this._domNode.setWidth(this._renderData.width);
this._domNode.setLineHeight(this._lineHeight);
this._domNode.setHeight(this._lineHeight);
return {
domNode: this._domNode.domNode,
position: this._position,
contentLeft: this._renderData.left,
height: this._lineHeight,
width: 2
};
}
private updatePosition(newPosition: Position): void {
this._position = newPosition;
}
}

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .cursors-layer {
position: absolute;
top: 0;
}
.monaco-editor .cursors-layer > .cursor {
position: absolute;
cursor: text;
}
.monaco-editor .cursors-layer > .cursor.secondary {
opacity: 0.6;
}
/* -- block-outline-style -- */
.monaco-editor .cursors-layer.cursor-block-outline-style > .cursor {
box-sizing: border-box;
background: transparent !important;
border-style: solid;
border-width: 1px;
}
/* -- underline-style -- */
.monaco-editor .cursors-layer.cursor-underline-style > .cursor {
border-bottom-width: 2px;
border-bottom-style: solid;
background: transparent !important;
box-sizing: border-box;
}
/* -- underline-thin-style -- */
.monaco-editor .cursors-layer.cursor-underline-thin-style > .cursor {
border-bottom-width: 1px;
border-bottom-style: solid;
background: transparent !important;
box-sizing: border-box;
}
@keyframes monaco-cursor-smooth {
0%,
20% {
opacity: 1;
}
60%,
100% {
opacity: 0;
}
}
@keyframes monaco-cursor-phase {
0%,
20% {
opacity: 1;
}
90%,
100% {
opacity: 0;
}
}
@keyframes monaco-cursor-expand {
0%,
20% {
transform: scaleY(1);
}
80%,
100% {
transform: scaleY(0);
}
}
.cursor-smooth {
animation: monaco-cursor-smooth 0.5s ease-in-out 0s 20 alternate;
}
.cursor-phase {
animation: monaco-cursor-phase 0.5s ease-in-out 0s 20 alternate;
}
.cursor-expand > .cursor {
animation: monaco-cursor-expand 0.5s ease-in-out 0s 20 alternate;
}

View File

@@ -0,0 +1,368 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./viewCursors';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { Position } from 'vs/editor/common/core/position';
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { TimeoutTimer, IntervalTimer } from 'vs/base/common/async';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorCursorForeground, editorCursorBackground } from 'vs/editor/common/view/editorColorRegistry';
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
export class ViewCursors extends ViewPart {
static BLINK_INTERVAL = 500;
private _readOnly: boolean;
private _cursorBlinking: TextEditorCursorBlinkingStyle;
private _cursorStyle: TextEditorCursorStyle;
private _selectionIsEmpty: boolean;
private _isVisible: boolean;
private _domNode: FastDomNode<HTMLElement>;
private _startCursorBlinkAnimation: TimeoutTimer;
private _cursorFlatBlinkInterval: IntervalTimer;
private _blinkingEnabled: boolean;
private _editorHasFocus: boolean;
private _primaryCursor: ViewCursor;
private _secondaryCursors: ViewCursor[];
private _renderData: IViewCursorRenderData[];
constructor(context: ViewContext) {
super(context);
this._readOnly = this._context.configuration.editor.readOnly;
this._cursorBlinking = this._context.configuration.editor.viewInfo.cursorBlinking;
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
this._selectionIsEmpty = true;
this._primaryCursor = new ViewCursor(this._context, false);
this._secondaryCursors = [];
this._renderData = [];
this._domNode = createFastDomNode(document.createElement('div'));
this._domNode.setAttribute('role', 'presentation');
this._domNode.setAttribute('aria-hidden', 'true');
this._updateDomClassName();
this._domNode.appendChild(this._primaryCursor.getDomNode());
this._startCursorBlinkAnimation = new TimeoutTimer();
this._cursorFlatBlinkInterval = new IntervalTimer();
this._blinkingEnabled = false;
this._editorHasFocus = false;
this._updateBlinking();
}
public dispose(): void {
super.dispose();
this._startCursorBlinkAnimation.dispose();
this._cursorFlatBlinkInterval.dispose();
}
public getDomNode(): FastDomNode<HTMLElement> {
return this._domNode;
}
// --- begin event handlers
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.readOnly) {
this._readOnly = this._context.configuration.editor.readOnly;
}
if (e.viewInfo) {
this._cursorBlinking = this._context.configuration.editor.viewInfo.cursorBlinking;
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
}
this._primaryCursor.onConfigurationChanged(e);
this._updateBlinking();
if (e.viewInfo) {
this._updateDomClassName();
}
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
this._secondaryCursors[i].onConfigurationChanged(e);
}
return true;
}
private _onCursorPositionChanged(position: Position, secondaryPositions: Position[], isInEditableRange: boolean): void {
this._primaryCursor.onCursorPositionChanged(position, isInEditableRange);
this._updateBlinking();
if (this._secondaryCursors.length < secondaryPositions.length) {
// Create new cursors
let addCnt = secondaryPositions.length - this._secondaryCursors.length;
for (let i = 0; i < addCnt; i++) {
let newCursor = new ViewCursor(this._context, true);
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
this._secondaryCursors.push(newCursor);
}
} else if (this._secondaryCursors.length > secondaryPositions.length) {
// Remove some cursors
let removeCnt = this._secondaryCursors.length - secondaryPositions.length;
for (let i = 0; i < removeCnt; i++) {
this._domNode.removeChild(this._secondaryCursors[0].getDomNode());
this._secondaryCursors.splice(0, 1);
}
}
for (let i = 0; i < secondaryPositions.length; i++) {
this._secondaryCursors[i].onCursorPositionChanged(secondaryPositions[i], isInEditableRange);
}
}
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
let positions: Position[] = [];
for (let i = 0, len = e.selections.length; i < len; i++) {
positions[i] = e.selections[i].getPosition();
}
this._onCursorPositionChanged(positions[0], positions.slice(1), e.isInEditableRange);
const selectionIsEmpty = e.selections[0].isEmpty();
if (this._selectionIsEmpty !== selectionIsEmpty) {
this._selectionIsEmpty = selectionIsEmpty;
this._updateDomClassName();
}
return true;
}
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
}
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
return true;
}
public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
this._editorHasFocus = e.isFocused;
this._updateBlinking();
return false;
}
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
return true;
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return true;
}
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
let shouldRender = (position: Position) => {
for (let i = 0, len = e.ranges.length; i < len; i++) {
if (e.ranges[i].fromLineNumber <= position.lineNumber && position.lineNumber <= e.ranges[i].toLineNumber) {
return true;
}
}
return false;
};
if (shouldRender(this._primaryCursor.getPosition())) {
return true;
}
for (let i = 0; i < this._secondaryCursors.length; i++) {
if (shouldRender(this._secondaryCursors[i].getPosition())) {
return true;
}
}
return false;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
// --- end event handlers
public getPosition(): Position {
return this._primaryCursor.getPosition();
}
// ---- blinking logic
private _getCursorBlinking(): TextEditorCursorBlinkingStyle {
if (!this._editorHasFocus) {
return TextEditorCursorBlinkingStyle.Hidden;
}
if (this._readOnly || !this._primaryCursor.getIsInEditableRange()) {
return TextEditorCursorBlinkingStyle.Solid;
}
return this._cursorBlinking;
}
private _updateBlinking(): void {
this._startCursorBlinkAnimation.cancel();
this._cursorFlatBlinkInterval.cancel();
let blinkingStyle = this._getCursorBlinking();
// hidden and solid are special as they involve no animations
let isHidden = (blinkingStyle === TextEditorCursorBlinkingStyle.Hidden);
let isSolid = (blinkingStyle === TextEditorCursorBlinkingStyle.Solid);
if (isHidden) {
this._hide();
} else {
this._show();
}
this._blinkingEnabled = false;
this._updateDomClassName();
if (!isHidden && !isSolid) {
if (blinkingStyle === TextEditorCursorBlinkingStyle.Blink) {
// flat blinking is handled by JavaScript to save battery life due to Chromium step timing issue https://bugs.chromium.org/p/chromium/issues/detail?id=361587
this._cursorFlatBlinkInterval.cancelAndSet(() => {
if (this._isVisible) {
this._hide();
} else {
this._show();
}
}, ViewCursors.BLINK_INTERVAL);
} else {
this._startCursorBlinkAnimation.setIfNotSet(() => {
this._blinkingEnabled = true;
this._updateDomClassName();
}, ViewCursors.BLINK_INTERVAL);
}
}
}
// --- end blinking logic
private _updateDomClassName(): void {
this._domNode.setClassName(this._getClassName());
}
private _getClassName(): string {
let result = 'cursors-layer';
if (!this._selectionIsEmpty) {
result += ' has-selection';
}
switch (this._cursorStyle) {
case TextEditorCursorStyle.Line:
result += ' cursor-line-style';
break;
case TextEditorCursorStyle.Block:
result += ' cursor-block-style';
break;
case TextEditorCursorStyle.Underline:
result += ' cursor-underline-style';
break;
case TextEditorCursorStyle.LineThin:
result += ' cursor-line-thin-style';
break;
case TextEditorCursorStyle.BlockOutline:
result += ' cursor-block-outline-style';
break;
case TextEditorCursorStyle.UnderlineThin:
result += ' cursor-underline-thin-style';
break;
default:
result += ' cursor-line-style';
}
if (this._blinkingEnabled) {
switch (this._getCursorBlinking()) {
case TextEditorCursorBlinkingStyle.Blink:
result += ' cursor-blink';
break;
case TextEditorCursorBlinkingStyle.Smooth:
result += ' cursor-smooth';
break;
case TextEditorCursorBlinkingStyle.Phase:
result += ' cursor-phase';
break;
case TextEditorCursorBlinkingStyle.Expand:
result += ' cursor-expand';
break;
case TextEditorCursorBlinkingStyle.Solid:
result += ' cursor-solid';
break;
default:
result += ' cursor-solid';
}
} else {
result += ' cursor-solid';
}
return result;
}
private _show(): void {
this._primaryCursor.show();
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
this._secondaryCursors[i].show();
}
this._isVisible = true;
}
private _hide(): void {
this._primaryCursor.hide();
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
this._secondaryCursors[i].hide();
}
this._isVisible = false;
}
// ---- IViewPart implementation
public prepareRender(ctx: RenderingContext): void {
this._primaryCursor.prepareRender(ctx);
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
this._secondaryCursors[i].prepareRender(ctx);
}
}
public render(ctx: RestrictedRenderingContext): void {
let renderData: IViewCursorRenderData[] = [], renderDataLen = 0;
const primaryRenderData = this._primaryCursor.render(ctx);
if (primaryRenderData) {
renderData[renderDataLen++] = primaryRenderData;
}
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
const secondaryRenderData = this._secondaryCursors[i].render(ctx);
if (secondaryRenderData) {
renderData[renderDataLen++] = secondaryRenderData;
}
}
this._renderData = renderData;
}
public getLastRenderData(): IViewCursorRenderData[] {
return this._renderData;
}
}
registerThemingParticipant((theme, collector) => {
let caret = theme.getColor(editorCursorForeground);
if (caret) {
let caretBackground = theme.getColor(editorCursorBackground);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (theme.type === 'hc') {
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
}
}
});

View File

@@ -0,0 +1,353 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { onUnexpectedError } from 'vs/base/common/errors';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IViewZone } from 'vs/editor/browser/editorBrowser';
import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { Position } from 'vs/editor/common/core/position';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
export interface IMyViewZone {
whitespaceId: number;
delegate: IViewZone;
isVisible: boolean;
domNode: FastDomNode<HTMLElement>;
marginDomNode: FastDomNode<HTMLElement>;
}
export interface IMyRenderData {
data: IViewWhitespaceViewportData[];
}
interface IComputedViewZoneProps {
afterViewLineNumber: number;
heightInPx: number;
}
export class ViewZones extends ViewPart {
private _zones: { [id: string]: IMyViewZone; };
private _lineHeight: number;
private _contentWidth: number;
private _contentLeft: number;
public domNode: FastDomNode<HTMLElement>;
public marginDomNode: FastDomNode<HTMLElement>;
constructor(context: ViewContext) {
super(context);
this._lineHeight = this._context.configuration.editor.lineHeight;
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName('view-zones');
this.domNode.setPosition('absolute');
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this.marginDomNode = createFastDomNode(document.createElement('div'));
this.marginDomNode.setClassName('margin-view-zones');
this.marginDomNode.setPosition('absolute');
this.marginDomNode.setAttribute('role', 'presentation');
this.marginDomNode.setAttribute('aria-hidden', 'true');
this._zones = {};
}
public dispose(): void {
super.dispose();
this._zones = {};
}
// ---- begin view event handlers
private _recomputeWhitespacesProps(): boolean {
let hadAChange = false;
let keys = Object.keys(this._zones);
for (let i = 0, len = keys.length; i < len; i++) {
let id = keys[i];
let zone = this._zones[id];
let props = this._computeWhitespaceProps(zone.delegate);
if (this._context.viewLayout.changeWhitespace(parseInt(id, 10), props.afterViewLineNumber, props.heightInPx)) {
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
hadAChange = true;
}
}
return hadAChange;
}
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
if (e.lineHeight) {
this._lineHeight = this._context.configuration.editor.lineHeight;
return this._recomputeWhitespacesProps();
}
if (e.layoutInfo) {
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
}
return true;
}
public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean {
return this._recomputeWhitespacesProps();
}
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
return true;
}
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
return e.scrollTopChanged || e.scrollWidthChanged;
}
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
return true;
}
// ---- end view event handlers
private _getZoneOrdinal(zone: IViewZone): number {
if (typeof zone.afterColumn !== 'undefined') {
return zone.afterColumn;
}
return 10000;
}
private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps {
if (zone.afterLineNumber === 0) {
return {
afterViewLineNumber: 0,
heightInPx: this._heightInPixels(zone)
};
}
let zoneAfterModelPosition: Position;
if (typeof zone.afterColumn !== 'undefined') {
zoneAfterModelPosition = this._context.model.validateModelPosition({
lineNumber: zone.afterLineNumber,
column: zone.afterColumn
});
} else {
let validAfterLineNumber = this._context.model.validateModelPosition({
lineNumber: zone.afterLineNumber,
column: 1
}).lineNumber;
zoneAfterModelPosition = new Position(
validAfterLineNumber,
this._context.model.getModelLineMaxColumn(validAfterLineNumber)
);
}
let zoneBeforeModelPosition: Position;
if (zoneAfterModelPosition.column === this._context.model.getModelLineMaxColumn(zoneAfterModelPosition.lineNumber)) {
zoneBeforeModelPosition = this._context.model.validateModelPosition({
lineNumber: zoneAfterModelPosition.lineNumber + 1,
column: 1
});
} else {
zoneBeforeModelPosition = this._context.model.validateModelPosition({
lineNumber: zoneAfterModelPosition.lineNumber,
column: zoneAfterModelPosition.column + 1
});
}
let viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(zoneAfterModelPosition);
let isVisible = this._context.model.coordinatesConverter.modelPositionIsVisible(zoneBeforeModelPosition);
return {
afterViewLineNumber: viewPosition.lineNumber,
heightInPx: (isVisible ? this._heightInPixels(zone) : 0)
};
}
public addZone(zone: IViewZone): number {
let props = this._computeWhitespaceProps(zone);
let whitespaceId = this._context.viewLayout.addWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx);
let myZone: IMyViewZone = {
whitespaceId: whitespaceId,
delegate: zone,
isVisible: false,
domNode: createFastDomNode(zone.domNode),
marginDomNode: zone.marginDomNode ? createFastDomNode(zone.marginDomNode) : null
};
this._safeCallOnComputedHeight(myZone.delegate, props.heightInPx);
myZone.domNode.setPosition('absolute');
myZone.domNode.domNode.style.width = '100%';
myZone.domNode.setDisplay('none');
myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString());
this.domNode.appendChild(myZone.domNode);
if (myZone.marginDomNode) {
myZone.marginDomNode.setPosition('absolute');
myZone.marginDomNode.domNode.style.width = '100%';
myZone.marginDomNode.setDisplay('none');
myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString());
this.marginDomNode.appendChild(myZone.marginDomNode);
}
this._zones[myZone.whitespaceId.toString()] = myZone;
this.setShouldRender();
return myZone.whitespaceId;
}
public removeZone(id: number): boolean {
if (this._zones.hasOwnProperty(id.toString())) {
let zone = this._zones[id.toString()];
delete this._zones[id.toString()];
this._context.viewLayout.removeWhitespace(zone.whitespaceId);
zone.domNode.removeAttribute('monaco-visible-view-zone');
zone.domNode.removeAttribute('monaco-view-zone');
zone.domNode.domNode.parentNode.removeChild(zone.domNode.domNode);
if (zone.marginDomNode) {
zone.marginDomNode.removeAttribute('monaco-visible-view-zone');
zone.marginDomNode.removeAttribute('monaco-view-zone');
zone.marginDomNode.domNode.parentNode.removeChild(zone.marginDomNode.domNode);
}
this.setShouldRender();
return true;
}
return false;
}
public layoutZone(id: number): boolean {
let changed = false;
if (this._zones.hasOwnProperty(id.toString())) {
let zone = this._zones[id.toString()];
let props = this._computeWhitespaceProps(zone.delegate);
// let newOrdinal = this._getZoneOrdinal(zone.delegate);
changed = this._context.viewLayout.changeWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx) || changed;
// TODO@Alex: change `newOrdinal` too
if (changed) {
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
this.setShouldRender();
}
}
return changed;
}
public shouldSuppressMouseDownOnViewZone(id: number): boolean {
if (this._zones.hasOwnProperty(id.toString())) {
let zone = this._zones[id.toString()];
return zone.delegate.suppressMouseDown;
}
return false;
}
private _heightInPixels(zone: IViewZone): number {
if (typeof zone.heightInPx === 'number') {
return zone.heightInPx;
}
if (typeof zone.heightInLines === 'number') {
return this._lineHeight * zone.heightInLines;
}
return this._lineHeight;
}
private _safeCallOnComputedHeight(zone: IViewZone, height: number): void {
if (typeof zone.onComputedHeight === 'function') {
try {
zone.onComputedHeight(height);
} catch (e) {
onUnexpectedError(e);
}
}
}
private _safeCallOnDomNodeTop(zone: IViewZone, top: number): void {
if (typeof zone.onDomNodeTop === 'function') {
try {
zone.onDomNodeTop(top);
} catch (e) {
onUnexpectedError(e);
}
}
}
public prepareRender(ctx: RenderingContext): void {
// Nothing to read
}
public render(ctx: RestrictedRenderingContext): void {
const visibleWhitespaces = ctx.viewportData.whitespaceViewportData;
let visibleZones: { [id: string]: IViewWhitespaceViewportData; } = {};
let hasVisibleZone = false;
for (let i = 0, len = visibleWhitespaces.length; i < len; i++) {
visibleZones[visibleWhitespaces[i].id.toString()] = visibleWhitespaces[i];
hasVisibleZone = true;
}
let keys = Object.keys(this._zones);
for (let i = 0, len = keys.length; i < len; i++) {
let id = keys[i];
let zone = this._zones[id];
let newTop = 0;
let newHeight = 0;
let newDisplay = 'none';
if (visibleZones.hasOwnProperty(id)) {
newTop = visibleZones[id].verticalOffset - ctx.bigNumbersDelta;
newHeight = visibleZones[id].height;
newDisplay = 'block';
// zone is visible
if (!zone.isVisible) {
zone.domNode.setAttribute('monaco-visible-view-zone', 'true');
zone.isVisible = true;
}
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(visibleZones[id].verticalOffset));
} else {
if (zone.isVisible) {
zone.domNode.removeAttribute('monaco-visible-view-zone');
zone.isVisible = false;
}
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(-1000000));
}
zone.domNode.setTop(newTop);
zone.domNode.setHeight(newHeight);
zone.domNode.setDisplay(newDisplay);
if (zone.marginDomNode) {
zone.marginDomNode.setTop(newTop);
zone.marginDomNode.setHeight(newHeight);
zone.marginDomNode.setDisplay(newDisplay);
}
}
if (hasVisibleZone) {
this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));
this.marginDomNode.setWidth(this._contentLeft);
}
}
}

View File

@@ -0,0 +1,535 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/editor';
import 'vs/css!./media/tokens';
import { onUnexpectedError } from 'vs/base/common/errors';
import { TPromise } from 'vs/base/common/winjs.base';
import * as dom from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { CommonCodeEditor } from 'vs/editor/common/commonCodeEditor';
import { CommonEditorConfiguration } from 'vs/editor/common/config/commonEditorConfig';
import { Range, IRange } from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditorAction } from 'vs/editor/common/editorCommonExtensions';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { Configuration } from 'vs/editor/browser/config/configuration';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { View, IOverlayWidgetData, IContentWidgetData } from 'vs/editor/browser/view/viewImpl';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { InternalEditorAction } from 'vs/editor/common/editorAction';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IPosition } from 'vs/editor/common/core/position';
import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer';
import { CoreEditorCommand } from 'vs/editor/common/controller/coreCommands';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder } from 'vs/editor/common/view/editorColorRegistry';
import { Color } from 'vs/base/common/color';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
export abstract class CodeEditorWidget extends CommonCodeEditor implements editorBrowser.ICodeEditor {
private readonly _onMouseUp: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseUp: Event<editorBrowser.IEditorMouseEvent> = this._onMouseUp.event;
private readonly _onMouseDown: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseDown: Event<editorBrowser.IEditorMouseEvent> = this._onMouseDown.event;
private readonly _onMouseDrag: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseDrag: Event<editorBrowser.IEditorMouseEvent> = this._onMouseDrag.event;
private readonly _onMouseDrop: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseDrop: Event<editorBrowser.IEditorMouseEvent> = this._onMouseDrop.event;
private readonly _onContextMenu: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onContextMenu: Event<editorBrowser.IEditorMouseEvent> = this._onContextMenu.event;
private readonly _onMouseMove: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseMove: Event<editorBrowser.IEditorMouseEvent> = this._onMouseMove.event;
private readonly _onMouseLeave: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>());
public readonly onMouseLeave: Event<editorBrowser.IEditorMouseEvent> = this._onMouseLeave.event;
private readonly _onKeyUp: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
private readonly _onKeyDown: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private readonly _onDidScrollChange: Emitter<editorCommon.IScrollEvent> = this._register(new Emitter<editorCommon.IScrollEvent>());
public readonly onDidScrollChange: Event<editorCommon.IScrollEvent> = this._onDidScrollChange.event;
private readonly _onDidChangeViewZones: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeViewZones: Event<void> = this._onDidChangeViewZones.event;
private _codeEditorService: ICodeEditorService;
private _commandService: ICommandService;
private _themeService: IThemeService;
protected domElement: HTMLElement;
private _focusTracker: CodeEditorWidgetFocusTracker;
_configuration: Configuration;
private contentWidgets: { [key: string]: IContentWidgetData; };
private overlayWidgets: { [key: string]: IOverlayWidgetData; };
_view: View;
constructor(
domElement: HTMLElement,
options: IEditorOptions,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService
) {
super(domElement, options, instantiationService, contextKeyService);
this._codeEditorService = codeEditorService;
this._commandService = commandService;
this._themeService = themeService;
this._focusTracker = new CodeEditorWidgetFocusTracker(domElement);
this._focusTracker.onChange(() => {
let hasFocus = this._focusTracker.hasFocus();
if (hasFocus) {
this._onDidFocusEditor.fire();
} else {
this._onDidBlurEditor.fire();
}
});
this.contentWidgets = {};
this.overlayWidgets = {};
let contributions = this._getContributions();
for (let i = 0, len = contributions.length; i < len; i++) {
let ctor = contributions[i];
try {
let contribution = this._instantiationService.createInstance(ctor, this);
this._contributions[contribution.getId()] = contribution;
} catch (err) {
onUnexpectedError(err);
}
}
this._getActions().forEach((action) => {
const internalAction = new InternalEditorAction(
action.id,
action.label,
action.alias,
action.precondition,
(): void | TPromise<void> => {
return this._instantiationService.invokeFunction((accessor) => {
return action.runEditorCommand(accessor, this, null);
});
},
this._contextKeyService
);
this._actions[internalAction.id] = internalAction;
});
this._codeEditorService.addCodeEditor(this);
}
protected abstract _getContributions(): editorBrowser.IEditorContributionCtor[];
protected abstract _getActions(): EditorAction[];
protected _createConfiguration(options: IEditorOptions): CommonEditorConfiguration {
return new Configuration(options, this.domElement);
}
public dispose(): void {
this._codeEditorService.removeCodeEditor(this);
this.contentWidgets = {};
this.overlayWidgets = {};
this._focusTracker.dispose();
super.dispose();
}
public createOverviewRuler(cssClassName: string, minimumHeight: number, maximumHeight: number): editorBrowser.IOverviewRuler {
return this._view.createOverviewRuler(cssClassName, minimumHeight, maximumHeight);
}
public getDomNode(): HTMLElement {
if (!this.hasView) {
return null;
}
return this._view.domNode.domNode;
}
public getCompletelyVisibleLinesRangeInViewport(): Range {
if (!this.hasView) {
return null;
}
const viewRange = this.viewModel.getCompletelyVisibleViewRange();
return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange);
}
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
if (!this.hasView) {
return;
}
this._view.delegateVerticalScrollbarMouseDown(browserEvent);
}
public layout(dimension?: editorCommon.IDimension): void {
this._configuration.observeReferenceElement(dimension);
this.render();
}
public focus(): void {
if (!this.hasView) {
return;
}
this._view.focus();
}
public isFocused(): boolean {
return this.hasView && this._view.isFocused();
}
public hasWidgetFocus(): boolean {
return this._focusTracker && this._focusTracker.hasFocus();
}
public addContentWidget(widget: editorBrowser.IContentWidget): void {
let widgetData: IContentWidgetData = {
widget: widget,
position: widget.getPosition()
};
if (this.contentWidgets.hasOwnProperty(widget.getId())) {
console.warn('Overwriting a content widget with the same id.');
}
this.contentWidgets[widget.getId()] = widgetData;
if (this.hasView) {
this._view.addContentWidget(widgetData);
}
}
public layoutContentWidget(widget: editorBrowser.IContentWidget): void {
let widgetId = widget.getId();
if (this.contentWidgets.hasOwnProperty(widgetId)) {
let widgetData = this.contentWidgets[widgetId];
widgetData.position = widget.getPosition();
if (this.hasView) {
this._view.layoutContentWidget(widgetData);
}
}
}
public removeContentWidget(widget: editorBrowser.IContentWidget): void {
let widgetId = widget.getId();
if (this.contentWidgets.hasOwnProperty(widgetId)) {
let widgetData = this.contentWidgets[widgetId];
delete this.contentWidgets[widgetId];
if (this.hasView) {
this._view.removeContentWidget(widgetData);
}
}
}
public addOverlayWidget(widget: editorBrowser.IOverlayWidget): void {
let widgetData: IOverlayWidgetData = {
widget: widget,
position: widget.getPosition()
};
if (this.overlayWidgets.hasOwnProperty(widget.getId())) {
console.warn('Overwriting an overlay widget with the same id.');
}
this.overlayWidgets[widget.getId()] = widgetData;
if (this.hasView) {
this._view.addOverlayWidget(widgetData);
}
}
public layoutOverlayWidget(widget: editorBrowser.IOverlayWidget): void {
let widgetId = widget.getId();
if (this.overlayWidgets.hasOwnProperty(widgetId)) {
let widgetData = this.overlayWidgets[widgetId];
widgetData.position = widget.getPosition();
if (this.hasView) {
this._view.layoutOverlayWidget(widgetData);
}
}
}
public removeOverlayWidget(widget: editorBrowser.IOverlayWidget): void {
let widgetId = widget.getId();
if (this.overlayWidgets.hasOwnProperty(widgetId)) {
let widgetData = this.overlayWidgets[widgetId];
delete this.overlayWidgets[widgetId];
if (this.hasView) {
this._view.removeOverlayWidget(widgetData);
}
}
}
public changeViewZones(callback: (accessor: editorBrowser.IViewZoneChangeAccessor) => void): void {
if (!this.hasView) {
return;
}
let hasChanges = this._view.change(callback);
if (hasChanges) {
this._onDidChangeViewZones.fire();
}
}
public getWhitespaces(): IEditorWhitespace[] {
if (!this.hasView) {
return [];
}
return this.viewModel.viewLayout.getWhitespaces();
}
private _getVerticalOffsetForPosition(modelLineNumber: number, modelColumn: number): number {
let modelPosition = this.model.validatePosition({
lineNumber: modelLineNumber,
column: modelColumn
});
let viewPosition = this.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
return this.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber);
}
public getTopForLineNumber(lineNumber: number): number {
if (!this.hasView) {
return -1;
}
return this._getVerticalOffsetForPosition(lineNumber, 1);
}
public getTopForPosition(lineNumber: number, column: number): number {
if (!this.hasView) {
return -1;
}
return this._getVerticalOffsetForPosition(lineNumber, column);
}
public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget {
if (!this.hasView) {
return null;
}
return this._view.getTargetAtClientPoint(clientX, clientY);
}
public getScrolledVisiblePosition(rawPosition: IPosition): { top: number; left: number; height: number; } {
if (!this.hasView) {
return null;
}
let position = this.model.validatePosition(rawPosition);
let layoutInfo = this._configuration.editor.layoutInfo;
let top = this._getVerticalOffsetForPosition(position.lineNumber, position.column) - this.getScrollTop();
let left = this._view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.glyphMarginWidth + layoutInfo.lineNumbersWidth + layoutInfo.decorationsWidth - this.getScrollLeft();
return {
top: top,
left: left,
height: this._configuration.editor.lineHeight
};
}
public getOffsetForColumn(lineNumber: number, column: number): number {
if (!this.hasView) {
return -1;
}
return this._view.getOffsetForColumn(lineNumber, column);
}
public render(): void {
if (!this.hasView) {
return;
}
this._view.render(true, false);
}
public setHiddenAreas(ranges: IRange[]): void {
if (this.viewModel) {
this.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)));
}
}
public setAriaActiveDescendant(id: string): void {
if (!this.hasView) {
return;
}
this._view.setAriaActiveDescendant(id);
}
public applyFontInfo(target: HTMLElement): void {
Configuration.applyFontInfoSlow(target, this._configuration.editor.fontInfo);
}
_attachModel(model: editorCommon.IModel): void {
this._view = null;
super._attachModel(model);
if (this._view) {
this.domElement.appendChild(this._view.domNode.domNode);
let keys = Object.keys(this.contentWidgets);
for (let i = 0, len = keys.length; i < len; i++) {
let widgetId = keys[i];
this._view.addContentWidget(this.contentWidgets[widgetId]);
}
keys = Object.keys(this.overlayWidgets);
for (let i = 0, len = keys.length; i < len; i++) {
let widgetId = keys[i];
this._view.addOverlayWidget(this.overlayWidgets[widgetId]);
}
this._view.render(false, true);
this.hasView = true;
}
}
protected _scheduleAtNextAnimationFrame(callback: () => void): IDisposable {
return dom.scheduleAtNextAnimationFrame(callback);
}
protected _createView(): void {
this._view = new View(
this._commandService,
this._configuration,
this._themeService,
this.viewModel,
this.cursor,
(editorCommand: CoreEditorCommand, args: any) => {
if (!this.cursor) {
return;
}
editorCommand.runCoreEditorCommand(this.cursor, args);
}
);
const viewEventBus = this._view.getInternalEventBus();
viewEventBus.onDidGainFocus = () => {
this._onDidFocusEditorText.fire();
// In IE, the focus is not synchronous, so we give it a little help
this._onDidFocusEditor.fire();
};
viewEventBus.onDidScroll = (e) => this._onDidScrollChange.fire(e);
viewEventBus.onDidLoseFocus = () => this._onDidBlurEditorText.fire();
viewEventBus.onContextMenu = (e) => this._onContextMenu.fire(e);
viewEventBus.onMouseDown = (e) => this._onMouseDown.fire(e);
viewEventBus.onMouseUp = (e) => this._onMouseUp.fire(e);
viewEventBus.onMouseDrag = (e) => this._onMouseDrag.fire(e);
viewEventBus.onMouseDrop = (e) => this._onMouseDrop.fire(e);
viewEventBus.onKeyUp = (e) => this._onKeyUp.fire(e);
viewEventBus.onMouseMove = (e) => this._onMouseMove.fire(e);
viewEventBus.onMouseLeave = (e) => this._onMouseLeave.fire(e);
viewEventBus.onKeyDown = (e) => this._onKeyDown.fire(e);
}
protected _detachModel(): editorCommon.IModel {
let removeDomNode: HTMLElement = null;
if (this._view) {
this._view.dispose();
removeDomNode = this._view.domNode.domNode;
this._view = null;
}
let result = super._detachModel();
if (removeDomNode) {
this.domElement.removeChild(removeDomNode);
}
return result;
}
// BEGIN decorations
protected _registerDecorationType(key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void {
this._codeEditorService.registerDecorationType(key, options, parentTypeKey);
}
protected _removeDecorationType(key: string): void {
this._codeEditorService.removeDecorationType(key);
}
protected _resolveDecorationOptions(typeKey: string, writable: boolean): editorCommon.IModelDecorationOptions {
return this._codeEditorService.resolveDecorationOptions(typeKey, writable);
}
// END decorations
}
class CodeEditorWidgetFocusTracker extends Disposable {
private _hasFocus: boolean;
private _domFocusTracker: dom.IFocusTracker;
private _onChange: Emitter<void> = this._register(new Emitter<void>());
public onChange: Event<void> = this._onChange.event;
constructor(domElement: HTMLElement) {
super();
this._hasFocus = false;
this._domFocusTracker = this._register(dom.trackFocus(domElement));
this._domFocusTracker.addFocusListener(() => {
this._hasFocus = true;
this._onChange.fire(void 0);
});
this._domFocusTracker.addBlurListener(() => {
this._hasFocus = false;
this._onChange.fire(void 0);
});
}
public hasFocus(): boolean {
return this._hasFocus;
}
}
const squigglyStart = encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' height='3' width='6'><g fill='`);
const squigglyEnd = encodeURIComponent(`'><polygon points='5.5,0 2.5,3 1.1,3 4.1,0'/><polygon points='4,0 6,2 6,0.6 5.4,0'/><polygon points='0,2 1,3 2.4,3 0,0.6'/></g></svg>`);
function getSquigglySVGData(color: Color) {
return squigglyStart + encodeURIComponent(color.toString()) + squigglyEnd;
}
registerThemingParticipant((theme, collector) => {
let errorBorderColor = theme.getColor(editorErrorBorder);
if (errorBorderColor) {
collector.addRule(`.monaco-editor .redsquiggly { border-bottom: 4px double ${errorBorderColor}; }`);
}
let errorForeground = theme.getColor(editorErrorForeground);
if (errorForeground) {
collector.addRule(`.monaco-editor .redsquiggly { background: url("data:image/svg+xml,${getSquigglySVGData(errorForeground)}") repeat-x bottom left; }`);
}
let warningBorderColor = theme.getColor(editorWarningBorder);
if (warningBorderColor) {
collector.addRule(`.monaco-editor .greensquiggly { border-bottom: 4px double ${warningBorderColor}; }`);
}
let warningForeground = theme.getColor(editorWarningForeground);
if (warningForeground) {
collector.addRule(`.monaco-editor .greensquiggly { background: url("data:image/svg+xml;utf8,${getSquigglySVGData(warningForeground)}") repeat-x bottom left; }`);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'vs/base/common/assert';
import { EventEmitter } from 'vs/base/common/eventEmitter';
import * as objects from 'vs/base/common/objects';
import { Range } from 'vs/editor/common/core/range';
import { ICommonDiffEditor, ILineChange, ScrollType } from 'vs/editor/common/editorCommon';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
interface IDiffRange {
rhs: boolean;
range: Range;
}
export interface Options {
followsCaret?: boolean;
ignoreCharChanges?: boolean;
alwaysRevealFirst?: boolean;
}
var defaultOptions: Options = {
followsCaret: true,
ignoreCharChanges: true,
alwaysRevealFirst: true
};
/**
* Create a new diff navigator for the provided diff editor.
*/
export class DiffNavigator extends EventEmitter {
public static Events = {
UPDATED: 'navigation.updated'
};
private editor: ICommonDiffEditor;
private options: Options;
private disposed: boolean;
private toUnbind: IDisposable[];
private nextIdx: number;
private ranges: IDiffRange[];
private ignoreSelectionChange: boolean;
public revealFirst: boolean;
constructor(editor: ICommonDiffEditor, options: Options = {}) {
super([
DiffNavigator.Events.UPDATED
]);
this.editor = editor;
this.options = objects.mixin(options, defaultOptions, false);
this.disposed = false;
this.toUnbind = [];
this.nextIdx = -1;
this.ranges = [];
this.ignoreSelectionChange = false;
this.revealFirst = this.options.alwaysRevealFirst;
// hook up to diff editor for diff, disposal, and caret move
this.toUnbind.push(this.editor.onDidDispose(() => this.dispose()));
this.toUnbind.push(this.editor.onDidUpdateDiff(() => this.onDiffUpdated()));
if (this.options.followsCaret) {
this.toUnbind.push(this.editor.getModifiedEditor().onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
if (this.ignoreSelectionChange) {
return;
}
this.nextIdx = -1;
}));
}
if (this.options.alwaysRevealFirst) {
this.toUnbind.push(this.editor.getModifiedEditor().onDidChangeModel((e) => {
this.revealFirst = true;
}));
}
// init things
this.init();
}
private init(): void {
var changes = this.editor.getLineChanges();
if (!changes) {
return;
}
}
private onDiffUpdated(): void {
this.init();
this.compute(this.editor.getLineChanges());
if (this.revealFirst) {
// Only reveal first on first non-null changes
if (this.editor.getLineChanges() !== null) {
this.revealFirst = false;
this.nextIdx = -1;
this.next();
}
}
}
private compute(lineChanges: ILineChange[]): void {
// new ranges
this.ranges = [];
if (lineChanges) {
// create ranges from changes
lineChanges.forEach((lineChange) => {
if (!this.options.ignoreCharChanges && lineChange.charChanges) {
lineChange.charChanges.forEach((charChange) => {
this.ranges.push({
rhs: true,
range: new Range(
charChange.modifiedStartLineNumber,
charChange.modifiedStartColumn,
charChange.modifiedEndLineNumber,
charChange.modifiedEndColumn)
});
});
} else {
this.ranges.push({
rhs: true,
range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedStartLineNumber, 1)
});
}
});
}
// sort
this.ranges.sort((left, right) => {
if (left.range.getStartPosition().isBeforeOrEqual(right.range.getStartPosition())) {
return -1;
} else if (right.range.getStartPosition().isBeforeOrEqual(left.range.getStartPosition())) {
return 1;
} else {
return 0;
}
});
this.emit(DiffNavigator.Events.UPDATED, {});
}
private initIdx(fwd: boolean): void {
var found = false;
var position = this.editor.getPosition();
for (var i = 0, len = this.ranges.length; i < len && !found; i++) {
var range = this.ranges[i].range;
if (position.isBeforeOrEqual(range.getStartPosition())) {
this.nextIdx = i + (fwd ? 0 : -1);
found = true;
}
}
if (!found) {
// after the last change
this.nextIdx = fwd ? 0 : this.ranges.length - 1;
}
if (this.nextIdx < 0) {
this.nextIdx = this.ranges.length - 1;
}
}
private move(fwd: boolean): void {
assert.ok(!this.disposed, 'Illegal State - diff navigator has been disposed');
if (!this.canNavigate()) {
return;
}
if (this.nextIdx === -1) {
this.initIdx(fwd);
} else if (fwd) {
this.nextIdx += 1;
if (this.nextIdx >= this.ranges.length) {
this.nextIdx = 0;
}
} else {
this.nextIdx -= 1;
if (this.nextIdx < 0) {
this.nextIdx = this.ranges.length - 1;
}
}
var info = this.ranges[this.nextIdx];
this.ignoreSelectionChange = true;
try {
var pos = info.range.getStartPosition();
this.editor.setPosition(pos);
this.editor.revealPositionInCenter(pos, ScrollType.Smooth);
} finally {
this.ignoreSelectionChange = false;
}
}
public canNavigate(): boolean {
return this.ranges && this.ranges.length > 0;
}
public next(): void {
this.move(true);
}
public previous(): void {
this.move(false);
}
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
this.ranges = null;
this.disposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,822 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/diffReview';
import * as nls from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { renderViewLine2 as renderViewLine, RenderLineInput } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { ViewLineToken } from 'vs/editor/common/core/viewLineToken';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { Position } from 'vs/editor/common/core/position';
import { ColorId, MetadataConsts, FontStyle } from 'vs/editor/common/modes';
import * as editorOptions from 'vs/editor/common/config/editorOptions';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { editorAction, EditorAction, ServicesAccessor } from 'vs/editor/common/editorCommonExtensions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
const DIFF_LINES_PADDING = 3;
const enum DiffEntryType {
Equal = 0,
Insert = 1,
Delete = 2
}
class DiffEntry {
readonly originalLineStart: number;
readonly originalLineEnd: number;
readonly modifiedLineStart: number;
readonly modifiedLineEnd: number;
constructor(originalLineStart: number, originalLineEnd: number, modifiedLineStart: number, modifiedLineEnd: number) {
this.originalLineStart = originalLineStart;
this.originalLineEnd = originalLineEnd;
this.modifiedLineStart = modifiedLineStart;
this.modifiedLineEnd = modifiedLineEnd;
}
public getType(): DiffEntryType {
if (this.originalLineStart === 0) {
return DiffEntryType.Insert;
}
if (this.modifiedLineStart === 0) {
return DiffEntryType.Delete;
}
return DiffEntryType.Equal;
}
}
class Diff {
readonly entries: DiffEntry[];
constructor(entries: DiffEntry[]) {
this.entries = entries;
}
}
export class DiffReview extends Disposable {
private readonly _diffEditor: DiffEditorWidget;
private _isVisible: boolean;
public readonly shadow: FastDomNode<HTMLElement>;
private readonly _actionBar: ActionBar;
public readonly actionBarContainer: FastDomNode<HTMLElement>;
public readonly domNode: FastDomNode<HTMLElement>;
private readonly _content: FastDomNode<HTMLElement>;
private readonly scrollbar: DomScrollableElement;
private _diffs: Diff[];
private _currentDiff: Diff;
constructor(diffEditor: DiffEditorWidget) {
super();
this._diffEditor = diffEditor;
this._isVisible = false;
this.shadow = createFastDomNode(document.createElement('div'));
this.shadow.setClassName('diff-review-shadow');
this.actionBarContainer = createFastDomNode(document.createElement('div'));
this.actionBarContainer.setClassName('diff-review-actions');
this._actionBar = this._register(new ActionBar(
this.actionBarContainer.domNode
));
this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review', true, () => {
this.hide();
return null;
}), { label: false, icon: true });
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName('diff-review monaco-editor-background');
this._content = createFastDomNode(document.createElement('div'));
this._content.setClassName('diff-review-content');
this.scrollbar = this._register(new DomScrollableElement(this._content.domNode, {}));
this.domNode.domNode.appendChild(this.scrollbar.getDomNode());
this._register(diffEditor.onDidUpdateDiff(() => {
if (!this._isVisible) {
return;
}
this._diffs = this._compute();
this._render();
}));
this._register(diffEditor.getModifiedEditor().onDidChangeCursorPosition(() => {
if (!this._isVisible) {
return;
}
this._render();
}));
this._register(diffEditor.getOriginalEditor().onDidFocusEditor(() => {
if (this._isVisible) {
this.hide();
}
}));
this._register(diffEditor.getModifiedEditor().onDidFocusEditor(() => {
if (this._isVisible) {
this.hide();
}
}));
this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => {
e.preventDefault();
let row = dom.findParentWithClass(e.target, 'diff-review-row');
if (row) {
this._goToRow(row);
}
}));
this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'keydown', (e) => {
if (
e.equals(KeyCode.DownArrow)
|| e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow)
|| e.equals(KeyMod.Alt | KeyCode.DownArrow)
) {
e.preventDefault();
this._goToRow(this._getNextRow());
}
if (
e.equals(KeyCode.UpArrow)
|| e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow)
|| e.equals(KeyMod.Alt | KeyCode.UpArrow)
) {
e.preventDefault();
this._goToRow(this._getPrevRow());
}
if (
e.equals(KeyCode.Escape)
|| e.equals(KeyMod.CtrlCmd | KeyCode.Escape)
|| e.equals(KeyMod.Alt | KeyCode.Escape)
|| e.equals(KeyMod.Shift | KeyCode.Escape)
) {
e.preventDefault();
this.hide();
}
if (
e.equals(KeyCode.Space)
|| e.equals(KeyCode.Enter)
) {
e.preventDefault();
this.accept();
}
}));
this._diffs = [];
this._currentDiff = null;
}
public prev(): void {
let index = 0;
if (!this._isVisible) {
this._diffs = this._compute();
}
if (this._isVisible) {
let currentIndex = -1;
for (let i = 0, len = this._diffs.length; i < len; i++) {
if (this._diffs[i] === this._currentDiff) {
currentIndex = i;
break;
}
}
index = (this._diffs.length + currentIndex - 1);
} else {
index = this._findDiffIndex(this._diffEditor.getPosition());
}
if (this._diffs.length === 0) {
// Nothing to do
return;
}
index = index % this._diffs.length;
this._diffEditor.setPosition(new Position(this._diffs[index].entries[0].modifiedLineStart, 1));
this._isVisible = true;
this._diffEditor.doLayout();
this._render();
this._goToRow(this._getNextRow());
}
public next(): void {
let index = 0;
if (!this._isVisible) {
this._diffs = this._compute();
}
if (this._isVisible) {
let currentIndex = -1;
for (let i = 0, len = this._diffs.length; i < len; i++) {
if (this._diffs[i] === this._currentDiff) {
currentIndex = i;
break;
}
}
index = (currentIndex + 1);
} else {
index = this._findDiffIndex(this._diffEditor.getPosition());
}
if (this._diffs.length === 0) {
// Nothing to do
return;
}
index = index % this._diffs.length;
this._diffEditor.setPosition(new Position(this._diffs[index].entries[0].modifiedLineStart, 1));
this._isVisible = true;
this._diffEditor.doLayout();
this._render();
this._goToRow(this._getNextRow());
}
private accept(): void {
let jumpToLineNumber = -1;
let current = this._getCurrentFocusedRow();
if (current) {
let lineNumber = parseInt(current.getAttribute('data-line'), 10);
if (!isNaN(lineNumber)) {
jumpToLineNumber = lineNumber;
}
}
this.hide();
if (jumpToLineNumber !== -1) {
this._diffEditor.setPosition(new Position(jumpToLineNumber, 1));
this._diffEditor.revealPosition(new Position(jumpToLineNumber, 1), editorCommon.ScrollType.Immediate);
}
}
private hide(): void {
this._isVisible = false;
this._diffEditor.focus();
this._diffEditor.doLayout();
this._render();
}
private _getPrevRow(): HTMLElement {
let current = this._getCurrentFocusedRow();
if (!current) {
return this._getFirstRow();
}
if (current.previousElementSibling) {
return <HTMLElement>current.previousElementSibling;
}
return current;
}
private _getNextRow(): HTMLElement {
let current = this._getCurrentFocusedRow();
if (!current) {
return this._getFirstRow();
}
if (current.nextElementSibling) {
return <HTMLElement>current.nextElementSibling;
}
return current;
}
private _getFirstRow(): HTMLElement {
return <HTMLElement>this.domNode.domNode.querySelector('.diff-review-row');
}
private _getCurrentFocusedRow(): HTMLElement {
let result = <HTMLElement>document.activeElement;
if (result && /diff-review-row/.test(result.className)) {
return result;
}
return null;
}
private _goToRow(row: HTMLElement): void {
let prev = this._getCurrentFocusedRow();
row.tabIndex = 0;
row.focus();
if (prev && prev !== row) {
prev.tabIndex = -1;
}
this.scrollbar.scanDomNode();
}
public isVisible(): boolean {
return this._isVisible;
}
private _width: number = 0;
public layout(top: number, width: number, height: number): void {
this._width = width;
this.shadow.setTop(top - 6);
this.shadow.setWidth(width);
this.shadow.setHeight(this._isVisible ? 6 : 0);
this.domNode.setTop(top);
this.domNode.setWidth(width);
this.domNode.setHeight(height);
this._content.setHeight(height);
this._content.setWidth(width);
if (this._isVisible) {
this.actionBarContainer.setAttribute('aria-hidden', 'false');
this.actionBarContainer.setDisplay('block');
} else {
this.actionBarContainer.setAttribute('aria-hidden', 'true');
this.actionBarContainer.setDisplay('none');
}
}
private _compute(): Diff[] {
const lineChanges = this._diffEditor.getLineChanges();
if (!lineChanges || lineChanges.length === 0) {
return [];
}
const originalModel = this._diffEditor.getOriginalEditor().getModel();
const modifiedModel = this._diffEditor.getModifiedEditor().getModel();
if (!originalModel || !modifiedModel) {
return [];
}
return DiffReview._mergeAdjacent(lineChanges, originalModel.getLineCount(), modifiedModel.getLineCount());
}
private static _mergeAdjacent(lineChanges: editorCommon.ILineChange[], originalLineCount: number, modifiedLineCount: number): Diff[] {
if (!lineChanges || lineChanges.length === 0) {
return [];
}
let diffs: Diff[] = [], diffsLength = 0;
for (let i = 0, len = lineChanges.length; i < len; i++) {
const lineChange = lineChanges[i];
const originalStart = lineChange.originalStartLineNumber;
const originalEnd = lineChange.originalEndLineNumber;
const modifiedStart = lineChange.modifiedStartLineNumber;
const modifiedEnd = lineChange.modifiedEndLineNumber;
let r: DiffEntry[] = [], rLength = 0;
// Emit before anchors
{
const originalEqualAbove = (originalEnd === 0 ? originalStart : originalStart - 1);
const modifiedEqualAbove = (modifiedEnd === 0 ? modifiedStart : modifiedStart - 1);
// Make sure we don't step into the previous diff
let minOriginal = 1;
let minModified = 1;
if (i > 0) {
const prevLineChange = lineChanges[i - 1];
if (prevLineChange.originalEndLineNumber === 0) {
minOriginal = prevLineChange.originalStartLineNumber + 1;
} else {
minOriginal = prevLineChange.originalEndLineNumber + 1;
}
if (prevLineChange.modifiedEndLineNumber === 0) {
minModified = prevLineChange.modifiedStartLineNumber + 1;
} else {
minModified = prevLineChange.modifiedEndLineNumber + 1;
}
}
let fromOriginal = originalEqualAbove - DIFF_LINES_PADDING + 1;
let fromModified = modifiedEqualAbove - DIFF_LINES_PADDING + 1;
if (fromOriginal < minOriginal) {
const delta = minOriginal - fromOriginal;
fromOriginal = fromOriginal + delta;
fromModified = fromModified + delta;
}
if (fromModified < minModified) {
const delta = minModified - fromModified;
fromOriginal = fromOriginal + delta;
fromModified = fromModified + delta;
}
r[rLength++] = new DiffEntry(
fromOriginal, originalEqualAbove,
fromModified, modifiedEqualAbove
);
}
// Emit deleted lines
{
if (originalEnd !== 0) {
r[rLength++] = new DiffEntry(originalStart, originalEnd, 0, 0);
}
}
// Emit inserted lines
{
if (modifiedEnd !== 0) {
r[rLength++] = new DiffEntry(0, 0, modifiedStart, modifiedEnd);
}
}
// Emit after anchors
{
const originalEqualBelow = (originalEnd === 0 ? originalStart + 1 : originalEnd + 1);
const modifiedEqualBelow = (modifiedEnd === 0 ? modifiedStart + 1 : modifiedEnd + 1);
// Make sure we don't step into the next diff
let maxOriginal = originalLineCount;
let maxModified = modifiedLineCount;
if (i + 1 < len) {
const nextLineChange = lineChanges[i + 1];
if (nextLineChange.originalEndLineNumber === 0) {
maxOriginal = nextLineChange.originalStartLineNumber;
} else {
maxOriginal = nextLineChange.originalStartLineNumber - 1;
}
if (nextLineChange.modifiedEndLineNumber === 0) {
maxModified = nextLineChange.modifiedStartLineNumber;
} else {
maxModified = nextLineChange.modifiedStartLineNumber - 1;
}
}
let toOriginal = originalEqualBelow + DIFF_LINES_PADDING - 1;
let toModified = modifiedEqualBelow + DIFF_LINES_PADDING - 1;
if (toOriginal > maxOriginal) {
const delta = maxOriginal - toOriginal;
toOriginal = toOriginal + delta;
toModified = toModified + delta;
}
if (toModified > maxModified) {
const delta = maxModified - toModified;
toOriginal = toOriginal + delta;
toModified = toModified + delta;
}
r[rLength++] = new DiffEntry(
originalEqualBelow, toOriginal,
modifiedEqualBelow, toModified,
);
}
diffs[diffsLength++] = new Diff(r);
}
// Merge adjacent diffs
let curr: DiffEntry[] = diffs[0].entries;
let r: Diff[] = [], rLength = 0;
for (let i = 1, len = diffs.length; i < len; i++) {
const thisDiff = diffs[i].entries;
const currLast = curr[curr.length - 1];
const thisFirst = thisDiff[0];
if (
currLast.getType() === DiffEntryType.Equal
&& thisFirst.getType() === DiffEntryType.Equal
&& thisFirst.originalLineStart <= currLast.originalLineEnd
) {
// We are dealing with equal lines that overlap
curr[curr.length - 1] = new DiffEntry(
currLast.originalLineStart, thisFirst.originalLineEnd,
currLast.modifiedLineStart, thisFirst.modifiedLineEnd
);
curr = curr.concat(thisDiff.slice(1));
continue;
}
r[rLength++] = new Diff(curr);
curr = thisDiff;
}
r[rLength++] = new Diff(curr);
return r;
}
private _findDiffIndex(pos: Position): number {
const lineNumber = pos.lineNumber;
for (let i = 0, len = this._diffs.length; i < len; i++) {
const diff = this._diffs[i].entries;
const lastModifiedLine = diff[diff.length - 1].modifiedLineEnd;
if (lineNumber <= lastModifiedLine) {
return i;
}
}
return 0;
}
private _render(): void {
const originalOpts = this._diffEditor.getOriginalEditor().getConfiguration();
const modifiedOpts = this._diffEditor.getModifiedEditor().getConfiguration();
const originalModel = this._diffEditor.getOriginalEditor().getModel();
const modifiedModel = this._diffEditor.getModifiedEditor().getModel();
const originalModelOpts = originalModel.getOptions();
const modifiedModelOpts = modifiedModel.getOptions();
if (!this._isVisible || !originalModel || !modifiedModel) {
dom.clearNode(this._content.domNode);
this._currentDiff = null;
this.scrollbar.scanDomNode();
return;
}
const pos = this._diffEditor.getPosition();
const diffIndex = this._findDiffIndex(pos);
if (this._diffs[diffIndex] === this._currentDiff) {
return;
}
this._currentDiff = this._diffs[diffIndex];
const diffs = this._diffs[diffIndex].entries;
let container = document.createElement('div');
container.className = 'diff-review-table';
container.setAttribute('role', 'list');
Configuration.applyFontInfoSlow(container, modifiedOpts.fontInfo);
let minOriginalLine = 0;
let maxOriginalLine = 0;
let minModifiedLine = 0;
let maxModifiedLine = 0;
for (let i = 0, len = diffs.length; i < len; i++) {
const diffEntry = diffs[i];
const originalLineStart = diffEntry.originalLineStart;
const originalLineEnd = diffEntry.originalLineEnd;
const modifiedLineStart = diffEntry.modifiedLineStart;
const modifiedLineEnd = diffEntry.modifiedLineEnd;
if (originalLineStart !== 0 && ((minOriginalLine === 0 || originalLineStart < minOriginalLine))) {
minOriginalLine = originalLineStart;
}
if (originalLineEnd !== 0 && ((maxOriginalLine === 0 || originalLineEnd > maxOriginalLine))) {
maxOriginalLine = originalLineEnd;
}
if (modifiedLineStart !== 0 && ((minModifiedLine === 0 || modifiedLineStart < minModifiedLine))) {
minModifiedLine = modifiedLineStart;
}
if (modifiedLineEnd !== 0 && ((maxModifiedLine === 0 || modifiedLineEnd > maxModifiedLine))) {
maxModifiedLine = modifiedLineEnd;
}
}
let header = document.createElement('div');
header.className = 'diff-review-row';
let cell = document.createElement('div');
cell.className = 'diff-review-cell diff-review-summary';
cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${maxOriginalLine - minOriginalLine + 1} +${minModifiedLine},${maxModifiedLine - minModifiedLine + 1} @@`));
header.setAttribute('data-line', String(minModifiedLine));
header.setAttribute('aria-label', nls.localize('header', "Difference {0} of {1}: original {2}, {3} lines, modified {4}, {5} lines", (diffIndex + 1), this._diffs.length, minOriginalLine, maxOriginalLine - minOriginalLine + 1, minModifiedLine, maxModifiedLine - minModifiedLine + 1));
header.appendChild(cell);
// @@ -504,7 +517,7 @@
header.setAttribute('role', 'listitem');
container.appendChild(header);
let modLine = minModifiedLine;
for (let i = 0, len = diffs.length; i < len; i++) {
const diffEntry = diffs[i];
DiffReview._renderSection(container, diffEntry, modLine, this._width, originalOpts, originalModel, originalModelOpts, modifiedOpts, modifiedModel, modifiedModelOpts);
if (diffEntry.modifiedLineStart !== 0) {
modLine = diffEntry.modifiedLineEnd;
}
}
dom.clearNode(this._content.domNode);
this._content.domNode.appendChild(container);
this.scrollbar.scanDomNode();
}
private static _renderSection(
dest: HTMLElement, diffEntry: DiffEntry, modLine: number, width: number,
originalOpts: editorOptions.InternalEditorOptions, originalModel: editorCommon.IModel, originalModelOpts: editorCommon.TextModelResolvedOptions,
modifiedOpts: editorOptions.InternalEditorOptions, modifiedModel: editorCommon.IModel, modifiedModelOpts: editorCommon.TextModelResolvedOptions
): void {
const type = diffEntry.getType();
let rowClassName: string = 'diff-review-row';
let lineNumbersExtraClassName: string = '';
let spacerClassName: string = 'diff-review-spacer';
switch (type) {
case DiffEntryType.Insert:
rowClassName = 'diff-review-row line-insert';
lineNumbersExtraClassName = ' char-insert';
spacerClassName = 'diff-review-spacer insert-sign';
break;
case DiffEntryType.Delete:
rowClassName = 'diff-review-row line-delete';
lineNumbersExtraClassName = ' char-delete';
spacerClassName = 'diff-review-spacer delete-sign';
break;
}
const originalLineStart = diffEntry.originalLineStart;
const originalLineEnd = diffEntry.originalLineEnd;
const modifiedLineStart = diffEntry.modifiedLineStart;
const modifiedLineEnd = diffEntry.modifiedLineEnd;
const cnt = Math.max(
modifiedLineEnd - modifiedLineStart,
originalLineEnd - originalLineStart
);
const originalLineNumbersWidth = originalOpts.layoutInfo.glyphMarginWidth + originalOpts.layoutInfo.lineNumbersWidth;
const modifiedLineNumbersWidth = 10 + modifiedOpts.layoutInfo.glyphMarginWidth + modifiedOpts.layoutInfo.lineNumbersWidth;
for (let i = 0; i <= cnt; i++) {
const originalLine = (originalLineStart === 0 ? 0 : originalLineStart + i);
const modifiedLine = (modifiedLineStart === 0 ? 0 : modifiedLineStart + i);
const row = document.createElement('div');
row.style.minWidth = width + 'px';
row.className = rowClassName;
row.setAttribute('role', 'listitem');
if (modifiedLine !== 0) {
modLine = modifiedLine;
}
row.setAttribute('data-line', String(modLine));
let cell = document.createElement('div');
cell.className = 'diff-review-cell';
row.appendChild(cell);
const originalLineNumber = document.createElement('span');
originalLineNumber.style.width = (originalLineNumbersWidth + 'px');
originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px');
originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName;
if (originalLine !== 0) {
originalLineNumber.appendChild(document.createTextNode(String(originalLine)));
} else {
originalLineNumber.innerHTML = '&nbsp;';
}
cell.appendChild(originalLineNumber);
const modifiedLineNumber = document.createElement('span');
modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px');
modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px');
modifiedLineNumber.style.paddingRight = '10px';
modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName;
if (modifiedLine !== 0) {
modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine)));
} else {
modifiedLineNumber.innerHTML = '&nbsp;';
}
cell.appendChild(modifiedLineNumber);
const spacer = document.createElement('span');
spacer.className = spacerClassName;
spacer.innerHTML = '&nbsp;&nbsp;';
cell.appendChild(spacer);
let lineContent: string;
if (modifiedLine !== 0) {
cell.insertAdjacentHTML('beforeend',
this._renderLine(modifiedModel, modifiedOpts, modifiedModelOpts.tabSize, modifiedLine)
);
lineContent = modifiedModel.getLineContent(modifiedLine);
} else {
cell.insertAdjacentHTML('beforeend',
this._renderLine(originalModel, originalOpts, originalModelOpts.tabSize, originalLine)
);
lineContent = originalModel.getLineContent(originalLine);
}
if (lineContent.length === 0) {
lineContent = nls.localize('blankLine', "blank");
}
let ariaLabel: string;
switch (type) {
case DiffEntryType.Equal:
ariaLabel = nls.localize('equalLine', "original {0}, modified {1}: {2}", originalLine, modifiedLine, lineContent);
break;
case DiffEntryType.Insert:
ariaLabel = nls.localize('insertLine', "+ modified {0}: {1}", modifiedLine, lineContent);
break;
case DiffEntryType.Delete:
ariaLabel = nls.localize('deleteLine', "- original {0}: {1}", originalLine, lineContent);
break;
}
row.setAttribute('aria-label', ariaLabel);
dest.appendChild(row);
}
}
private static _renderLine(model: editorCommon.IModel, config: editorOptions.InternalEditorOptions, tabSize: number, lineNumber: number): string {
const lineContent = model.getLineContent(lineNumber);
const defaultMetadata = (
(FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET)
| (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET)
| (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET)
) >>> 0;
const r = renderViewLine(new RenderLineInput(
(config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations),
lineContent,
model.mightContainRTL(),
0,
[new ViewLineToken(lineContent.length, defaultMetadata)],
[],
tabSize,
config.fontInfo.spaceWidth,
config.viewInfo.stopRenderingLineAfter,
config.viewInfo.renderWhitespace,
config.viewInfo.renderControlCharacters,
config.viewInfo.fontLigatures
));
return r.html;
}
}
// theming
registerThemingParticipant((theme, collector) => {
let lineNumbers = theme.getColor(editorLineNumbers);
if (lineNumbers) {
collector.addRule(`.monaco-diff-editor .diff-review-line-number { color: ${lineNumbers}; }`);
}
const shadow = theme.getColor(scrollbarShadow);
if (shadow) {
collector.addRule(`.monaco-diff-editor .diff-review-shadow { box-shadow: ${shadow} 0 -6px 6px -6px inset; }`);
}
});
@editorAction
class DiffReviewNext extends EditorAction {
constructor() {
super({
id: 'editor.action.diffReview.next',
label: nls.localize('editor.action.diffReview.next', "Go to Next Difference"),
alias: 'Go to Next Difference',
precondition: ContextKeyExpr.has('isInDiffEditor'),
kbOpts: {
kbExpr: null,
primary: KeyCode.F7
}
});
}
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
const diffEditor = findFocusedDiffEditor(accessor);
if (diffEditor) {
diffEditor.diffReviewNext();
}
}
}
@editorAction
class DiffReviewPrev extends EditorAction {
constructor() {
super({
id: 'editor.action.diffReview.prev',
label: nls.localize('editor.action.diffReview.prev', "Go to Previous Difference"),
alias: 'Go to Previous Difference',
precondition: ContextKeyExpr.has('isInDiffEditor'),
kbOpts: {
kbExpr: null,
primary: KeyMod.Shift | KeyCode.F7
}
});
}
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
const diffEditor = findFocusedDiffEditor(accessor);
if (diffEditor) {
diffEditor.diffReviewPrev();
}
}
}
function findFocusedDiffEditor(accessor: ServicesAccessor): DiffEditorWidget {
const codeEditorService = accessor.get(ICodeEditorService);
const diffEditors = codeEditorService.listDiffEditors();
for (let i = 0, len = diffEditors.length; i < len; i++) {
const diffEditor = <DiffEditorWidget>diffEditors[i];
if (diffEditor.hasWidgetFocus()) {
return diffEditor;
}
}
return null;
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as objects from 'vs/base/common/objects';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditor } from 'vs/editor/browser/codeEditor';
import { IConfigurationChangedEvent, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class EmbeddedCodeEditorWidget extends CodeEditor {
private _parentEditor: ICodeEditor;
private _overwriteOptions: IEditorOptions;
constructor(
domElement: HTMLElement,
options: IEditorOptions,
parentEditor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService
) {
super(domElement, parentEditor.getRawConfiguration(), instantiationService, codeEditorService, commandService, contextKeyService, themeService);
this._parentEditor = parentEditor;
this._overwriteOptions = options;
// Overwrite parent's options
super.updateOptions(this._overwriteOptions);
this._register(parentEditor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => this._onParentConfigurationChanged(e)));
}
public getParentEditor(): ICodeEditor {
return this._parentEditor;
}
private _onParentConfigurationChanged(e: IConfigurationChangedEvent): void {
super.updateOptions(this._parentEditor.getRawConfiguration());
super.updateOptions(this._overwriteOptions);
}
public updateOptions(newOptions: IEditorOptions): void {
objects.mixin(this._overwriteOptions, newOptions, true);
super.updateOptions(this._overwriteOptions);
}
}

View File

@@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><rect height="11" width="3" y="3" x="7" fill="#C5C5C5"/><rect height="3" width="11" y="7" x="3" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><rect height="11" width="3" y="3" x="7" fill="#424242"/><rect height="3" width="11" y="7" x="3" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><rect height="3" width="11" y="7" x="3" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 147 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><rect height="3" width="11" y="7" x="3" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* ---------- DiffEditor ---------- */
.monaco-diff-editor .diffOverview {
z-index: 9;
}
/* colors not externalized: using transparancy on background */
.monaco-diff-editor.vs .diffOverview { background: rgba(0, 0, 0, 0.03); }
.monaco-diff-editor.vs-dark .diffOverview { background: rgba(255, 255, 255, 0.01); }
.monaco-diff-editor .diffViewport {
box-shadow: inset 0px 0px 1px 0px #B9B9B9;
background: rgba(0, 0, 0, 0.10);
}
.monaco-diff-editor.vs-dark .diffViewport,
.monaco-diff-editor.hc-black .diffViewport {
background: rgba(255, 255, 255, 0.10);
}
.monaco-scrollable-element.modified-in-monaco-diff-editor.vs .scrollbar { background: rgba(0,0,0,0); }
.monaco-scrollable-element.modified-in-monaco-diff-editor.vs-dark .scrollbar { background: rgba(0,0,0,0); }
.monaco-scrollable-element.modified-in-monaco-diff-editor.hc-black .scrollbar { background: none; }
.monaco-scrollable-element.modified-in-monaco-diff-editor .slider {
z-index: 10;
}
.modified-in-monaco-diff-editor .slider.active { background: rgba(171, 171, 171, .4); }
.modified-in-monaco-diff-editor.hc-black .slider.active { background: none; }
/* ---------- Diff ---------- */
.monaco-editor .insert-sign,
.monaco-diff-editor .insert-sign,
.monaco-editor .delete-sign,
.monaco-diff-editor .delete-sign {
background-size: 60%;
opacity: 0.7;
background-repeat: no-repeat;
background-position: 50% 50%;
}
.monaco-editor.hc-black .insert-sign,
.monaco-diff-editor.hc-black .insert-sign,
.monaco-editor.hc-black .delete-sign,
.monaco-diff-editor.hc-black .delete-sign {
opacity: 1;
}
.monaco-editor .insert-sign,
.monaco-diff-editor .insert-sign {
background-image: url('addition.svg');
}
.monaco-editor .delete-sign,
.monaco-diff-editor .delete-sign {
background-image: url('deletion.svg');
}
.monaco-editor.vs-dark .insert-sign,
.monaco-diff-editor.vs-dark .insert-sign,
.monaco-editor.hc-black .insert-sign,
.monaco-diff-editor.hc-black .insert-sign {
background-image: url('addition-inverse.svg');
}
.monaco-editor.vs-dark .delete-sign,
.monaco-diff-editor.vs-dark .delete-sign,
.monaco-editor.hc-black .delete-sign,
.monaco-diff-editor.hc-black .delete-sign {
background-image: url('deletion-inverse.svg');
}
.monaco-editor .inline-deleted-margin-view-zone {
text-align: right;
}
.monaco-editor .inline-added-margin-view-zone {
text-align: right;
}
.monaco-editor .diagonal-fill {
background: url('diagonal-fill.png');
}
.monaco-editor.vs-dark .diagonal-fill {
opacity: 0.2;
}
.monaco-editor.hc-black .diagonal-fill {
background: none;
}
/* ---------- Inline Diff ---------- */
.monaco-editor .view-zones .view-lines .view-line span {
display: inline-block;
}

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-diff-editor .diff-review-line-number {
text-align: right;
display: inline-block;
}
.monaco-diff-editor .diff-review {
position: absolute;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
.monaco-diff-editor .diff-review-summary {
padding-left: 10px;
}
.monaco-diff-editor .diff-review-shadow {
position: absolute;
}
.monaco-diff-editor .diff-review-row {
white-space: pre;
}
.monaco-diff-editor .diff-review-table {
display: table;
min-width: 100%;
}
.monaco-diff-editor .diff-review-row {
display: table-row;
width: 100%;
}
.monaco-diff-editor .diff-review-cell {
display: table-cell;
}
.monaco-diff-editor .diff-review-spacer {
display: inline-block;
width: 10px;
}
.monaco-diff-editor .diff-review-actions {
display: inline-block;
position: absolute;
right: 10px;
top: 2px;
}
.monaco-diff-editor .diff-review-actions .action-label {
width: 16px;
height: 16px;
margin: 2px 0;
}
.monaco-diff-editor .action-label.icon.close-diff-review {
background: url('close.svg') center center no-repeat;
}
.monaco-diff-editor.hc-black .action-label.icon.close-diff-review,
.monaco-diff-editor.vs-dark .action-label.icon.close-diff-review {
background: url('close-inverse.svg') center center no-repeat;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* -------------------- IE10 remove auto clear button -------------------- */
::-ms-clear {
display: none;
}
/* All widgets */
/* I am not a big fan of this rule */
.monaco-editor .editor-widget input {
color: inherit;
}
/* -------------------- Editor -------------------- */
.monaco-editor {
position: relative;
overflow: visible;
-webkit-text-size-adjust: 100%;
-webkit-font-feature-settings: "liga" off, "calt" off;
font-feature-settings: "liga" off, "calt" off;
}
.monaco-editor.enable-ligatures {
-webkit-font-feature-settings: "liga" on, "calt" on;
font-feature-settings: "liga" on, "calt" on;
}
/* -------------------- Misc -------------------- */
.monaco-editor .overflow-guard {
position: relative;
overflow: hidden;
}
.monaco-editor .view-overlays {
position: absolute;
top: 0;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .vs-whitespace {
display:inline-block;
}