SQL Operations Studio Public Preview 1 (0.23) release source code
39
src/vs/editor/browser/codeEditor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
159
src/vs/editor/browser/config/charWidthReader.ts
Normal 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 = ' ';
|
||||
// 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();
|
||||
}
|
||||
364
src/vs/editor/browser/config/configuration.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
79
src/vs/editor/browser/config/elementSizeObserver.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
573
src/vs/editor/browser/controller/mouseHandler.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
923
src/vs/editor/browser/controller/mouseTarget.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
248
src/vs/editor/browser/controller/pointerHandler.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
34
src/vs/editor/browser/controller/textAreaHandler.css
Normal 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;
|
||||
}
|
||||
508
src/vs/editor/browser/controller/textAreaHandler.ts
Normal 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}`;
|
||||
}
|
||||
585
src/vs/editor/browser/controller/textAreaInput.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
289
src/vs/editor/browser/controller/textAreaState.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
483
src/vs/editor/browser/editorBrowser.ts
Normal 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;
|
||||
}
|
||||
43
src/vs/editor/browser/editorBrowserExtensions.ts
Normal 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);
|
||||
180
src/vs/editor/browser/editorDom.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
476
src/vs/editor/browser/services/codeEditorServiceImpl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/vs/editor/browser/view/dynamicViewOverlay.ts
Normal 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;
|
||||
|
||||
}
|
||||
313
src/vs/editor/browser/view/viewController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
597
src/vs/editor/browser/view/viewImpl.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
610
src/vs/editor/browser/view/viewLayer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/vs/editor/browser/view/viewOutgoingEvents.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
286
src/vs/editor/browser/view/viewOverlays.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
81
src/vs/editor/browser/view/viewPart.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
431
src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }`);
|
||||
}
|
||||
}
|
||||
});
|
||||
12
src/vs/editor/browser/viewParts/decorations/decorations.css
Normal 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;
|
||||
}
|
||||
209
src/vs/editor/browser/viewParts/decorations/decorations.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
17
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css
Normal 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;
|
||||
}
|
||||
200
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
131
src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts
Normal 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}; }`);
|
||||
}
|
||||
});
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
38
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css
Normal 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;
|
||||
}
|
||||
169
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
Normal 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}; }`);
|
||||
}
|
||||
});
|
||||
146
src/vs/editor/browser/viewParts/lines/rangeUtil.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
613
src/vs/editor/browser/viewParts/lines/viewLine.ts
Normal 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);
|
||||
}
|
||||
55
src/vs/editor/browser/viewParts/lines/viewLines.css
Normal 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;
|
||||
}*/
|
||||
689
src/vs/editor/browser/viewParts/lines/viewLines.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
95
src/vs/editor/browser/viewParts/margin/margin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
27
src/vs/editor/browser/viewParts/minimap/minimap.css
Normal 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;
|
||||
}
|
||||
911
src/vs/editor/browser/viewParts/minimap/minimap.ts
Normal 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; }`);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
152
src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
9
src/vs/editor/browser/viewParts/rulers/rulers.css
Normal 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;
|
||||
}
|
||||
112
src/vs/editor/browser/viewParts/rulers/rulers.ts
Normal 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}; }`);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }`);
|
||||
}
|
||||
});
|
||||
22
src/vs/editor/browser/viewParts/selections/selections.css
Normal 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; }
|
||||
409
src/vs/editor/browser/viewParts/selections/selections.ts
Normal 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}; }`);
|
||||
}
|
||||
});
|
||||
203
src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
85
src/vs/editor/browser/viewParts/viewCursors/viewCursors.css
Normal 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;
|
||||
}
|
||||
368
src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts
Normal 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}; }`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
353
src/vs/editor/browser/viewParts/viewZones/viewZones.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
535
src/vs/editor/browser/widget/codeEditorWidget.ts
Normal 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; }`);
|
||||
}
|
||||
});
|
||||
2059
src/vs/editor/browser/widget/diffEditorWidget.ts
Normal file
226
src/vs/editor/browser/widget/diffNavigator.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
822
src/vs/editor/browser/widget/diffReview.ts
Normal 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 = ' ';
|
||||
}
|
||||
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 = ' ';
|
||||
}
|
||||
cell.appendChild(modifiedLineNumber);
|
||||
|
||||
const spacer = document.createElement('span');
|
||||
spacer.className = spacerClassName;
|
||||
spacer.innerHTML = ' ';
|
||||
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;
|
||||
}
|
||||
56
src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
src/vs/editor/browser/widget/media/addition-inverse.svg
Normal 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 |
1
src/vs/editor/browser/widget/media/addition.svg
Normal 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 |
1
src/vs/editor/browser/widget/media/close-inverse.svg
Normal 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 |
1
src/vs/editor/browser/widget/media/close.svg
Normal 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 |
1
src/vs/editor/browser/widget/media/deletion-inverse.svg
Normal 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 |
1
src/vs/editor/browser/widget/media/deletion.svg
Normal 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 |
BIN
src/vs/editor/browser/widget/media/diagonal-fill.png
Normal file
|
After Width: | Height: | Size: 185 B |
94
src/vs/editor/browser/widget/media/diffEditor.css
Normal 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;
|
||||
}
|
||||
70
src/vs/editor/browser/widget/media/diffReview.css
Normal 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;
|
||||
}
|
||||
42
src/vs/editor/browser/widget/media/editor.css
Normal 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;
|
||||
}
|
||||
9
src/vs/editor/browser/widget/media/tokens.css
Normal 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;
|
||||
}
|
||||
|
||||