Path: blob/main/src/vs/workbench/api/browser/mainThreadEditor.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../base/common/event.js';6import { DisposableStore } from '../../../base/common/lifecycle.js';7import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';8import { RenderLineNumbersType, TextEditorCursorStyle, cursorStyleToString, EditorOption } from '../../../editor/common/config/editorOptions.js';9import { IRange, Range } from '../../../editor/common/core/range.js';10import { ISelection, Selection } from '../../../editor/common/core/selection.js';11import { IDecorationOptions, ScrollType } from '../../../editor/common/editorCommon.js';12import { ITextModel, ITextModelUpdateOptions } from '../../../editor/common/model.js';13import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js';14import { IModelService } from '../../../editor/common/services/model.js';15import { SnippetController2 } from '../../../editor/contrib/snippet/browser/snippetController2.js';16import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ISnippetOptions, ITextEditorConfigurationUpdate, TextEditorRevealType } from '../common/extHost.protocol.js';17import { IEditorPane } from '../../common/editor.js';18import { equals } from '../../../base/common/arrays.js';19import { CodeEditorStateFlag, EditorState } from '../../../editor/contrib/editorState/browser/editorState.js';20import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';21import { SnippetParser } from '../../../editor/contrib/snippet/browser/snippetParser.js';22import { MainThreadDocuments } from './mainThreadDocuments.js';23import { ISnippetEdit } from '../../../editor/contrib/snippet/browser/snippetSession.js';2425export interface IFocusTracker {26onGainedFocus(): void;27onLostFocus(): void;28}2930export class MainThreadTextEditorProperties {3132public static readFromEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): MainThreadTextEditorProperties {33const selections = MainThreadTextEditorProperties._readSelectionsFromCodeEditor(previousProperties, codeEditor);34const options = MainThreadTextEditorProperties._readOptionsFromCodeEditor(previousProperties, model, codeEditor);35const visibleRanges = MainThreadTextEditorProperties._readVisibleRangesFromCodeEditor(previousProperties, codeEditor);36return new MainThreadTextEditorProperties(selections, options, visibleRanges);37}3839private static _readSelectionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Selection[] {40let result: Selection[] | null = null;41if (codeEditor) {42result = codeEditor.getSelections();43}44if (!result && previousProperties) {45result = previousProperties.selections;46}47if (!result) {48result = [new Selection(1, 1, 1, 1)];49}50return result;51}5253private static _readOptionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): IResolvedTextEditorConfiguration {54if (model.isDisposed()) {55if (previousProperties) {56// shutdown time57return previousProperties.options;58} else {59throw new Error('No valid properties');60}61}6263let cursorStyle: TextEditorCursorStyle;64let lineNumbers: RenderLineNumbersType;65if (codeEditor) {66const options = codeEditor.getOptions();67const lineNumbersOpts = options.get(EditorOption.lineNumbers);68cursorStyle = options.get(EditorOption.cursorStyle);69lineNumbers = lineNumbersOpts.renderType;70} else if (previousProperties) {71cursorStyle = previousProperties.options.cursorStyle;72lineNumbers = previousProperties.options.lineNumbers;73} else {74cursorStyle = TextEditorCursorStyle.Line;75lineNumbers = RenderLineNumbersType.On;76}7778const modelOptions = model.getOptions();79return {80insertSpaces: modelOptions.insertSpaces,81tabSize: modelOptions.tabSize,82indentSize: modelOptions.indentSize,83originalIndentSize: modelOptions.originalIndentSize,84cursorStyle: cursorStyle,85lineNumbers: lineNumbers86};87}8889private static _readVisibleRangesFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Range[] {90if (codeEditor) {91return codeEditor.getVisibleRanges();92}93return [];94}9596constructor(97public readonly selections: Selection[],98public readonly options: IResolvedTextEditorConfiguration,99public readonly visibleRanges: Range[]100) {101}102103public generateDelta(oldProps: MainThreadTextEditorProperties | null, selectionChangeSource: string | null): IEditorPropertiesChangeData | null {104const delta: IEditorPropertiesChangeData = {105options: null,106selections: null,107visibleRanges: null108};109110if (!oldProps || !MainThreadTextEditorProperties._selectionsEqual(oldProps.selections, this.selections)) {111delta.selections = {112selections: this.selections,113source: selectionChangeSource ?? undefined,114};115}116117if (!oldProps || !MainThreadTextEditorProperties._optionsEqual(oldProps.options, this.options)) {118delta.options = this.options;119}120121if (!oldProps || !MainThreadTextEditorProperties._rangesEqual(oldProps.visibleRanges, this.visibleRanges)) {122delta.visibleRanges = this.visibleRanges;123}124125if (delta.selections || delta.options || delta.visibleRanges) {126// something changed127return delta;128}129// nothing changed130return null;131}132133private static _selectionsEqual(a: readonly Selection[], b: readonly Selection[]): boolean {134return equals(a, b, (aValue, bValue) => aValue.equalsSelection(bValue));135}136137private static _rangesEqual(a: readonly Range[], b: readonly Range[]): boolean {138return equals(a, b, (aValue, bValue) => aValue.equalsRange(bValue));139}140141private static _optionsEqual(a: IResolvedTextEditorConfiguration, b: IResolvedTextEditorConfiguration): boolean {142if (a && !b || !a && b) {143return false;144}145if (!a && !b) {146return true;147}148return (149a.tabSize === b.tabSize150&& a.indentSize === b.indentSize151&& a.insertSpaces === b.insertSpaces152&& a.cursorStyle === b.cursorStyle153&& a.lineNumbers === b.lineNumbers154);155}156}157158/**159* Text Editor that is permanently bound to the same model.160* It can be bound or not to a CodeEditor.161*/162export class MainThreadTextEditor {163164private readonly _id: string;165private readonly _model: ITextModel;166private readonly _mainThreadDocuments: MainThreadDocuments;167private readonly _modelService: IModelService;168private readonly _clipboardService: IClipboardService;169private readonly _modelListeners = new DisposableStore();170private _codeEditor: ICodeEditor | null;171private readonly _focusTracker: IFocusTracker;172private readonly _codeEditorListeners = new DisposableStore();173174private _properties: MainThreadTextEditorProperties | null;175private readonly _onPropertiesChanged: Emitter<IEditorPropertiesChangeData>;176177constructor(178id: string,179model: ITextModel,180codeEditor: ICodeEditor,181focusTracker: IFocusTracker,182mainThreadDocuments: MainThreadDocuments,183modelService: IModelService,184clipboardService: IClipboardService,185) {186this._id = id;187this._model = model;188this._codeEditor = null;189this._properties = null;190this._focusTracker = focusTracker;191this._mainThreadDocuments = mainThreadDocuments;192this._modelService = modelService;193this._clipboardService = clipboardService;194195this._onPropertiesChanged = new Emitter<IEditorPropertiesChangeData>();196197this._modelListeners.add(this._model.onDidChangeOptions((e) => {198this._updatePropertiesNow(null);199}));200201this.setCodeEditor(codeEditor);202this._updatePropertiesNow(null);203}204205public dispose(): void {206this._modelListeners.dispose();207this._codeEditor = null;208this._codeEditorListeners.dispose();209}210211private _updatePropertiesNow(selectionChangeSource: string | null): void {212this._setProperties(213MainThreadTextEditorProperties.readFromEditor(this._properties, this._model, this._codeEditor),214selectionChangeSource215);216}217218private _setProperties(newProperties: MainThreadTextEditorProperties, selectionChangeSource: string | null): void {219const delta = newProperties.generateDelta(this._properties, selectionChangeSource);220this._properties = newProperties;221if (delta) {222this._onPropertiesChanged.fire(delta);223}224}225226public getId(): string {227return this._id;228}229230public getModel(): ITextModel {231return this._model;232}233234public getCodeEditor(): ICodeEditor | null {235return this._codeEditor;236}237238public hasCodeEditor(codeEditor: ICodeEditor | null): boolean {239return (this._codeEditor === codeEditor);240}241242public setCodeEditor(codeEditor: ICodeEditor | null): void {243if (this.hasCodeEditor(codeEditor)) {244// Nothing to do...245return;246}247this._codeEditorListeners.clear();248249this._codeEditor = codeEditor;250if (this._codeEditor) {251252// Catch early the case that this code editor gets a different model set and disassociate from this model253this._codeEditorListeners.add(this._codeEditor.onDidChangeModel(() => {254this.setCodeEditor(null);255}));256257this._codeEditorListeners.add(this._codeEditor.onDidFocusEditorWidget(() => {258this._focusTracker.onGainedFocus();259}));260this._codeEditorListeners.add(this._codeEditor.onDidBlurEditorWidget(() => {261this._focusTracker.onLostFocus();262}));263264let nextSelectionChangeSource: string | null = null;265this._codeEditorListeners.add(this._mainThreadDocuments.onIsCaughtUpWithContentChanges((uri) => {266if (uri.toString() === this._model.uri.toString()) {267const selectionChangeSource = nextSelectionChangeSource;268nextSelectionChangeSource = null;269this._updatePropertiesNow(selectionChangeSource);270}271}));272273const isValidCodeEditor = () => {274// Due to event timings, it is possible that there is a model change event not yet delivered to us.275// > e.g. a model change event is emitted to a listener which then decides to update editor options276// > In this case the editor configuration change event reaches us first.277// So simply check that the model is still attached to this code editor278return (this._codeEditor && this._codeEditor.getModel() === this._model);279};280281const updateProperties = (selectionChangeSource: string | null) => {282// Some editor events get delivered faster than model content changes. This is283// problematic, as this leads to editor properties reaching the extension host284// too soon, before the model content change that was the root cause.285//286// If this case is identified, then let's update editor properties on the next model287// content change instead.288if (this._mainThreadDocuments.isCaughtUpWithContentChanges(this._model.uri)) {289nextSelectionChangeSource = null;290this._updatePropertiesNow(selectionChangeSource);291} else {292// update editor properties on the next model content change293nextSelectionChangeSource = selectionChangeSource;294}295};296297this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => {298// selection299if (!isValidCodeEditor()) {300return;301}302updateProperties(e.source);303}));304this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration((e) => {305// options306if (!isValidCodeEditor()) {307return;308}309updateProperties(null);310}));311this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => {312// visibleRanges313if (!isValidCodeEditor()) {314return;315}316updateProperties(null);317}));318this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => {319// visibleRanges320if (!isValidCodeEditor()) {321return;322}323updateProperties(null);324}));325this._updatePropertiesNow(null);326}327}328329public isVisible(): boolean {330return !!this._codeEditor;331}332333public getProperties(): MainThreadTextEditorProperties {334return this._properties!;335}336337public get onPropertiesChanged(): Event<IEditorPropertiesChangeData> {338return this._onPropertiesChanged.event;339}340341public setSelections(selections: ISelection[]): void {342if (this._codeEditor) {343this._codeEditor.setSelections(selections);344return;345}346347const newSelections = selections.map(Selection.liftSelection);348this._setProperties(349new MainThreadTextEditorProperties(newSelections, this._properties!.options, this._properties!.visibleRanges),350null351);352}353354private _setIndentConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {355const creationOpts = this._modelService.getCreationOptions(this._model.getLanguageId(), this._model.uri, this._model.isForSimpleWidget);356357if (newConfiguration.tabSize === 'auto' || newConfiguration.insertSpaces === 'auto') {358// one of the options was set to 'auto' => detect indentation359let insertSpaces = creationOpts.insertSpaces;360let tabSize = creationOpts.tabSize;361362if (newConfiguration.insertSpaces !== 'auto' && typeof newConfiguration.insertSpaces !== 'undefined') {363insertSpaces = newConfiguration.insertSpaces;364}365366if (newConfiguration.tabSize !== 'auto' && typeof newConfiguration.tabSize !== 'undefined') {367tabSize = newConfiguration.tabSize;368}369370this._model.detectIndentation(insertSpaces, tabSize);371return;372}373374const newOpts: ITextModelUpdateOptions = {};375if (typeof newConfiguration.insertSpaces !== 'undefined') {376newOpts.insertSpaces = newConfiguration.insertSpaces;377}378if (typeof newConfiguration.tabSize !== 'undefined') {379newOpts.tabSize = newConfiguration.tabSize;380}381if (typeof newConfiguration.indentSize !== 'undefined') {382newOpts.indentSize = newConfiguration.indentSize;383}384this._model.updateOptions(newOpts);385}386387public setConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {388this._setIndentConfiguration(newConfiguration);389390if (!this._codeEditor) {391return;392}393394if (newConfiguration.cursorStyle) {395const newCursorStyle = cursorStyleToString(newConfiguration.cursorStyle);396this._codeEditor.updateOptions({397cursorStyle: newCursorStyle398});399}400401if (typeof newConfiguration.lineNumbers !== 'undefined') {402let lineNumbers: 'on' | 'off' | 'relative' | 'interval';403switch (newConfiguration.lineNumbers) {404case RenderLineNumbersType.On:405lineNumbers = 'on';406break;407case RenderLineNumbersType.Relative:408lineNumbers = 'relative';409break;410case RenderLineNumbersType.Interval:411lineNumbers = 'interval';412break;413default:414lineNumbers = 'off';415}416this._codeEditor.updateOptions({417lineNumbers: lineNumbers418});419}420}421422public setDecorations(key: string, ranges: IDecorationOptions[]): void {423if (!this._codeEditor) {424return;425}426this._codeEditor.setDecorationsByType('exthost-api', key, ranges);427}428429public setDecorationsFast(key: string, _ranges: number[]): void {430if (!this._codeEditor) {431return;432}433const ranges: Range[] = [];434for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) {435ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]);436}437this._codeEditor.setDecorationsByTypeFast(key, ranges);438}439440public revealRange(range: IRange, revealType: TextEditorRevealType): void {441if (!this._codeEditor) {442return;443}444switch (revealType) {445case TextEditorRevealType.Default:446this._codeEditor.revealRange(range, ScrollType.Smooth);447break;448case TextEditorRevealType.InCenter:449this._codeEditor.revealRangeInCenter(range, ScrollType.Smooth);450break;451case TextEditorRevealType.InCenterIfOutsideViewport:452this._codeEditor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);453break;454case TextEditorRevealType.AtTop:455this._codeEditor.revealRangeAtTop(range, ScrollType.Smooth);456break;457default:458console.warn(`Unknown revealType: ${revealType}`);459break;460}461}462463public isFocused(): boolean {464if (this._codeEditor) {465return this._codeEditor.hasTextFocus();466}467return false;468}469470public matches(editor: IEditorPane): boolean {471if (!editor) {472return false;473}474return editor.getControl() === this._codeEditor;475}476477public applyEdits(versionIdCheck: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): boolean {478if (this._model.getVersionId() !== versionIdCheck) {479// throw new Error('Model has changed in the meantime!');480// model changed in the meantime481return false;482}483484if (!this._codeEditor) {485// console.warn('applyEdits on invisible editor');486return false;487}488489if (typeof opts.setEndOfLine !== 'undefined') {490this._model.pushEOL(opts.setEndOfLine);491}492493const transformedEdits = edits.map((edit): ISingleEditOperation => {494return {495range: Range.lift(edit.range),496text: edit.text,497forceMoveMarkers: edit.forceMoveMarkers498};499});500501if (opts.undoStopBefore) {502this._codeEditor.pushUndoStop();503}504this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits);505if (opts.undoStopAfter) {506this._codeEditor.pushUndoStop();507}508return true;509}510511async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions) {512513if (!this._codeEditor || !this._codeEditor.hasModel()) {514return false;515}516517// check if clipboard is required and only iff read it (async)518let clipboardText: string | undefined;519const needsTemplate = SnippetParser.guessNeedsClipboard(template);520if (needsTemplate) {521const state = new EditorState(this._codeEditor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);522clipboardText = await this._clipboardService.readText();523if (!state.validate(this._codeEditor)) {524return false;525}526}527528if (this._codeEditor.getModel().getVersionId() !== modelVersionId) {529return false;530}531532const snippetController = SnippetController2.get(this._codeEditor);533if (!snippetController) {534return false;535}536537this._codeEditor.focus();538539// make modifications as snippet edit540const edits: ISnippetEdit[] = ranges.map(range => ({ range: Range.lift(range), template }));541snippetController.apply(edits, {542overwriteBefore: 0, overwriteAfter: 0,543undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter,544adjustWhitespace: !opts.keepWhitespace,545clipboardText546});547548return true;549}550}551552553