diff --git a/src/vs/base/browser/compositionEvent.ts b/src/vs/base/browser/compositionEvent.ts new file mode 100644 index 0000000000000..a75749721f8fd --- /dev/null +++ b/src/vs/base/browser/compositionEvent.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export interface ICompositionEvent { + data: string; + locale: string; +} \ No newline at end of file diff --git a/src/vs/editor/browser/controller/keyboardHandler.ts b/src/vs/editor/browser/controller/keyboardHandler.ts index dc0f6434a09e1..cc4138df46e66 100644 --- a/src/vs/editor/browser/controller/keyboardHandler.ts +++ b/src/vs/editor/browser/controller/keyboardHandler.ts @@ -9,6 +9,7 @@ import {Disposable, IDisposable, dispose} 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 {ICompositionEvent} from 'vs/base/browser/compositionEvent'; import {StyleMutator} from 'vs/base/browser/styleMutator'; import {GlobalScreenReaderNVDA} from 'vs/editor/common/config/commonEditorConfig'; import {TextAreaHandler} from 'vs/editor/common/controller/textAreaHandler'; @@ -104,11 +105,14 @@ class TextAreaWrapper extends Disposable implements ITextAreaWrapper { private _onKeyPress = this._register(new Emitter()); public onKeyPress: Event = this._onKeyPress.event; - private _onCompositionStart = this._register(new Emitter()); - public onCompositionStart: Event = this._onCompositionStart.event; + private _onCompositionStart = this._register(new Emitter()); + public onCompositionStart: Event = this._onCompositionStart.event; - private _onCompositionEnd = this._register(new Emitter()); - public onCompositionEnd: Event = this._onCompositionEnd.event; + private _onCompositionUpdate = this._register(new Emitter()); + public onCompositionUpdate: Event = this._onCompositionUpdate.event; + + private _onCompositionEnd = this._register(new Emitter()); + public onCompositionEnd: Event = this._onCompositionEnd.event; private _onInput = this._register(new Emitter()); public onInput: Event = this._onInput.event; @@ -129,8 +133,9 @@ class TextAreaWrapper extends Disposable implements ITextAreaWrapper { 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, '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)))); @@ -203,6 +208,8 @@ 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(); @@ -241,11 +248,11 @@ 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); + this.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); + if (this.visibleRange) { + StyleMutator.setTop(this.textArea.actual, this.visibleRange.top); + StyleMutator.setLeft(this.textArea.actual, this.contentLeft + this.visibleRange.left - this.scrollLeft); } if (browser.isIE11orEarlier) { @@ -256,12 +263,25 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable { StyleMutator.setHeight(this.textArea.actual, this.context.configuration.editor.lineHeight); dom.addClass(this.viewHelper.viewDomNode, 'ime-input'); })); + + this._toDispose.push(this.textAreaHandler.onCompositionUpdate((e) => { + + // adjust width by its size + let canvasElem = document.createElement('canvas'); + let context = canvasElem.getContext('2d'); + context.font = window.getComputedStyle(this.textArea.actual).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()); @@ -302,8 +322,13 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable { return false; } + 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; } diff --git a/src/vs/editor/common/controller/textAreaHandler.ts b/src/vs/editor/common/controller/textAreaHandler.ts index e79ba12a90e03..91f8329819929 100644 --- a/src/vs/editor/common/controller/textAreaHandler.ts +++ b/src/vs/editor/common/controller/textAreaHandler.ts @@ -12,6 +12,7 @@ import {IClipboardEvent, IKeyboardEventWrapper, ISimpleModel, ITextAreaWrapper, import {Position} from 'vs/editor/common/core/position'; import {Range} from 'vs/editor/common/core/range'; import {EndOfLinePreference, IEditorPosition, IEditorRange} from 'vs/editor/common/editorCommon'; +import {ICompositionEvent} from 'vs/base/browser/compositionEvent'; enum ReadFromTextArea { Type, @@ -56,8 +57,11 @@ export class TextAreaHandler extends Disposable { private _onCompositionStart = this._register(new Emitter()); public onCompositionStart: Event = this._onCompositionStart.event; - private _onCompositionEnd = this._register(new Emitter()); - public onCompositionEnd: Event = this._onCompositionEnd.event; + private _onCompositionUpdate = this._register(new Emitter()); + public onCompositionUpdate: Event = this._onCompositionUpdate.event; + + private _onCompositionEnd = this._register(new Emitter()); + public onCompositionEnd: Event = this._onCompositionEnd.event; private Browser:IBrowser; private textArea:ITextAreaWrapper; @@ -108,8 +112,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; } @@ -117,7 +121,7 @@ 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) { this.setTextAreaState('compositionstart', this.textAreaState.toEmpty()); @@ -136,13 +140,26 @@ export class TextAreaHandler extends Disposable { showAtLineNumber = this.cursorPosition.lineNumber; showAtColumn = this.cursorPosition.column; } - this._onCompositionStart.fire({ showAtLineNumber: showAtLineNumber, showAtColumn: showAtColumn }); })); + this._register(this.textArea.onCompositionUpdate((e) => { + this.textAreaState = this.textAreaState.fromText(e.data); + let typeInput = this.textAreaState.updateComposition(); + if (this._nextCommand === ReadFromTextArea.Type) { + if (typeInput.text !== '') { + this._onType.fire(typeInput); + } + } else { + this.executePaste(typeInput.text); + this._nextCommand = ReadFromTextArea.Type; + } + this._onCompositionUpdate.fire(e); + })); + let readFromTextArea = () => { this.textAreaState = this.textAreaState.fromTextArea(this.textArea); let typeInput = this.textAreaState.deduceInput(); @@ -157,9 +174,11 @@ 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); this.lastCompositionEndTime = (new Date()).getTime(); if (!this.textareaIsShownAtCursor) { @@ -238,6 +257,10 @@ export class TextAreaHandler extends Disposable { this._writePlaceholderAndSelectTextArea('selection changed'); } + public getCursorPosition(): IEditorPosition { + return this.cursorPosition; + } + public setCursorPosition(primary: IEditorPosition): void { this.cursorPosition = primary; } diff --git a/src/vs/editor/common/controller/textAreaState.ts b/src/vs/editor/common/controller/textAreaState.ts index 3a404426832fa..be7794ed0f068 100644 --- a/src/vs/editor/common/controller/textAreaState.ts +++ b/src/vs/editor/common/controller/textAreaState.ts @@ -8,6 +8,7 @@ import Event from 'vs/base/common/event'; import {commonPrefixLength, commonSuffixLength} from 'vs/base/common/strings'; import {Range} from 'vs/editor/common/core/range'; import {EndOfLinePreference, IEditorPosition, IEditorRange, IRange} from 'vs/editor/common/editorCommon'; +import {ICompositionEvent} from 'vs/base/browser/compositionEvent'; export interface IClipboardEvent { canUseTextData(): boolean; @@ -26,8 +27,9 @@ export interface ITextAreaWrapper { onKeyDown: Event; onKeyUp: Event; onKeyPress: Event; - onCompositionStart: Event; - onCompositionEnd: Event; + onCompositionStart: Event; + onCompositionUpdate: Event; + onCompositionEnd: Event; onInput: Event; onCut: Event; onCopy: Event; @@ -105,6 +107,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 { diff --git a/src/vs/editor/test/common/mocks/mockTextAreaWrapper.ts b/src/vs/editor/test/common/mocks/mockTextAreaWrapper.ts index 11bf927d1eab8..ecb9ca85ff979 100644 --- a/src/vs/editor/test/common/mocks/mockTextAreaWrapper.ts +++ b/src/vs/editor/test/common/mocks/mockTextAreaWrapper.ts @@ -7,6 +7,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 {ICompositionEvent} from 'vs/base/browser/compositionEvent'; export class MockTextAreaWrapper extends Disposable implements ITextAreaWrapper { @@ -19,11 +20,14 @@ export class MockTextAreaWrapper extends Disposable implements ITextAreaWrapper private _onKeyPress = this._register(new Emitter()); public onKeyPress: Event = this._onKeyPress.event; - private _onCompositionStart = this._register(new Emitter()); - public onCompositionStart: Event = this._onCompositionStart.event; + private _onCompositionStart = this._register(new Emitter()); + public onCompositionStart: Event = this._onCompositionStart.event; - private _onCompositionEnd = this._register(new Emitter()); - public onCompositionEnd: Event = this._onCompositionEnd.event; + private _onCompositionUpdate = this._register(new Emitter()); + public onCompositionUpdate: Event = this._onCompositionUpdate.event; + + private _onCompositionEnd = this._register(new Emitter()); + public onCompositionEnd: Event = this._onCompositionEnd.event; private _onInput = this._register(new Emitter()); public onInput: Event = this._onInput.event;