提交 fd052288 编写于 作者: A Alex Dima

Backport IME related changes (Microsoft/monaco-editor#104)

上级 8302cd72
......@@ -56,6 +56,8 @@ export const isIE9 = (userAgent.indexOf('MSIE 9') >= 0);
export const isIE11orEarlier = isIE11 || isIE10 || isIE9;
export const isIE10orEarlier = isIE10 || isIE9;
export const isIE10orLater = isIE11 || isIE10;
export const isEdge = (userAgent.indexOf('Edge/') >= 0);
export const isEdgeOrIE = isEdge || isIE11 || isIE10 || isIE9;
export const isOpera = (userAgent.indexOf('Opera') >= 0);
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import Event, {Emitter} from 'vs/base/common/event';
import {Disposable} from 'vs/base/common/lifecycle';
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {IClipboardEvent, ICompositionEvent, IKeyboardEventWrapper, ITextAreaWrapper} from 'vs/editor/common/controller/textAreaState';
class ClipboardEventWrapper implements IClipboardEvent {
private _event:ClipboardEvent;
constructor(event:ClipboardEvent) {
this._event = event;
}
public canUseTextData(): boolean {
if (this._event.clipboardData) {
return true;
}
if ((<any>window).clipboardData) {
return true;
}
return false;
}
public setTextData(text:string): void {
if (this._event.clipboardData) {
this._event.clipboardData.setData('text/plain', text);
this._event.preventDefault();
return;
}
if ((<any>window).clipboardData) {
(<any>window).clipboardData.setData('Text', text);
this._event.preventDefault();
return;
}
throw new Error('ClipboardEventWrapper.setTextData: Cannot use text data!');
}
public getTextData(): string {
if (this._event.clipboardData) {
this._event.preventDefault();
return this._event.clipboardData.getData('text/plain');
}
if ((<any>window).clipboardData) {
this._event.preventDefault();
return (<any>window).clipboardData.getData('Text');
}
throw new Error('ClipboardEventWrapper.getTextData: Cannot use text data!');
}
}
class KeyboardEventWrapper implements IKeyboardEventWrapper {
public _actual: IKeyboardEvent;
constructor(actual:IKeyboardEvent) {
this._actual = actual;
}
public equals(keybinding:number): boolean {
return this._actual.equals(keybinding);
}
public preventDefault(): void {
this._actual.preventDefault();
}
public isDefaultPrevented(): boolean {
if (this._actual.browserEvent) {
return this._actual.browserEvent.defaultPrevented;
}
return false;
}
}
export class TextAreaWrapper extends Disposable implements ITextAreaWrapper {
private _textArea: HTMLTextAreaElement;
private _onKeyDown = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyDown: Event<IKeyboardEventWrapper> = this._onKeyDown.event;
private _onKeyUp = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyUp: Event<IKeyboardEventWrapper> = this._onKeyUp.event;
private _onKeyPress = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyPress: Event<IKeyboardEventWrapper> = this._onKeyPress.event;
private _onCompositionStart = this._register(new Emitter<ICompositionEvent>());
public onCompositionStart: Event<ICompositionEvent> = this._onCompositionStart.event;
private _onCompositionUpdate = this._register(new Emitter<ICompositionEvent>());
public onCompositionUpdate: Event<ICompositionEvent> = this._onCompositionUpdate.event;
private _onCompositionEnd = this._register(new Emitter<ICompositionEvent>());
public onCompositionEnd: Event<ICompositionEvent> = this._onCompositionEnd.event;
private _onInput = this._register(new Emitter<void>());
public onInput: Event<void> = this._onInput.event;
private _onCut = this._register(new Emitter<IClipboardEvent>());
public onCut: Event<IClipboardEvent> = this._onCut.event;
private _onCopy = this._register(new Emitter<IClipboardEvent>());
public onCopy: Event<IClipboardEvent> = this._onCopy.event;
private _onPaste = this._register(new Emitter<IClipboardEvent>());
public onPaste: Event<IClipboardEvent> = this._onPaste.event;
constructor(textArea: HTMLTextAreaElement) {
super();
this._textArea = textArea;
this._register(dom.addStandardDisposableListener(this._textArea, 'keydown', (e) => this._onKeyDown.fire(new KeyboardEventWrapper(e))));
this._register(dom.addStandardDisposableListener(this._textArea, 'keyup', (e) => this._onKeyUp.fire(new KeyboardEventWrapper(e))));
this._register(dom.addStandardDisposableListener(this._textArea, 'keypress', (e) => this._onKeyPress.fire(new KeyboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'compositionstart', (e) => this._onCompositionStart.fire(e)));
this._register(dom.addDisposableListener(this._textArea, 'compositionupdate', (e) => this._onCompositionUpdate.fire(e)));
this._register(dom.addDisposableListener(this._textArea, 'compositionend', (e) => this._onCompositionEnd.fire(e)));
this._register(dom.addDisposableListener(this._textArea, 'input', (e) => this._onInput.fire()));
this._register(dom.addDisposableListener(this._textArea, 'cut', (e:ClipboardEvent) => this._onCut.fire(new ClipboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'copy', (e:ClipboardEvent) => this._onCopy.fire(new ClipboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'paste', (e:ClipboardEvent) => this._onPaste.fire(new ClipboardEventWrapper(e))));
}
public get actual(): HTMLTextAreaElement {
return this._textArea;
}
public getValue(): string {
// console.log('current value: ' + this._textArea.value);
return this._textArea.value;
}
public setValue(reason:string, value:string): void {
// console.log('reason: ' + reason + ', current value: ' + this._textArea.value + ' => new value: ' + value);
this._textArea.value = value;
}
public getSelectionStart(): number {
return this._textArea.selectionStart;
}
public getSelectionEnd(): number {
return this._textArea.selectionEnd;
}
public setSelectionRange(selectionStart:number, selectionEnd:number): void {
let activeElement = document.activeElement;
if (activeElement === this._textArea) {
this._textArea.setSelectionRange(selectionStart, selectionEnd);
} else {
this._setSelectionRangeJumpy(selectionStart, selectionEnd);
}
}
private _setSelectionRangeJumpy(selectionStart:number, selectionEnd:number): void {
try {
let scrollState = dom.saveParentsScrollTop(this._textArea);
this._textArea.focus();
this._textArea.setSelectionRange(selectionStart, selectionEnd);
dom.restoreParentsScrollTop(this._textArea, scrollState);
} catch(e) {
// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
console.log('an error has been thrown!');
}
}
public isInOverwriteMode(): boolean {
// In IE, pressing Insert will bring the typing into overwrite mode
if (browser.isIE11orEarlier && document.queryCommandValue('OverWrite')) {
return true;
}
return false;
}
}
......@@ -4,184 +4,38 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import Event, {Emitter} from 'vs/base/common/event';
import {Disposable, IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import {IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {StyleMutator} from 'vs/base/browser/styleMutator';
import {GlobalScreenReaderNVDA} from 'vs/editor/common/config/commonEditorConfig';
import {TextAreaHandler} from 'vs/editor/common/controller/textAreaHandler';
import {IClipboardEvent, IKeyboardEventWrapper, ITextAreaWrapper, TextAreaStrategy} from 'vs/editor/common/controller/textAreaState';
import {TextAreaStrategy} from 'vs/editor/common/controller/textAreaState';
import {Range} from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {ViewEventHandler} from 'vs/editor/common/viewModel/viewEventHandler';
import {IKeyboardHandlerHelper, IViewContext, IViewController} from 'vs/editor/browser/editorBrowser';
class ClipboardEventWrapper implements IClipboardEvent {
private _event:ClipboardEvent;
constructor(event:ClipboardEvent) {
this._event = event;
}
public canUseTextData(): boolean {
if (this._event.clipboardData) {
return true;
}
if ((<any>window).clipboardData) {
return true;
}
return false;
}
public setTextData(text:string): void {
if (this._event.clipboardData) {
this._event.clipboardData.setData('text/plain', text);
this._event.preventDefault();
return;
}
if ((<any>window).clipboardData) {
(<any>window).clipboardData.setData('Text', text);
this._event.preventDefault();
return;
}
throw new Error('ClipboardEventWrapper.setTextData: Cannot use text data!');
}
public getTextData(): string {
if (this._event.clipboardData) {
this._event.preventDefault();
return this._event.clipboardData.getData('text/plain');
}
if ((<any>window).clipboardData) {
this._event.preventDefault();
return (<any>window).clipboardData.getData('Text');
}
throw new Error('ClipboardEventWrapper.getTextData: Cannot use text data!');
import {IViewContext, IViewController, IKeyboardHandlerHelper} from 'vs/editor/browser/editorBrowser';
import {TextAreaWrapper} from 'vs/editor/browser/controller/input/textAreaWrapper';
function applyEditorFontInfo(target:HTMLElement, source:editorCommon.IConfiguration): void {
let styling = source.editor.stylingInfo;
if (styling.fontFamily && styling.fontFamily.length > 0) {
target.style.fontFamily = styling.fontFamily;
} else {
target.style.fontFamily = '';
}
if (styling.fontSize > 0) {
target.style.fontSize = styling.fontSize + 'px';
} else {
target.style.fontSize = '';
}
if (styling.lineHeight > 0) {
target.style.lineHeight = styling.lineHeight + 'px';
} else {
target.style.lineHeight = '';
}
}
class KeyboardEventWrapper implements IKeyboardEventWrapper {
public _actual: IKeyboardEvent;
constructor(actual:IKeyboardEvent) {
this._actual = actual;
}
public equals(keybinding:number): boolean {
return this._actual.equals(keybinding);
}
public preventDefault(): void {
this._actual.preventDefault();
}
public isDefaultPrevented(): boolean {
if (this._actual.browserEvent) {
return this._actual.browserEvent.defaultPrevented;
}
return false;
}
}
class TextAreaWrapper extends Disposable implements ITextAreaWrapper {
private _textArea: HTMLTextAreaElement;
private _onKeyDown = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyDown: Event<IKeyboardEventWrapper> = this._onKeyDown.event;
private _onKeyUp = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyUp: Event<IKeyboardEventWrapper> = this._onKeyUp.event;
private _onKeyPress = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyPress: Event<IKeyboardEventWrapper> = this._onKeyPress.event;
private _onCompositionStart = this._register(new Emitter<void>());
public onCompositionStart: Event<void> = this._onCompositionStart.event;
private _onCompositionEnd = this._register(new Emitter<void>());
public onCompositionEnd: Event<void> = this._onCompositionEnd.event;
private _onInput = this._register(new Emitter<void>());
public onInput: Event<void> = this._onInput.event;
private _onCut = this._register(new Emitter<IClipboardEvent>());
public onCut: Event<IClipboardEvent> = this._onCut.event;
private _onCopy = this._register(new Emitter<IClipboardEvent>());
public onCopy: Event<IClipboardEvent> = this._onCopy.event;
private _onPaste = this._register(new Emitter<IClipboardEvent>());
public onPaste: Event<IClipboardEvent> = this._onPaste.event;
constructor(textArea: HTMLTextAreaElement) {
super();
this._textArea = textArea;
this._register(dom.addStandardDisposableListener(this._textArea, 'keydown', (e) => this._onKeyDown.fire(new KeyboardEventWrapper(e))));
this._register(dom.addStandardDisposableListener(this._textArea, 'keyup', (e) => this._onKeyUp.fire(new KeyboardEventWrapper(e))));
this._register(dom.addStandardDisposableListener(this._textArea, 'keypress', (e) => this._onKeyPress.fire(new KeyboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'compositionstart', (e) => this._onCompositionStart.fire()));
this._register(dom.addDisposableListener(this._textArea, 'compositionend', (e) => this._onCompositionEnd.fire()));
this._register(dom.addDisposableListener(this._textArea, 'input', (e) => this._onInput.fire()));
this._register(dom.addDisposableListener(this._textArea, 'cut', (e:ClipboardEvent) => this._onCut.fire(new ClipboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'copy', (e:ClipboardEvent) => this._onCopy.fire(new ClipboardEventWrapper(e))));
this._register(dom.addDisposableListener(this._textArea, 'paste', (e:ClipboardEvent) => this._onPaste.fire(new ClipboardEventWrapper(e))));
}
public get actual(): HTMLTextAreaElement {
return this._textArea;
}
public getValue(): string {
// console.log('current value: ' + this._textArea.value);
return this._textArea.value;
}
public setValue(reason:string, value:string): void {
// console.log('reason: ' + reason + ', current value: ' + this._textArea.value + ' => new value: ' + value);
this._textArea.value = value;
}
public getSelectionStart(): number {
return this._textArea.selectionStart;
}
public getSelectionEnd(): number {
return this._textArea.selectionEnd;
}
public setSelectionRange(selectionStart:number, selectionEnd:number): void {
// console.log('setSelectionRange: ' + selectionStart + ', ' + selectionEnd);
try {
let scrollState = dom.saveParentsScrollTop(this._textArea);
this._textArea.focus();
this._textArea.setSelectionRange(selectionStart, selectionEnd);
dom.restoreParentsScrollTop(this._textArea, scrollState);
} catch(e) {
// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
console.log('an error has been thrown!');
}
}
public isInOverwriteMode(): boolean {
// In IE, pressing Insert will bring the typing into overwrite mode
if (browser.isIE11orEarlier && document.queryCommandValue('OverWrite')) {
return true;
}
return false;
}
}
export class KeyboardHandler extends ViewEventHandler implements IDisposable {
private context:IViewContext;
......@@ -195,19 +49,22 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable {
private contentWidth:number;
private scrollLeft:number;
private visibleRange:editorCommon.VisibleRange;
constructor(context:IViewContext, viewController:IViewController, viewHelper:IKeyboardHandlerHelper) {
super();
this.context = context;
this.viewController = viewController;
this.textArea = new TextAreaWrapper(viewHelper.textArea);
applyEditorFontInfo(this.textArea.actual, this.context.configuration);
this.viewHelper = viewHelper;
this.contentLeft = 0;
this.contentWidth = 0;
this.scrollLeft = 0;
this.textAreaHandler = new TextAreaHandler(browser, this._getStrategy(), this.textArea, this.context.model);
this.textAreaHandler = new TextAreaHandler(browser, this._getStrategy(), this.textArea, this.context.model, () => this.viewHelper.flushAnyAccumulatedEvents());
this._toDispose = [];
this._toDispose.push(this.textAreaHandler.onKeyDown((e) => this.viewController.emitKeyDown(<IKeyboardEvent>e._actual)));
......@@ -233,27 +90,47 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable {
this.context.privateViewEventBus.emit(editorCommon.ViewEventNames.RevealRangeEvent, revealPositionEvent);
// Find range pixel position
let visibleRange = this.viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
if (visibleRange) {
StyleMutator.setTop(this.textArea.actual, visibleRange.top);
StyleMutator.setLeft(this.textArea.actual, this.contentLeft + visibleRange.left - this.scrollLeft);
}
this.visibleRange = this.viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column);
if (browser.isIE11orEarlier) {
StyleMutator.setWidth(this.textArea.actual, this.contentWidth);
if (this.visibleRange) {
StyleMutator.setTop(this.textArea.actual, this.visibleRange.top);
StyleMutator.setLeft(this.textArea.actual, this.contentLeft + this.visibleRange.left - this.scrollLeft);
}
// Show the textarea
StyleMutator.setHeight(this.textArea.actual, this.context.configuration.editor.lineHeight);
dom.addClass(this.viewHelper.viewDomNode, 'ime-input');
}));
this._toDispose.push(this.textAreaHandler.onCompositionUpdate((e) => {
if (browser.isEdgeOrIE || browser.isFirefox) {
// Due to isEdgeOrIE (where the textarea was not cleared initially)
// we cannot assume the text consists only of the composited text
StyleMutator.setWidth(this.textArea.actual, 0);
} else {
// adjust width by its size
let canvasElem = <HTMLCanvasElement>document.createElement('canvas');
let context = canvasElem.getContext('2d');
let cs = dom.getComputedStyle(this.textArea.actual);
if (browser.isFirefox) {
// computedStyle.font is empty in Firefox...
context.font = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontStretch} ${cs.fontSize} / ${cs.lineHeight} '${cs.fontFamily}'`;
} else {
context.font = cs.font;
}
let metrics = context.measureText(e.data);
StyleMutator.setWidth(this.textArea.actual, metrics.width);
}
}));
this._toDispose.push(this.textAreaHandler.onCompositionEnd((e) => {
this.textArea.actual.style.height = '';
this.textArea.actual.style.width = '';
StyleMutator.setLeft(this.textArea.actual, 0);
StyleMutator.setTop(this.textArea.actual, 0);
dom.removeClass(this.viewHelper.viewDomNode, 'ime-input');
this.visibleRange = null;
}));
this._toDispose.push(GlobalScreenReaderNVDA.onChange((value) => {
this.textAreaHandler.setStrategy(this._getStrategy());
......@@ -286,8 +163,7 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable {
public onConfigurationChanged(e: editorCommon.IConfigurationChangedEvent): boolean {
// Give textarea same font size & line height as editor, for the IME case (when the textarea is visible)
StyleMutator.setFontSize(this.textArea.actual, this.context.configuration.editor.fontSize);
StyleMutator.setLineHeight(this.textArea.actual, this.context.configuration.editor.lineHeight);
applyEditorFontInfo(this.textArea.actual, this.context.configuration);
if (e.experimentalScreenReader) {
this.textAreaHandler.setStrategy(this._getStrategy());
}
......@@ -296,6 +172,10 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable {
public onScrollChanged(e:editorCommon.IScrollEvent): boolean {
this.scrollLeft = e.scrollLeft;
if (this.visibleRange) {
StyleMutator.setTop(this.textArea.actual, this.visibleRange.top);
StyleMutator.setLeft(this.textArea.actual, this.contentLeft + this.visibleRange.left - this.scrollLeft);
}
return false;
}
......@@ -309,11 +189,6 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable {
return false;
}
public onCursorPositionChanged(e:editorCommon.IViewCursorPositionChangedEvent): boolean {
this.textAreaHandler.setCursorPosition(e.position);
return false;
}
public onLayoutChanged(layoutInfo:editorCommon.IEditorLayoutInfo): boolean {
this.contentLeft = layoutInfo.contentLeft;
this.contentWidth = layoutInfo.contentWidth;
......
......@@ -42,6 +42,7 @@ export interface IKeyboardHandlerHelper {
viewDomNode:HTMLElement;
textArea:HTMLTextAreaElement;
visibleRangeForPositionRelativeToEditor(lineNumber:number, column:number): editorCommon.VisibleRange;
flushAnyAccumulatedEvents(): void;
}
export interface IPointerHandlerHelper {
......
......@@ -396,6 +396,9 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp
return null;
}
return visibleRanges[0];
},
flushAnyAccumulatedEvents: () => {
this._flushAnyAccumulatedEvents();
}
};
}
......
......@@ -8,10 +8,9 @@ import {RunOnceScheduler} from 'vs/base/common/async';
import Event, {Emitter} from 'vs/base/common/event';
import {CommonKeybindings} from 'vs/base/common/keyCodes';
import {Disposable} from 'vs/base/common/lifecycle';
import {IClipboardEvent, IKeyboardEventWrapper, ISimpleModel, ITextAreaWrapper, ITypeData, TextAreaState, TextAreaStrategy, createTextAreaState} from 'vs/editor/common/controller/textAreaState';
import {Position} from 'vs/editor/common/core/position';
import {IClipboardEvent, ICompositionEvent, IKeyboardEventWrapper, ISimpleModel, ITextAreaWrapper, ITypeData, TextAreaState, TextAreaStrategy, createTextAreaState} from 'vs/editor/common/controller/textAreaState';
import {Range} from 'vs/editor/common/core/range';
import {EndOfLinePreference, IEditorPosition, IEditorRange} from 'vs/editor/common/editorCommon';
import {EndOfLinePreference, IEditorRange} from 'vs/editor/common/editorCommon';
enum ReadFromTextArea {
Type,
......@@ -21,7 +20,7 @@ enum ReadFromTextArea {
export interface IBrowser {
isIPad: boolean;
isChrome: boolean;
isIE11orEarlier: boolean;
isEdgeOrIE: boolean;
isFirefox: boolean;
enableEmptySelectionClipboard: boolean;
}
......@@ -56,12 +55,16 @@ export class TextAreaHandler extends Disposable {
private _onCompositionStart = this._register(new Emitter<ICompositionStartData>());
public onCompositionStart: Event<ICompositionStartData> = this._onCompositionStart.event;
private _onCompositionEnd = this._register(new Emitter<void>());
public onCompositionEnd: Event<void> = this._onCompositionEnd.event;
private _onCompositionUpdate = this._register(new Emitter<ICompositionEvent>());
public onCompositionUpdate: Event<ICompositionEvent> = this._onCompositionUpdate.event;
private _onCompositionEnd = this._register(new Emitter<ICompositionEvent>());
public onCompositionEnd: Event<ICompositionEvent> = this._onCompositionEnd.event;
private Browser:IBrowser;
private textArea:ITextAreaWrapper;
private model:ISimpleModel;
private flushAnyAccumulatedEvents:()=>void;
private selection:IEditorRange;
private selections:IEditorRange[];
......@@ -70,7 +73,6 @@ export class TextAreaHandler extends Disposable {
private asyncTriggerCut: RunOnceScheduler;
private lastCompositionEndTime:number;
private cursorPosition:IEditorPosition;
private textAreaState:TextAreaState;
private textareaIsShownAtCursor: boolean;
......@@ -80,14 +82,14 @@ export class TextAreaHandler extends Disposable {
private _nextCommand: ReadFromTextArea;
constructor(Browser:IBrowser, strategy:TextAreaStrategy, textArea:ITextAreaWrapper, model:ISimpleModel) {
constructor(Browser:IBrowser, strategy:TextAreaStrategy, textArea:ITextAreaWrapper, model:ISimpleModel, flushAnyAccumulatedEvents:()=>void) {
super();
this.Browser = Browser;
this.textArea = textArea;
this.model = model;
this.flushAnyAccumulatedEvents = flushAnyAccumulatedEvents;
this.selection = new Range(1, 1, 1, 1);
this.selections = [new Range(1, 1, 1, 1)];
this.cursorPosition = new Position(1, 1);
this._nextCommand = ReadFromTextArea.Type;
this.asyncTriggerCut = new RunOnceScheduler(() => this._onCut.fire(), 0);
......@@ -106,8 +108,8 @@ export class TextAreaHandler extends Disposable {
this.textareaIsShownAtCursor = false;
this._register(this.textArea.onCompositionStart(() => {
let timeSinceLastCompositionEnd = (new Date().getTime()) - this.lastCompositionEndTime;
this._register(this.textArea.onCompositionStart((e) => {
if (this.textareaIsShownAtCursor) {
return;
}
......@@ -115,32 +117,26 @@ export class TextAreaHandler extends Disposable {
this.textareaIsShownAtCursor = true;
// In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled.
let shouldEmptyTextArea = (timeSinceLastCompositionEnd >= 100);
let shouldEmptyTextArea = true;
if (shouldEmptyTextArea) {
if (!this.Browser.isIE11orEarlier) {
if (!this.Browser.isEdgeOrIE) {
this.setTextAreaState('compositionstart', this.textAreaState.toEmpty());
}
}
let showAtLineNumber: number;
let showAtColumn: number;
// In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled.
if (this.Browser.isIE11orEarlier) {
// Ensure selection start is in viewport
showAtLineNumber = this.selection.startLineNumber;
showAtColumn = (this.selection.startColumn - this.textAreaState.getSelectionStart());
} else {
showAtLineNumber = this.cursorPosition.lineNumber;
showAtColumn = this.cursorPosition.column;
}
this._onCompositionStart.fire({
showAtLineNumber: showAtLineNumber,
showAtColumn: showAtColumn
showAtLineNumber: this.selection.startLineNumber,
showAtColumn: this.selection.startColumn
});
}));
this._register(this.textArea.onCompositionUpdate((e) => {
this.textAreaState = this.textAreaState.fromText(e.data);
let typeInput = this.textAreaState.updateComposition();
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
}));
let readFromTextArea = () => {
this.textAreaState = this.textAreaState.fromTextArea(this.textArea);
let typeInput = this.textAreaState.deduceInput();
......@@ -155,9 +151,15 @@ export class TextAreaHandler extends Disposable {
}
};
this._register(this.textArea.onCompositionEnd(() => {
// console.log('onCompositionEnd: ' + this.textArea.getValue());
// readFromTextArea();
this._register(this.textArea.onCompositionEnd((e) => {
// console.log('onCompositionEnd: ' + e.data);
this.textAreaState = this.textAreaState.fromText(e.data);
let typeInput = this.textAreaState.updateComposition();
this._onType.fire(typeInput);
// Due to isEdgeOrIE (where the textarea was not cleared initially)
// we cannot assume the text at the end consists only of the composited text
this.textAreaState = this.textAreaState.fromTextArea(this.textArea);
this.lastCompositionEndTime = (new Date()).getTime();
if (!this.textareaIsShownAtCursor) {
......@@ -181,11 +183,15 @@ export class TextAreaHandler extends Disposable {
// --- Clipboard operations
this._register(this.textArea.onCut((e) => {
// Ensure we have the latest selection => ask all pending events to be sent
this.flushAnyAccumulatedEvents();
this._ensureClipboardGetsEditorSelection(e);
this.asyncTriggerCut.schedule();
}));
this._register(this.textArea.onCopy((e) => {
// Ensure we have the latest selection => ask all pending events to be sent
this.flushAnyAccumulatedEvents();
this._ensureClipboardGetsEditorSelection(e);
}));
......@@ -232,10 +238,6 @@ export class TextAreaHandler extends Disposable {
this._writePlaceholderAndSelectTextArea('selection changed');
}
public setCursorPosition(primary: IEditorPosition): void {
this.cursorPosition = primary;
}
// --- end event handlers
private setTextAreaState(reason:string, textAreaState:TextAreaState): void {
......
......@@ -15,6 +15,11 @@ export interface IClipboardEvent {
getTextData(): string;
}
export interface ICompositionEvent {
data: string;
locale: string;
}
export interface IKeyboardEventWrapper {
_actual: any;
equals(keybinding:number): boolean;
......@@ -26,8 +31,9 @@ export interface ITextAreaWrapper {
onKeyDown: Event<IKeyboardEventWrapper>;
onKeyUp: Event<IKeyboardEventWrapper>;
onKeyPress: Event<IKeyboardEventWrapper>;
onCompositionStart: Event<void>;
onCompositionEnd: Event<void>;
onCompositionStart: Event<ICompositionEvent>;
onCompositionUpdate: Event<ICompositionEvent>;
onCompositionEnd: Event<ICompositionEvent>;
onInput: Event<void>;
onCut: Event<IClipboardEvent>;
onCopy: Event<IClipboardEvent>;
......@@ -105,6 +111,21 @@ export abstract class TextAreaState {
public abstract fromText(text:string): TextAreaState;
public updateComposition(): ITypeData {
if (!this.previousState) {
// This is the EMPTY state
return {
text: '',
replaceCharCnt: 0
};
}
return {
text: this.value,
replaceCharCnt: this.previousState.selectionEnd - this.previousState.selectionStart
};
}
public abstract resetSelection(): TextAreaState;
public getSelectionStart(): number {
......@@ -378,7 +399,7 @@ export class NVDAPagedTextAreaState extends TextAreaState {
let posttext = model.getValueInRange(posttextRange, EndOfLinePreference.LF);
let text:string = null;
if (selectionStartPage <= selectionEndPage) {
if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
// take full selection
text = model.getValueInRange(selection, EndOfLinePreference.LF);
} else {
......@@ -391,6 +412,19 @@ export class NVDAPagedTextAreaState extends TextAreaState {
);
}
// 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 NVDAPagedTextAreaState(this, pretext + text + posttext, pretext.length, pretext.length + text.length, false);
}
......
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<style>
.container {
border-top: 1px solid #ccc;
padding-top: 5px;
clear: both;
margin-top: 30px;
}
.container .title {
margin-bottom: 10px;
}
.container button {
float: left;
}
.container textarea {
float: left;
width: 200px;
height: 100px;
margin-left: 50px;
}
.container .output {
float: left;
background: lightblue;
margin: 0;
margin-left: 50px;
}
.container .check {
float: left;
background: grey;
margin: 0;
margin-left: 50px;
}
.container .check.good {
background: lightgreen;
}
</style>
</head>
<body>
<h3>Detailed setup steps at https://github.com/Microsoft/vscode/wiki/IME-Test</h3>
<script src="../../../../loader.js"></script>
<script>
require.config({
baseUrl: '../../../../../../out'
});
require(['vs/editor/test/browser/controller/imeTester'], function(imeTester) {
// console.log('loaded', imeTester);
// imeTester.createTest();
});
</script>
</body>
</html>
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {TextAreaHandler} from 'vs/editor/common/controller/textAreaHandler';
import * as browser from 'vs/base/browser/browser';
import {TextAreaStrategy, ISimpleModel} from 'vs/editor/common/controller/textAreaState';
import {Range} from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import {TextAreaWrapper} from 'vs/editor/browser/controller/input/textAreaWrapper';
import {Position} from 'vs/editor/common/core/position';
// To run this test, open imeTester.html
class SingleLineTestModel implements ISimpleModel {
private _line:string;
private _eol:string;
constructor(line:string) {
this._line = line;
this._eol = '\n';
}
setText(text:string) {
this._line = text;
}
getLineMaxColumn(lineNumber:number): number {
return this._line.length + 1;
}
getEOL(): string {
return this._eol;
}
getValueInRange(range:editorCommon.IRange, eol:editorCommon.EndOfLinePreference): string {
return this._line.substring(range.startColumn - 1, range.endColumn - 1);
}
getModelLineContent(lineNumber:number): string {
return this._line;
}
getLineCount(): number {
return 1;
}
convertViewPositionToModelPosition(viewLineNumber:number, viewColumn:number): Position {
return new Position(viewLineNumber, viewColumn);
}
}
class TestView {
private _model: SingleLineTestModel;
constructor(model:SingleLineTestModel) {
this._model = model;
}
public paint(output:HTMLElement) {
let r = '';
for (let i = 1; i <= this._model.getLineCount(); i++) {
let content = this._model.getModelLineContent(i);
r += content + '<br/>';
}
output.innerHTML = r;
}
}
function doCreateTest(strategy:TextAreaStrategy, description:string, inputStr:string, expectedStr:string): HTMLElement {
let container = document.createElement('div');
container.className = 'container';
let title = document.createElement('div');
title.className = 'title';
title.innerHTML = TextAreaStrategy[strategy] + ' strategy: ' + description + '. Type <strong>' + inputStr + '</strong>';
container.appendChild(title);
let startBtn = document.createElement('button');
startBtn.innerHTML = 'Start';
container.appendChild(startBtn);
let input = document.createElement('textarea');
input.setAttribute('rows', '10');
input.setAttribute('cols', '40');
container.appendChild(input);
let textAreaWrapper = new TextAreaWrapper(input);
let model = new SingleLineTestModel('some text');
let handler = new TextAreaHandler(browser, strategy, textAreaWrapper, model, () => {});
input.onfocus = () => {
handler.setHasFocus(true);
};
input.onblur = () => {
handler.setHasFocus(false);
};
let output = document.createElement('pre');
output.className = 'output';
container.appendChild(output);
let check = document.createElement('pre');
check.className = 'check';
container.appendChild(check);
let br = document.createElement('br');
br.style.clear = 'both';
container.appendChild(br);
let view = new TestView(model);
let cursorOffset: number;
let cursorLength: number;
let updatePosition = (off:number, len:number) => {
cursorOffset = off;
cursorLength = len;
handler.setCursorSelections(new Range(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength), []);
handler.writePlaceholderAndSelectTextAreaSync();
};
let updateModelAndPosition = (text:string, off:number, len:number) => {
model.setText(text);
updatePosition(off, len);
view.paint(output);
let expected = 'some ' + expectedStr + ' text';
if (text === expected) {
check.innerHTML = '[GOOD]';
check.className = 'check good';
} else {
check.innerHTML = '[BAD]';
check.className = 'check bad';
}
check.innerHTML += expected;
};
handler.onType((e) => {
console.log('type text: ' + e.text + ', replaceCharCnt: ' + e.replaceCharCnt);
let text = model.getModelLineContent(1);
let preText = text.substring(0, cursorOffset - e.replaceCharCnt);
let postText = text.substring(cursorOffset + cursorLength);
let midText = e.text;
updateModelAndPosition(preText + midText + postText, (preText + midText).length, 0);
});
view.paint(output);
startBtn.onclick = function() {
updateModelAndPosition('some text', 5, 0);
input.focus();
};
return container;
}
const TESTS = [
{ description: 'Japanese IME 1', in: 'sennsei [Enter]', out: 'せんせい' },
{ description: 'Japanese IME 2', in: 'konnichiha [Enter]', out: 'こんいちは' },
{ description: 'Japanese IME 3', in: 'mikann [Enter]', out: 'みかん' },
{ description: 'Korean IME 1', in: 'gksrmf [Space]', out: '한글 ' },
{ description: 'Chinese IME 1', in: '.,', out: '。,' },
{ description: 'Chinese IME 2', in: 'ni [Space] hao [Space]', out: '你好' },
{ description: 'Chinese IME 3', in: 'hazni [Space]', out: '哈祝你' },
];
TESTS.forEach((t) => {
document.body.appendChild(doCreateTest(TextAreaStrategy.NVDA, t.description, t.in, t.out));
document.body.appendChild(doCreateTest(TextAreaStrategy.IENarrator, t.description, t.in, t.out));
});
......@@ -6,7 +6,7 @@
import Event, {Emitter} from 'vs/base/common/event';
import {Disposable} from 'vs/base/common/lifecycle';
import {IClipboardEvent, IKeyboardEventWrapper, ITextAreaWrapper} from 'vs/editor/common/controller/textAreaState';
import {IClipboardEvent, ICompositionEvent, IKeyboardEventWrapper, ITextAreaWrapper} from 'vs/editor/common/controller/textAreaState';
export class MockTextAreaWrapper extends Disposable implements ITextAreaWrapper {
......@@ -19,11 +19,14 @@ export class MockTextAreaWrapper extends Disposable implements ITextAreaWrapper
private _onKeyPress = this._register(new Emitter<IKeyboardEventWrapper>());
public onKeyPress: Event<IKeyboardEventWrapper> = this._onKeyPress.event;
private _onCompositionStart = this._register(new Emitter<void>());
public onCompositionStart: Event<void> = this._onCompositionStart.event;
private _onCompositionStart = this._register(new Emitter<ICompositionEvent>());
public onCompositionStart: Event<ICompositionEvent> = this._onCompositionStart.event;
private _onCompositionEnd = this._register(new Emitter<void>());
public onCompositionEnd: Event<void> = this._onCompositionEnd.event;
private _onCompositionUpdate = this._register(new Emitter<ICompositionEvent>());
public onCompositionUpdate: Event<ICompositionEvent> = this._onCompositionUpdate.event;
private _onCompositionEnd = this._register(new Emitter<ICompositionEvent>());
public onCompositionEnd: Event<ICompositionEvent> = this._onCompositionEnd.event;
private _onInput = this._register(new Emitter<void>());
public onInput: Event<void> = this._onInput.event;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册