textAreaHandler.ts 16.9 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import 'vs/css!./textAreaHandler';
8
import * as platform from 'vs/base/common/platform';
A
Alex Dima 已提交
9
import * as browser from 'vs/base/browser/browser';
10
import { TextAreaInput, ITextAreaInputHost, IPasteData, ICompositionData } from 'vs/editor/browser/controller/textAreaInput';
11
import { ISimpleModel, ITypeData, TextAreaState, PagedScreenReaderStrategy } from 'vs/editor/browser/controller/textAreaState';
J
Johannes Rieken 已提交
12
import { Range } from 'vs/editor/common/core/range';
13 14
import { Selection } from 'vs/editor/common/core/selection';
import { Position } from 'vs/editor/common/core/position';
J
Johannes Rieken 已提交
15 16
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ViewContext } from 'vs/editor/common/view/viewContext';
17
import { HorizontalRange, RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
18
import * as viewEvents from 'vs/editor/common/view/viewEvents';
19
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
20
import { ViewController } from 'vs/editor/browser/view/viewController';
A
Alex Dima 已提交
21
import { EndOfLinePreference } from "vs/editor/common/editorCommon";
22
import { IKeyboardEvent } from "vs/base/browser/keyboardEvent";
23
import { PartFingerprints, PartFingerprint, ViewPart } from "vs/editor/browser/view/viewPart";
24 25
import { Margin } from "vs/editor/browser/viewParts/margin/margin";
import { LineNumbersOverlay } from "vs/editor/browser/viewParts/lineNumbers/lineNumbers";
A
Alex Dima 已提交
26
import { BareFontInfo } from "vs/editor/common/config/fontInfo";
27

28
export interface ITextAreaHandlerHelper {
29
	visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalRange;
30
}
31

32
class VisibleTextAreaData {
33
	_visibleTextAreaBrand: void;
34 35 36

	public readonly top: number;
	public readonly left: number;
37
	public readonly width: number;
38

39
	constructor(top: number, left: number, width: number) {
40 41
		this.top = top;
		this.left = left;
42 43 44
		this.width = width;
	}

45 46
	public setWidth(width: number): VisibleTextAreaData {
		return new VisibleTextAreaData(this.top, this.left, width);
47 48
	}
}
49

50 51 52
const canUseZeroSizeTextarea = (browser.isEdgeOrIE || browser.isFirefox);

export class TextAreaHandler extends ViewPart {
53

54
	private readonly _viewController: ViewController;
55
	private readonly _viewHelper: ITextAreaHandlerHelper;
56

57
	private _pixelRatio: number;
58
	private _accessibilitySupport: platform.AccessibilitySupport;
59 60
	private _contentLeft: number;
	private _contentWidth: number;
61
	private _contentHeight: number;
62 63
	private _scrollLeft: number;
	private _scrollTop: number;
64 65
	private _fontInfo: BareFontInfo;
	private _lineHeight: number;
66
	private _emptySelectionClipboard: boolean;
67 68 69 70

	/**
	 * Defined only when the text area is visible (composition case).
	 */
71
	private _visibleTextArea: VisibleTextAreaData;
72
	private _selections: Selection[];
A
Alex Dima 已提交
73 74 75
	private _lastCopiedValue: string;
	private _lastCopiedValueIsFromEmptySelection: boolean;

76 77
	public readonly textArea: FastDomNode<HTMLTextAreaElement>;
	public readonly textAreaCover: FastDomNode<HTMLElement>;
78
	private readonly _textAreaInput: TextAreaInput;
79

80
	constructor(context: ViewContext, viewController: ViewController, viewHelper: ITextAreaHandlerHelper) {
81
		super(context);
E
Erich Gamma 已提交
82

83 84
		this._viewController = viewController;
		this._viewHelper = viewHelper;
85

86 87 88
		const conf = this._context.configuration.editor;

		this._pixelRatio = conf.pixelRatio;
89
		this._accessibilitySupport = conf.accessibilitySupport;
90 91 92
		this._contentLeft = conf.layoutInfo.contentLeft;
		this._contentWidth = conf.layoutInfo.contentWidth;
		this._contentHeight = conf.layoutInfo.contentHeight;
93 94
		this._scrollLeft = 0;
		this._scrollTop = 0;
95 96
		this._fontInfo = conf.fontInfo;
		this._lineHeight = conf.lineHeight;
97
		this._emptySelectionClipboard = conf.emptySelectionClipboard;
98

99
		this._visibleTextArea = null;
100
		this._selections = [new Selection(1, 1, 1, 1)];
A
Alex Dima 已提交
101 102 103
		this._lastCopiedValue = null;
		this._lastCopiedValueIsFromEmptySelection = false;

104 105 106 107 108 109 110 111
		// 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('spellcheck', 'false');
112
		this.textArea.setAttribute('aria-label', conf.viewInfo.ariaLabel);
113 114 115 116 117 118 119 120
		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');

121 122 123 124 125 126 127 128 129 130 131 132
		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);
			}
		};

133
		const textAreaInputHost: ITextAreaInputHost = {
A
Alex Dima 已提交
134
			getPlainTextToCopy: (): string => {
135
				const whatToCopy = this._context.model.getPlainTextToCopy(this._selections, this._emptySelectionClipboard);
A
Alex Dima 已提交
136

137
				if (this._emptySelectionClipboard) {
A
Alex Dima 已提交
138 139 140 141 142 143 144 145 146 147 148 149 150 151
					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;
			},
152

A
Alex Dima 已提交
153
			getHTMLToCopy: (): string => {
154
				return this._context.model.getHTMLToCopy(this._selections, this._emptySelectionClipboard);
A
Alex Dima 已提交
155
			},
156 157 158 159 160 161 162 163

			getScreenReaderContent: (currentState: TextAreaState): TextAreaState => {

				if (browser.isIPad) {
					// Do not place anything in the textarea for the iPad
					return TextAreaState.EMPTY;
				}

164 165 166 167 168
				if (this._accessibilitySupport === platform.AccessibilitySupport.Disabled) {
					// We know for a fact that a screen reader is not attached
					return TextAreaState.EMPTY;
				}

169
				return PagedScreenReaderStrategy.fromEditorSelection(currentState, simpleModel, this._selections[0]);
A
Alex Dima 已提交
170 171
			}
		};
172

173
		this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, this.textArea));
174 175 176 177 178 179 180 181 182 183

		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) => {
A
Alex Dima 已提交
184
			let pasteOnNewLine = false;
185
			if (this._emptySelectionClipboard) {
A
Alex Dima 已提交
186 187
				pasteOnNewLine = (e.text === this._lastCopiedValue && this._lastCopiedValueIsFromEmptySelection);
			}
188 189 190 191 192
			this._viewController.paste('keyboard', e.text, pasteOnNewLine);
		}));

		this._register(this._textAreaInput.onCut(() => {
			this._viewController.cut('keyboard');
A
Alex Dima 已提交
193
		}));
194 195

		this._register(this._textAreaInput.onType((e: ITypeData) => {
196
			if (e.replaceCharCnt) {
197
				this._viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt);
198
			} else {
199
				this._viewController.type('keyboard', e.text);
200 201
			}
		}));
202

203
		this._register(this._textAreaInput.onCompositionStart(() => {
A
Alex Dima 已提交
204 205
			const lineNumber = this._selections[0].startLineNumber;
			const column = this._selections[0].startColumn;
206

207
			this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
A
Alex Dima 已提交
208
				new Range(lineNumber, column, lineNumber, column),
209
				viewEvents.VerticalRevealType.Simple,
210
				true
A
Alex Dima 已提交
211
			));
212 213

			// Find range pixel position
214
			const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
215 216

			if (visibleRange) {
217 218
				this._visibleTextArea = new VisibleTextAreaData(
					this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber),
219 220 221
					visibleRange.left,
					canUseZeroSizeTextarea ? 0 : 1
				);
222
				this._render();
223 224 225
			}

			// Show the textarea
226
			this.textArea.setClassName('inputarea ime-input');
227

228
			this._viewController.compositionStart('keyboard');
229
		}));
230

231
		this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
232 233 234
			if (browser.isEdgeOrIE) {
				// Due to isEdgeOrIE (where the textarea was not cleared initially)
				// we cannot assume the text consists only of the composited text
235
				this._visibleTextArea = this._visibleTextArea.setWidth(0);
236 237
			} else {
				// adjust width by its size
238
				this._visibleTextArea = this._visibleTextArea.setWidth(measureText(e.data, this._fontInfo));
239
			}
240
			this._render();
241 242
		}));

243
		this._register(this._textAreaInput.onCompositionEnd(() => {
244

245 246
			this._visibleTextArea = null;
			this._render();
247

248
			this.textArea.setClassName('inputarea');
249
			this._viewController.compositionEnd('keyboard');
250
		}));
251

252 253 254 255 256 257 258
		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));
		}));
E
Erich Gamma 已提交
259 260
	}

261
	public dispose(): void {
A
Alex Dima 已提交
262
		super.dispose();
263 264
	}

265 266
	// --- begin event handlers

A
Alex Dima 已提交
267
	public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
268 269
		const conf = this._context.configuration.editor;

270
		if (e.fontInfo) {
271
			this._fontInfo = conf.fontInfo;
272
		}
A
Alex Dima 已提交
273
		if (e.viewInfo) {
274
			this.textArea.setAttribute('aria-label', conf.viewInfo.ariaLabel);
275
		}
A
Alex Dima 已提交
276
		if (e.layoutInfo) {
277 278 279
			this._contentLeft = conf.layoutInfo.contentLeft;
			this._contentWidth = conf.layoutInfo.contentWidth;
			this._contentHeight = conf.layoutInfo.contentHeight;
A
Alex Dima 已提交
280
		}
281
		if (e.lineHeight) {
282
			this._lineHeight = conf.lineHeight;
283
		}
284 285 286
		if (e.pixelRatio) {
			this._pixelRatio = conf.pixelRatio;
		}
287 288 289 290
		if (e.accessibilitySupport) {
			this._accessibilitySupport = conf.accessibilitySupport;
			this._textAreaInput.writeScreenReaderContent('strategy changed');
		}
291 292 293
		if (e.emptySelectionClipboard) {
			this._emptySelectionClipboard = conf.emptySelectionClipboard;
		}
294

295
		return true;
296
	}
297 298
	public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
		this._selections = e.selections.slice(0);
299
		this._textAreaInput.writeScreenReaderContent('selection changed');
300
		return true;
301
	}
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
	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;
	}
318
	public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
319 320
		this._scrollLeft = e.scrollLeft;
		this._scrollTop = e.scrollTop;
321 322 323
		return true;
	}
	public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
324
		return true;
325 326
	}

327 328
	// --- end event handlers

329 330
	// --- begin view API

A
Alex Dima 已提交
331 332 333 334 335 336 337 338
	public isFocused(): boolean {
		return this._textAreaInput.isFocused();
	}

	public focusTextArea(): void {
		this._textAreaInput.focusTextArea();
	}

339 340
	public setAriaActiveDescendant(id: string): void {
		if (id) {
341 342 343 344
			this.textArea.setAttribute('role', 'combobox');
			if (this.textArea.getAttribute('aria-activedescendant') !== id) {
				this.textArea.setAttribute('aria-haspopup', 'true');
				this.textArea.setAttribute('aria-activedescendant', id);
345 346
			}
		} else {
347 348 349
			this.textArea.setAttribute('role', 'textbox');
			this.textArea.removeAttribute('aria-activedescendant');
			this.textArea.removeAttribute('aria-haspopup');
350 351 352 353
		}
	}

	// --- end view API
354

355 356
	private _primaryCursorVisibleRange: HorizontalRange = null;

357
	public prepareRender(ctx: RenderingContext): void {
358 359 360 361 362 363 364 365
		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);
		}
366 367 368
	}

	public render(ctx: RestrictedRenderingContext): void {
369
		this._textAreaInput.writeScreenReaderContent('render');
370 371 372 373 374 375
		this._render();
	}

	private _render(): void {
		if (this._visibleTextArea) {
			// The text area is visible for composition reasons
376 377 378 379
			this._renderInsideEditor(
				this._visibleTextArea.top - this._scrollTop,
				this._contentLeft + this._visibleTextArea.left - this._scrollLeft,
				this._visibleTextArea.width,
380 381
				this._lineHeight,
				true
382 383 384
			);
			return;
		}
385

386 387 388 389 390
		if (!this._primaryCursorVisibleRange) {
			// The primary cursor is outside the viewport => place textarea to the top left
			this._renderAtTopLeft();
			return;
		}
391

392 393 394 395 396 397 398
		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;
		}

399
		const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;
400 401 402 403 404 405 406
		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
407 408 409 410 411
		this._renderInsideEditor(
			top, left,
			canUseZeroSizeTextarea ? 0 : 1, canUseZeroSizeTextarea ? 0 : 1,
			false
		);
412 413
	}

414
	private _renderInsideEditor(top: number, left: number, width: number, height: number, useEditorFont: boolean): void {
415 416 417
		const ta = this.textArea;
		const tac = this.textAreaCover;

418 419 420 421
		if (useEditorFont) {
			Configuration.applyFontInfo(ta, this._fontInfo);
		} else {
			ta.setFontSize(1);
422 423
			// Chrome does not generate input events in empty textareas that end
			// up having a line height smaller than 1 screen pixel.
424
			ta.setLineHeight(Math.ceil(Math.max(this._pixelRatio, 1 / this._pixelRatio)));
425 426
		}

427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
		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;

442
		Configuration.applyFontInfo(ta, this._fontInfo);
443 444 445 446 447 448 449 450 451 452 453 454
		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;
		}
455

456 457
		// (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.
458

459 460 461 462 463 464 465 466 467 468
		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);
469
			} else {
470
				tac.setClassName('monaco-editor-background textAreaCover');
471 472 473
			}
		}
	}
474
}
A
Alex Dima 已提交
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500

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}`;
}