Merge pull request #191010 from microsoft/merogge/text-area

synchronize xterm textarea on focus & up arrow
This commit is contained in:
Megan Rogge 2023-08-22 15:07:42 -07:00 committed by GitHub
commit 691b5edcf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -3,11 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal';
import type { Terminal, ITerminalAddon } from 'xterm'; import type { Terminal, ITerminalAddon } from 'xterm';
import { debounce } from 'vs/base/common/decorators';
import { addDisposableListener } from 'vs/base/browser/dom';
export interface ITextAreaData { export interface ITextAreaData {
content: string; content: string;
@ -16,11 +18,14 @@ export interface ITextAreaData {
export class TextAreaSyncAddon extends Disposable implements ITerminalAddon { export class TextAreaSyncAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined; private _terminal: Terminal | undefined;
private _onCursorMoveListener = this._register(new MutableDisposable()); private _listeners = this._register(new MutableDisposable<DisposableStore>());
private _currentCommand: string | undefined;
private _cursorX: number | undefined;
activate(terminal: Terminal): void { activate(terminal: Terminal): void {
this._terminal = terminal; this._terminal = terminal;
if (this._accessibilityService.isScreenReaderOptimized()) { if (this._accessibilityService.isScreenReaderOptimized()) {
this._onCursorMoveListener.value = this._terminal.onCursorMove(() => this._refreshTextArea()); this._registerSyncListeners();
} }
} }
@ -31,75 +36,78 @@ export class TextAreaSyncAddon extends Disposable implements ITerminalAddon {
) { ) {
super(); super();
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {
if (this._accessibilityService.isScreenReaderOptimized() && this._terminal) { if (this._accessibilityService.isScreenReaderOptimized()) {
this._refreshTextArea(); this._syncTextArea();
this._onCursorMoveListener.value = this._terminal.onCursorMove(() => this._refreshTextArea()); this._registerSyncListeners();
} else { } else {
this._onCursorMoveListener.clear(); this._listeners.clear();
} }
})); }));
} }
private _refreshTextArea(focusChanged?: boolean): void { private _registerSyncListeners(): void {
if (this._accessibilityService.isScreenReaderOptimized() && this._terminal?.textarea) {
this._listeners.value = new DisposableStore();
this._listeners.value.add(this._terminal.onCursorMove(() => this._syncTextArea()));
this._listeners.value.add(this._terminal.onData(() => this._syncTextArea()));
this._listeners.value.add(addDisposableListener(this._terminal.textarea, 'focus', () => this._syncTextArea()));
}
}
@debounce(50)
private _syncTextArea(): void {
this._logService.debug('TextAreaSyncAddon#syncTextArea');
const textArea = this._terminal?.textarea;
if (!textArea) {
this._logService.debug(`TextAreaSyncAddon#syncTextArea: no textarea`);
return;
}
this._updateCommandAndCursor();
if (this._currentCommand !== textArea.value) {
textArea.value = this._currentCommand || '';
this._logService.debug(`TextAreaSyncAddon#syncTextArea: text changed to "${this._currentCommand}"`);
} else if (!this._currentCommand) {
textArea.value = '';
this._logService.debug(`TextAreaSyncAddon#syncTextArea: text cleared`);
}
if (this._cursorX !== textArea.selectionStart) {
textArea.selectionStart = this._cursorX ?? 0;
textArea.selectionEnd = this._cursorX ?? 0;
this._logService.debug(`TextAreaSyncAddon#syncTextArea: selection start/end changed to ${this._cursorX}`);
}
}
private _updateCommandAndCursor(): void {
if (!this._terminal) { if (!this._terminal) {
return; return;
} }
this._logService.debug('TextAreaSyncAddon#refreshTextArea');
const commandCapability = this._capabilities.get(TerminalCapability.CommandDetection); const commandCapability = this._capabilities.get(TerminalCapability.CommandDetection);
const currentCommand = commandCapability?.currentCommand; const currentCommand = commandCapability?.currentCommand;
if (!currentCommand) { if (!currentCommand) {
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: no currentCommand`); this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: no current command`);
return; return;
} }
const buffer = this._terminal.buffer.active; const buffer = this._terminal.buffer.active;
const line = buffer.getLine(buffer.cursorY)?.translateToString(true); const line = buffer.getLine(buffer.cursorY)?.translateToString(true);
let commandStartX: number | undefined;
if (!line) { if (!line) {
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: no line`); this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: no line`);
return; return;
} }
let content: string | undefined; if (currentCommand.commandStartX !== undefined) {
if (currentCommand.commandStartX) {
// Left prompt // Left prompt
content = line.substring(currentCommand.commandStartX); this._currentCommand = line.substring(currentCommand.commandStartX);
commandStartX = currentCommand.commandStartX; this._cursorX = buffer.cursorX - currentCommand.commandStartX;
} else if (currentCommand.commandRightPromptStartX) { } else if (currentCommand.commandRightPromptStartX !== undefined) {
// Right prompt // Right prompt
content = line.substring(0, currentCommand.commandRightPromptStartX); this._currentCommand = line.substring(0, currentCommand.commandRightPromptStartX);
commandStartX = 0; this._cursorX = buffer.cursorX;
} else {
this._currentCommand = undefined;
this._cursorX = undefined;
this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: neither commandStartX nor commandRightPromptStartX`);
} }
if (!content) {
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: no content`);
return;
}
if (commandStartX === undefined) {
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: no commandStartX`);
return;
}
const textArea = this._terminal.textarea;
if (!textArea) {
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: no textarea`);
return;
}
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: content is "${content}"`);
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: textContent is "${textArea.textContent}"`);
if (focusChanged || content !== textArea.textContent) {
textArea.textContent = content;
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: textContent changed to "${content}"`);
}
const cursorX = buffer.cursorX - commandStartX;
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: cursorX is ${cursorX}`);
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: selectionStart is ${textArea.selectionStart}`);
if (focusChanged || cursorX !== textArea.selectionStart) {
textArea.selectionStart = cursorX;
textArea.selectionEnd = cursorX;
this._logService.debug(`TextAreaSyncAddon#refreshTextArea: selectionStart changed to ${cursorX}`);
}
// TODO: cursorY?
} }
} }