UNPKG

monaco-editor

Version:
537 lines (534 loc) • 27.9 kB
import './nativeEditContext.css'; import { isFirefox } from '../../../../../base/browser/browser.js'; import { getWindow, addDisposableListener, getActiveElement, getWindowId } from '../../../../../base/browser/dom.js'; import { FastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; import { FocusTracker, editContextAddDisposableListener } from './nativeEditContextUtils.js'; import { ScreenReaderSupport } from './screenReaderSupport.js'; import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; import { Position } from '../../../../common/core/position.js'; import '../../../../common/core/text/positionToOffset.js'; import { EditContext } from './editContextFactory.js'; import { NativeEditContextRegistry } from './nativeEditContextRegistry.js'; import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js'; import { IME } from '../../../../../base/common/ime.js'; import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { inputLatency } from '../../../../../base/browser/performance.js'; import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffsetImpl.js'; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (undefined && undefined.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; // Corresponds to classes in nativeEditContext.css var CompositionClassName; (function (CompositionClassName) { CompositionClassName["NONE"] = "edit-context-composition-none"; CompositionClassName["SECONDARY"] = "edit-context-composition-secondary"; CompositionClassName["PRIMARY"] = "edit-context-composition-primary"; })(CompositionClassName || (CompositionClassName = {})); let NativeEditContext = class NativeEditContext extends AbstractEditContext { constructor(ownerID, context, overflowGuardContainer, _viewController, _visibleRangeProvider, instantiationService, logService) { super(context); this._viewController = _viewController; this._visibleRangeProvider = _visibleRangeProvider; this.logService = logService; this._previousEditContextSelection = new OffsetRange(0, 0); this._previousEditContextText = ''; this._editContextPrimarySelection = new Selection(1, 1, 1, 1); this._parentBounds = null; this._decorations = []; this._primarySelection = new Selection(1, 1, 1, 1); this._targetWindowId = -1; this._scrollTop = 0; this._scrollLeft = 0; this._linesVisibleRanges = null; this.domNode = new FastDomNode(document.createElement('div')); this.domNode.setClassName(`native-edit-context`); this._imeTextArea = new FastDomNode(document.createElement('textarea')); this._imeTextArea.setClassName(`ime-text-area`); this._imeTextArea.setAttribute('readonly', 'true'); this._imeTextArea.setAttribute('tabindex', '-1'); this._imeTextArea.setAttribute('aria-hidden', 'true'); this.domNode.setAttribute('autocorrect', 'off'); this.domNode.setAttribute('autocapitalize', 'off'); this.domNode.setAttribute('autocomplete', 'off'); this.domNode.setAttribute('spellcheck', 'false'); this._updateDomAttributes(); overflowGuardContainer.appendChild(this.domNode); overflowGuardContainer.appendChild(this._imeTextArea); this._parent = overflowGuardContainer.domNode; this._focusTracker = this._register(new FocusTracker(logService, this.domNode.domNode, (newFocusValue) => { logService.trace('NativeEditContext#handleFocusChange : ', newFocusValue); this._screenReaderSupport.handleFocusChange(newFocusValue); this._context.viewModel.setHasFocus(newFocusValue); })); const window = getWindow(this.domNode.domNode); this._editContext = EditContext.create(window); this.setEditContextOnDomNode(); this._screenReaderSupport = this._register(instantiationService.createInstance(ScreenReaderSupport, this.domNode, context, this._viewController)); this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => { this.logService.trace('NativeEditContext#copy'); // !!!!! // This is a workaround for what we think is an Electron bug where // execCommand('copy') does not always work (it does not fire a clipboard event) // !!!!! // We signal that we have executed a copy command CopyOptions.electronBugWorkaroundCopyEventHasFired = true; const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._context, this.logService, isFirefox); this._onWillCopy.fire(copyEvent); if (copyEvent.isHandled) { return; } copyEvent.ensureClipboardGetsEditorData(); })); this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => { this.logService.trace('NativeEditContext#cut'); const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._context, this.logService, isFirefox); this._onWillCut.fire(cutEvent); if (cutEvent.isHandled) { return; } // 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._screenReaderSupport.onWillCut(); cutEvent.ensureClipboardGetsEditorData(); this.logService.trace('NativeEditContext#cut (before viewController.cut)'); this._viewController.cut(); })); this._register(addDisposableListener(this.domNode.domNode, 'selectionchange', () => { inputLatency.onSelectionChange(); })); this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { inputLatency.onBeforeInput(); if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') { this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } })); this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => { this.logService.trace('NativeEditContext#paste'); const pasteEvent = createClipboardPasteEvent(e); this._onWillPaste.fire(pasteEvent); if (pasteEvent.isHandled) { e.preventDefault(); return; } e.preventDefault(); if (!e.clipboardData) { return; } this.logService.trace('NativeEditContext#paste with id : ', pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length); if (!pasteEvent.text) { return; } let pasteOnNewLine = false; let multicursorText = null; let mode = null; if (pasteEvent.metadata) { const options = this._context.configuration.options; const emptySelectionClipboard = options.get(45 /* EditorOption.emptySelectionClipboard */); pasteOnNewLine = emptySelectionClipboard && !!pasteEvent.metadata.isFromEmptySelection; multicursorText = typeof pasteEvent.metadata.multicursorText !== 'undefined' ? pasteEvent.metadata.multicursorText : null; mode = pasteEvent.metadata.mode; } this.logService.trace('NativeEditContext#paste (before viewController.paste)'); this._viewController.paste(pasteEvent.text, pasteOnNewLine, multicursorText, mode); })); // Edit context events this._register(editContextAddDisposableListener(this._editContext, 'textformatupdate', (e) => this._handleTextFormatUpdate(e))); this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e))); let highSurrogateCharacter; this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { inputLatency.onInput(); const text = e.text; if (text.length === 1) { const charCode = text.charCodeAt(0); if (isHighSurrogate(charCode)) { highSurrogateCharacter = text; return; } if (isLowSurrogate(charCode) && highSurrogateCharacter) { const textUpdateEvent = { text: highSurrogateCharacter + text, selectionEnd: e.selectionEnd, selectionStart: e.selectionStart, updateRangeStart: e.updateRangeStart - 1, updateRangeEnd: e.updateRangeEnd - 1 }; highSurrogateCharacter = undefined; this._emitTypeEvent(this._viewController, textUpdateEvent); return; } } this._emitTypeEvent(this._viewController, e); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => { this._updateEditContext(); // Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors this._viewController.compositionStart(); // Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionStart(); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => { this._updateEditContext(); // Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors this._viewController.compositionEnd(); // Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionEnd(); })); let reenableTracking = false; this._register(IME.onDidChange(() => { if (IME.enabled && reenableTracking) { this._focusTracker.resume(); this.domNode.focus(); reenableTracking = false; } if (!IME.enabled && this.isFocused()) { this._focusTracker.pause(); this._imeTextArea.focus(); reenableTracking = true; } })); this._register(NativeEditContextRegistry.register(ownerID, this)); this._register(context.viewModel.model.onDidChangeContent((e) => { let doChange = false; for (const change of e.changes) { if (change.range.startLineNumber <= this._editContextPrimarySelection.endLineNumber && change.range.endLineNumber >= this._editContextPrimarySelection.startLineNumber) { doChange = true; break; } } if (doChange) { this._updateEditContext(); } })); } // --- Public methods --- dispose() { // Force blue the dom node so can write in pane with no native edit context after disposal this.domNode.domNode.editContext = undefined; this.domNode.domNode.blur(); this.domNode.domNode.remove(); this._imeTextArea.domNode.remove(); super.dispose(); } setAriaOptions(options) { this._screenReaderSupport.setAriaOptions(options); } /* Last rendered data needed for correct hit-testing and determining the mouse position. * Without this, the selection will blink as incorrect mouse position is calculated */ getLastRenderData() { return this._primarySelection.getPosition(); } onBeforeRender(viewportData) { // We need to read the position of the container dom node // It is best to do this before we begin touching the DOM at all // Because the sync layout will be fast if we do it here this._parentBounds = this._parent.getBoundingClientRect(); } prepareRender(ctx) { this._screenReaderSupport.prepareRender(ctx); this._updateSelectionAndControlBoundsData(ctx); } render(ctx) { this._screenReaderSupport.render(ctx); this._updateSelectionAndControlBounds(); } onCursorStateChanged(e) { this._primarySelection = e.modelSelections[0] ?? new Selection(1, 1, 1, 1); this._screenReaderSupport.onCursorStateChanged(e); this._updateEditContext(); return true; } onConfigurationChanged(e) { this._screenReaderSupport.onConfigurationChanged(e); this._updateDomAttributes(); return true; } onDecorationsChanged(e) { // true for inline decorations that can end up relayouting text return true; } onFlushed(e) { return true; } onLinesChanged(e) { return true; } onLinesDeleted(e) { return true; } onLinesInserted(e) { return true; } onScrollChanged(e) { this._scrollLeft = e.scrollLeft; this._scrollTop = e.scrollTop; return true; } onZonesChanged(e) { return true; } handleWillPaste() { this.logService.trace('NativeEditContext#handleWillPaste'); this._prepareScreenReaderForPaste(); } _prepareScreenReaderForPaste() { this._screenReaderSupport.onWillPaste(); } handleWillCopy() { this.logService.trace('NativeEditContext#handleWillCopy'); this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement()); } writeScreenReaderContent() { this._screenReaderSupport.writeScreenReaderContent(); } isFocused() { return this._focusTracker.isFocused; } focus() { this._focusTracker.focus(); // If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus this.refreshFocusState(); } refreshFocusState() { this._focusTracker.refreshFocusState(); } // TODO: added as a workaround fix for https://github.com/microsoft/vscode/issues/229825 // When this issue will be fixed the following should be removed. setEditContextOnDomNode() { const targetWindow = getWindow(this.domNode.domNode); const targetWindowId = getWindowId(targetWindow); if (this._targetWindowId !== targetWindowId) { this.domNode.domNode.editContext = this._editContext; this._targetWindowId = targetWindowId; } } // --- Private methods --- _onKeyUp(e) { inputLatency.onKeyUp(); this._viewController.emitKeyUp(new StandardKeyboardEvent(e)); } _onKeyDown(e) { inputLatency.onKeyDown(); const standardKeyboardEvent = new StandardKeyboardEvent(e); // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further if (standardKeyboardEvent.keyCode === 114 /* KeyCode.KEY_IN_COMPOSITION */) { standardKeyboardEvent.stopPropagation(); } this._viewController.emitKeyDown(standardKeyboardEvent); } _updateDomAttributes() { const options = this._context.configuration.options; this.domNode.domNode.setAttribute('tabindex', String(options.get(140 /* EditorOption.tabIndex */))); } _updateEditContext() { const editContextState = this._getNewEditContextState(); if (!editContextState) { return; } const newText = editContextState.text ?? ' '; if (newText !== this._previousEditContextText) { this._editContext.updateText(0, this._previousEditContextText.length, newText); this._previousEditContextText = newText; } if (editContextState.selectionStartOffset !== this._previousEditContextSelection.start || editContextState.selectionEndOffset !== this._previousEditContextSelection.endExclusive) { this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset); } this._editContextPrimarySelection = editContextState.editContextPrimarySelection; this._previousEditContextSelection = new OffsetRange(editContextState.selectionStartOffset, editContextState.selectionEndOffset); } _emitTypeEvent(viewController, e) { if (!this._editContext) { return; } const selectionEndOffset = this._previousEditContextSelection.endExclusive; const selectionStartOffset = this._previousEditContextSelection.start; this._previousEditContextSelection = new OffsetRange(e.selectionStart, e.selectionEnd); let replaceNextCharCnt = 0; let replacePrevCharCnt = 0; if (e.updateRangeEnd > selectionEndOffset) { replaceNextCharCnt = e.updateRangeEnd - selectionEndOffset; } if (e.updateRangeStart < selectionStartOffset) { replacePrevCharCnt = selectionStartOffset - e.updateRangeStart; } let text = ''; if (selectionStartOffset < e.updateRangeStart) { text += this._editContext.text.substring(selectionStartOffset, e.updateRangeStart); } text += e.text; if (selectionEndOffset > e.updateRangeEnd) { text += this._editContext.text.substring(e.updateRangeEnd, selectionEndOffset); } let positionDelta = 0; if (e.selectionStart === e.selectionEnd && selectionStartOffset === selectionEndOffset) { positionDelta = e.selectionStart - (e.updateRangeStart + e.text.length); } const typeInput = { text, replacePrevCharCnt, replaceNextCharCnt, positionDelta }; this._onType(viewController, typeInput); } _onType(viewController, typeInput) { if (typeInput.replacePrevCharCnt || typeInput.replaceNextCharCnt || typeInput.positionDelta) { viewController.compositionType(typeInput.text, typeInput.replacePrevCharCnt, typeInput.replaceNextCharCnt, typeInput.positionDelta); } else { viewController.type(typeInput.text); } } _getNewEditContextState() { const editContextPrimarySelection = this._primarySelection; const model = this._context.viewModel.model; if (!model.isValidRange(editContextPrimarySelection)) { return; } const primarySelectionStartLine = editContextPrimarySelection.startLineNumber; const primarySelectionEndLine = editContextPrimarySelection.endLineNumber; const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine); const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber); const text = model.getValueInRange(rangeOfText, 0 /* EndOfLinePreference.TextDefined */); const selectionStartOffset = editContextPrimarySelection.startColumn - 1; const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber; return { text, selectionStartOffset, selectionEndOffset, editContextPrimarySelection }; } _editContextStartPosition() { return new Position(this._editContextPrimarySelection.startLineNumber, 1); } _handleTextFormatUpdate(e) { if (!this._editContext) { return; } const formats = e.getTextFormats(); const editContextStartPosition = this._editContextStartPosition(); const decorations = []; formats.forEach(f => { const textModel = this._context.viewModel.model; const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition); const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart); const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd); const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration); const thickness = f.underlineThickness.toLowerCase(); let decorationClassName = CompositionClassName.NONE; switch (thickness) { case 'thin': decorationClassName = CompositionClassName.SECONDARY; break; case 'thick': decorationClassName = CompositionClassName.PRIMARY; break; } decorations.push({ range: decorationRange, options: { description: 'textFormatDecoration', inlineClassName: decorationClassName, } }); }); this._decorations = this._context.viewModel.model.deltaDecorations(this._decorations, decorations); } _updateSelectionAndControlBoundsData(ctx) { const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); if (this._primarySelection.isEmpty()) { const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition()); this._linesVisibleRanges = linesVisibleRanges; } else { this._linesVisibleRanges = null; } } _updateSelectionAndControlBounds() { const options = this._context.configuration.options; const contentLeft = options.get(165 /* EditorOption.layoutInfo */).contentLeft; const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber); const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); // !!! Make sure this doesn't force an extra layout // !!! by using the cached parent bounds read in onBeforeRender const parentBounds = this._parentBounds; const top = parentBounds.top + verticalOffsetStart - this._scrollTop; const height = verticalOffsetEnd - verticalOffsetStart; let left = parentBounds.left + contentLeft - this._scrollLeft; let width; if (this._primarySelection.isEmpty()) { if (this._linesVisibleRanges) { left += this._linesVisibleRanges.left; } width = 0; } else { width = parentBounds.width - contentLeft; } const selectionBounds = new DOMRect(left, top, width, height); this._editContext.updateSelectionBounds(selectionBounds); this._editContext.updateControlBounds(selectionBounds); } _updateCharacterBounds(e) { const options = this._context.configuration.options; const typicalHalfWidthCharacterWidth = options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth; const contentLeft = options.get(165 /* EditorOption.layoutInfo */).contentLeft; const parentBounds = this._parentBounds; const characterBounds = []; const offsetTransformer = new PositionOffsetTransformer(this._editContext.text); for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) { const editContextStartPosition = offsetTransformer.getPosition(offset); const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1; const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column); const characterEndPosition = characterStartPosition.delta(0, 1); const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition); const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange); const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? []; const lineNumber = characterViewRange.startLineNumber; const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber); const top = parentBounds.top + characterVerticalOffset - this._scrollTop; let left = 0; let width = typicalHalfWidthCharacterWidth; if (characterLinesVisibleRanges.length > 0) { for (const visibleRange of characterLinesVisibleRanges[0].ranges) { left = visibleRange.left; width = visibleRange.width; break; } } const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber); characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight)); } this._editContext.updateCharacterBounds(e.rangeStart, characterBounds); } }; NativeEditContext = __decorate([ __param(5, IInstantiationService), __param(6, ILogService) ], NativeEditContext); export { NativeEditContext };