Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ export const CopyOptions = {

export const PasteOptions = {
electronBugWorkaroundPasteEventHasFired: false,
electronBugWorkaroundPasteEventLock: false
};

interface InMemoryClipboardMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
white-space: pre-wrap;
}

.monaco-editor .ime-text-area {
.monaco-editor .ime-text-area,
.monaco-editor .native-edit-context-textarea {
min-width: 0;
min-height: 0;
margin: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class NativeEditContext extends AbstractEditContext {

// Text area used to handle paste events
public readonly domNode: FastDomNode<HTMLDivElement>;
private readonly _pasteTextArea: FastDomNode<HTMLTextAreaElement>;
private readonly _imeTextArea: FastDomNode<HTMLTextAreaElement>;
private readonly _editContext: EditContext;
private readonly _screenReaderSupport: ScreenReaderSupport;
Expand Down Expand Up @@ -85,6 +86,10 @@ export class NativeEditContext extends AbstractEditContext {
this.domNode = new FastDomNode(document.createElement('div'));
this.domNode.setClassName(`native-edit-context`);
this._imeTextArea = new FastDomNode(document.createElement('textarea'));
this._pasteTextArea = new FastDomNode(document.createElement('textarea'));
this._pasteTextArea.setClassName('native-edit-context-textarea');
this._pasteTextArea.setAttribute('tabindex', '-1');
this._pasteTextArea.setAttribute('aria-hidden', 'true');
this._imeTextArea.setClassName(`ime-text-area`);
this._imeTextArea.setAttribute('readonly', 'true');
this._imeTextArea.setAttribute('tabindex', '-1');
Expand All @@ -97,6 +102,7 @@ export class NativeEditContext extends AbstractEditContext {
this._updateDomAttributes();

overflowGuardContainer.appendChild(this.domNode);
overflowGuardContainer.appendChild(this._pasteTextArea);
overflowGuardContainer.appendChild(this._imeTextArea);
this._parent = overflowGuardContainer.domNode;

Expand Down Expand Up @@ -139,8 +145,9 @@ export class NativeEditContext extends AbstractEditContext {
this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 });
}
}));
this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => {
this._register(addDisposableListener(this._pasteTextArea.domNode, 'paste', (e) => {
this.logService.trace('NativeEditContext#paste');
this._screenReaderSupport.onPaste();
const pasteData = computePasteData(e, this._context, this.logService);
if (!pasteData) {
return;
Expand Down Expand Up @@ -217,9 +224,30 @@ export class NativeEditContext extends AbstractEditContext {
this.domNode.domNode.blur();
this.domNode.domNode.remove();
this._imeTextArea.domNode.remove();
this._pasteTextArea.domNode.remove();
super.dispose();
}

public executePaste(): boolean {
this._onWillPaste();
try {
// pause focus tracking because we don't want to react to focus/blur
// events while pasting since we move the focus to the textarea
this._focusTracker.pause();

// Since we can not call execCommand('paste') on a dom node with edit context set
// we added a hidden text area that receives the paste execution
this._pasteTextArea.focus();
const result = this._pasteTextArea.domNode.ownerDocument.execCommand('paste');
this._pasteTextArea.domNode.textContent = '';
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a textarea element, the .value property should be used instead of .textContent to clear its content. While both may work in some cases, .value is the correct DOM property for form input elements like textarea. This ensures proper clearing of the textarea's value and maintains consistency with standard DOM practices.

Suggested change
this._pasteTextArea.domNode.textContent = '';
this._pasteTextArea.domNode.value = '';

Copilot uses AI. Check for mistakes.
this.domNode.focus();
return result;
} finally {
// resume focus tracking
this._focusTracker.resume();
}
}

public setAriaOptions(options: IEditorAriaOptions): void {
this._screenReaderSupport.setAriaOptions(options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export class RichScreenReaderContent extends Disposable implements IScreenReader
this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize);
}

public onPaste(): void {
this._setIgnoreSelectionChangeTime('onPaste');
}

public onWillCut(): void {
this._setIgnoreSelectionChangeTime('onCut');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class SimpleScreenReaderContent extends Disposable implements IScreenRead
this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize);
}

public onPaste(): void {
this._setIgnoreSelectionChangeTime('onPaste');
}

public onWillCut(): void {
this._setIgnoreSelectionChangeTime('onCut');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class ScreenReaderSupport extends Disposable {
this._updateDomAttributes();
}

public onPaste(): void {
this._state.value?.onPaste();
}

public onWillPaste(): void {
this._state.value?.onWillPaste();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export interface IScreenReaderContent {

dispose(): void;

/**
* Handle screen reader content on pasting the content
*/
onPaste(): void;

/**
* Handle screen reader content before cutting the content
*/
Expand Down
46 changes: 15 additions & 31 deletions src/vs/editor/contrib/clipboard/browser/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as browser from '../../../../base/browser/browser.js';
import { getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js';
import { getActiveDocument } from '../../../../base/browser/dom.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import * as platform from '../../../../base/common/platform.js';
import * as nls from '../../../../nls.js';
Expand Down Expand Up @@ -308,41 +308,25 @@ if (PasteAction) {
const focusedEditor = codeEditorService.getFocusedCodeEditor();
if (focusedEditor && focusedEditor.hasModel() && focusedEditor.hasTextFocus()) {
// execCommand(paste) does not work with edit context
const editContextEnabled = focusedEditor.getOption(EditorOption.effectiveEditContext);
if (editContextEnabled) {
let result: boolean;
const editContext = focusedEditor.getOption(EditorOption.editContext);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use EditorOption.effectiveEditContext instead of EditorOption.editContext. The effectiveEditContext is a computed option that checks both whether edit context is enabled in settings AND whether the environment actually supports it (via env.editContextSupported). Using just editContext could cause issues in environments where edit context is not supported. Other parts of this file (lines 253 and 287) correctly use effectiveEditContext.

Suggested change
const editContext = focusedEditor.getOption(EditorOption.editContext);
const editContext = focusedEditor.getOption(EditorOption.effectiveEditContext);

Copilot uses AI. Check for mistakes.
if (editContext) {
const nativeEditContext = NativeEditContextRegistry.get(focusedEditor.getId());
if (nativeEditContext) {
nativeEditContext.onWillPaste();
}
}

logService.trace('registerExecCommandImpl (before triggerPaste)');
PasteOptions.electronBugWorkaroundPasteEventHasFired = false;
logService.trace('(before triggerPaste) PasteOptions.electronBugWorkaroundPasteEventHasFired : ', PasteOptions.electronBugWorkaroundPasteEventHasFired);
const triggerPaste = clipboardService.triggerPaste(getActiveWindow().vscodeWindowId);
if (triggerPaste) {
logService.trace('registerExecCommandImpl (triggerPaste defined)');
PasteOptions.electronBugWorkaroundPasteEventLock = false;
return triggerPaste.then(async () => {
logService.trace('(triggerPaste) PasteOptions.electronBugWorkaroundPasteEventHasFired : ', PasteOptions.electronBugWorkaroundPasteEventHasFired);
PasteOptions.electronBugWorkaroundPasteEventHasFired = false;
result = nativeEditContext.executePaste();
if (PasteOptions.electronBugWorkaroundPasteEventHasFired === false) {
// Ensure this doesn't run twice, what appears to be happening is
// triggerPasteis called once but it's handler is called multiple times
// when it reproduces
logService.trace('(triggerPaste) PasteOptions.electronBugWorkaroundPasteEventLock : ', PasteOptions.electronBugWorkaroundPasteEventLock);
if (PasteOptions.electronBugWorkaroundPasteEventLock === true) {
return;
}
PasteOptions.electronBugWorkaroundPasteEventLock = true;
return pasteWithNavigatorAPI(focusedEditor, clipboardService, logService);
}
logService.trace('registerExecCommandImpl (after triggerPaste)');
return CopyPasteController.get(focusedEditor)?.finishedPaste() ?? Promise.resolve();
});
} else {
result = false;
}
} else {
logService.trace('registerExecCommandImpl (triggerPaste undefined)');
result = focusedEditor.getContainerDomNode().ownerDocument.execCommand('paste');
}
if (platform.isWeb) {
if (result) {
return CopyPasteController.get(focusedEditor)?.finishedPaste() ?? Promise.resolve();
} else if (platform.isWeb) {
logService.trace('registerExecCommandImpl (Paste handling on web)');
// Use the clipboard service if document.execCommand('paste') was not successful
return pasteWithNavigatorAPI(focusedEditor, clipboardService, logService);
Expand All @@ -356,8 +340,8 @@ if (PasteAction) {
PasteAction.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: unknown) => {
const logService = accessor.get(ILogService);
logService.trace('registerExecCommandImpl (addImplementation generic-dom for : paste)');
const triggerPaste = accessor.get(IClipboardService).triggerPaste(getActiveWindow().vscodeWindowId);
return triggerPaste ?? false;
getActiveDocument().execCommand('paste');
return true;
});
}

Expand Down
Loading