Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts
4780 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 { createStyleSheetFromObservable } from '../../../../../base/browser/domStylesheets.js';6import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';7import { Disposable } from '../../../../../base/common/lifecycle.js';8import { derived, mapObservableArrayCached, derivedDisposable, derivedObservableWithCache, IObservable, ISettableObservable, constObservable, observableValue } from '../../../../../base/common/observable.js';9import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';10import { ICodeEditor } from '../../../../browser/editorBrowser.js';11import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js';12import { EditorOption } from '../../../../common/config/editorOptions.js';13import { LineRange } from '../../../../common/core/ranges/lineRange.js';14import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js';15import { GhostTextOrReplacement } from '../model/ghostText.js';16import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js';17import { InlineCompletionItem } from '../model/inlineSuggestionItem.js';18import { convertItemsToStableObservables } from '../utils.js';19import { GhostTextView, GhostTextWidgetWarning, IGhostTextWidgetData } from './ghostText/ghostTextView.js';20import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from './inlineEdits/components/gutterIndicatorView.js';21import { InlineEditsOnboardingExperience } from './inlineEdits/inlineEditsNewUsers.js';22import { InlineCompletionViewKind, InlineEditTabAction } from './inlineEdits/inlineEditsViewInterface.js';23import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js';2425export class InlineSuggestionsView extends Disposable {26public static hot = createHotClass(this);2728private readonly _ghostTexts = derived(this, (reader) => {29const model = this._model.read(reader);30return model?.ghostTexts.read(reader) ?? [];31});3233private readonly _stablizedGhostTexts;34private readonly _editorObs;35private readonly _ghostTextWidgets;3637private readonly _inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineSuggestion);38private readonly _everHadInlineEdit = derivedObservableWithCache<boolean>(this,39(reader, last) => last || !!this._inlineEdit.read(reader)40|| !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineSuggestion?.showInlineEditMenu41);4243// To break a cyclic dependency44private readonly _indicatorIsHoverVisible = observableValue<IObservable<boolean> | undefined>(this, undefined);4546private readonly _showInlineEditCollapsed = derived(this, reader => {47const s = this._model.read(reader)?.showCollapsed.read(reader) ?? false;48return s && !this._indicatorIsHoverVisible.read(reader)?.read(reader);49});5051private readonly _inlineEditWidget = derivedDisposable(reader => {52if (!this._everHadInlineEdit.read(reader)) {53return undefined;54}55return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer, this._editor, this._model, this._showInlineEditCollapsed);56});5758private readonly _fontFamily;5960constructor(61private readonly _editor: ICodeEditor,62private readonly _model: IObservable<InlineCompletionsModel | undefined>,63private readonly _focusIsInMenu: ISettableObservable<boolean>,64@IInstantiationService private readonly _instantiationService: IInstantiationService65) {66super();6768this._stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store);69this._editorObs = observableCodeEditor(this._editor);7071this._ghostTextWidgets = mapObservableArrayCached(72this,73this._stablizedGhostTexts,74(ghostText, store) => store.add(this._createGhostText(ghostText))75).recomputeInitiallyAndOnChange(this._store);7677this._inlineEditWidget.recomputeInitiallyAndOnChange(this._store);7879this._fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily);8081this._register(createStyleSheetFromObservable(derived(reader => {82const fontFamily = this._fontFamily.read(reader);83return `84.monaco-editor .ghost-text-decoration,85.monaco-editor .ghost-text-decoration-preview,86.monaco-editor .ghost-text {87font-family: ${fontFamily};88}`;89})));9091this._register(new InlineCompletionsHintsWidget(this._editor, this._model, this._instantiationService));9293this._indicator = this._register(this._instantiationService.createInstance(94InlineEditsGutterIndicator,95this._editorObs,96derived(reader => {97const s = this._gutterIndicatorState.read(reader);98if (!s) { return undefined; }99return new InlineEditsGutterIndicatorData(100InlineSuggestionGutterMenuData.fromInlineSuggestion(s.inlineSuggestion),101s.displayRange,102SimpleInlineSuggestModel.fromInlineCompletionModel(s.model),103s.inlineSuggestion.action?.kind === 'edit' ? s.inlineSuggestion.action.alternativeAction : undefined,104);105}),106this._gutterIndicatorState.map((s, reader) => s?.tabAction.read(reader) ?? InlineEditTabAction.Inactive),107this._gutterIndicatorState.map((s, reader) => s?.gutterIndicatorOffset.read(reader) ?? 0),108this._inlineEditWidget.map((w, reader) => w?.view.inlineEditsIsHovered.read(reader) ?? false),109this._focusIsInMenu,110));111this._indicatorIsHoverVisible.set(this._indicator.isHoverVisible, undefined);112113derived(reader => {114const w = this._inlineEditWidget.read(reader);115if (!w) { return undefined; }116return reader.store.add(this._instantiationService.createInstance(117InlineEditsOnboardingExperience,118w._inlineEditModel,119constObservable(this._indicator),120w.view._inlineCollapsedView,121));122}).recomputeInitiallyAndOnChange(this._store);123}124125private _createGhostText(ghostText: IObservable<GhostTextOrReplacement>): GhostTextView {126return this._instantiationService.createInstance(127GhostTextView,128this._editor,129derived(reader => {130const model = this._model.read(reader);131const inlineCompletion = model?.inlineCompletionState.read(reader)?.inlineSuggestion;132if (!model || !inlineCompletion) {133// editor.suggest.preview: true causes situations where we have ghost text, but no suggest preview.134return {135ghostText: ghostText.read(reader),136handleInlineCompletionShown: () => { /* no-op */ },137warning: undefined,138};139}140return {141ghostText: ghostText.read(reader),142handleInlineCompletionShown: (viewData) => model.handleInlineSuggestionShown(inlineCompletion, InlineCompletionViewKind.GhostText, viewData, Date.now()),143warning: GhostTextWidgetWarning.from(model?.warning.read(reader)),144} satisfies IGhostTextWidgetData;145}),146{147useSyntaxHighlighting: this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled),148},149);150}151152public shouldShowHoverAtViewZone(viewZoneId: string): boolean {153return this._ghostTextWidgets.get()[0]?.ownsViewZone(viewZoneId) ?? false;154}155156private readonly _gutterIndicatorState = derived(reader => {157const model = this._model.read(reader);158if (!model) {159return undefined;160}161162const state = model.state.read(reader);163164if (state?.kind === 'ghostText' && state.inlineSuggestion?.showInlineEditMenu) {165return {166displayRange: LineRange.ofLength(state.primaryGhostText.lineNumber, 1),167tabAction: derived<InlineEditTabAction>(this,168reader => this._editorObs.isFocused.read(reader) ? InlineEditTabAction.Accept : InlineEditTabAction.Inactive169),170gutterIndicatorOffset: constObservable(getGhostTextTopOffset(state.inlineSuggestion, this._editor)),171inlineSuggestion: state.inlineSuggestion,172model,173};174} else if (state?.kind === 'inlineEdit') {175const inlineEditWidget = this._inlineEditWidget.read(reader)?.view;176if (!inlineEditWidget) { return undefined; }177178const displayRange = inlineEditWidget.displayRange.read(reader);179if (!displayRange) { return undefined; }180return {181displayRange,182tabAction: derived(reader => {183if (this._editorObs.isFocused.read(reader)) {184if (model.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; }185if (model.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; }186}187return InlineEditTabAction.Inactive;188}),189gutterIndicatorOffset: inlineEditWidget.gutterIndicatorOffset,190inlineSuggestion: state.inlineSuggestion,191model,192};193} else {194return undefined;195}196});197198protected readonly _indicator;199}200201function getGhostTextTopOffset(inlineCompletion: InlineCompletionItem, editor: ICodeEditor): number {202const replacement = inlineCompletion.getSingleTextEdit();203const textModel = editor.getModel();204if (!textModel) {205return 0;206}207208const EOL = textModel.getEOL();209if (replacement.range.isEmpty() && replacement.text.startsWith(EOL)) {210const lineHeight = editor.getLineHeightForPosition(replacement.range.getStartPosition());211return countPrefixRepeats(replacement.text, EOL) * lineHeight;212}213214return 0;215}216217function countPrefixRepeats(str: string, prefix: string): number {218if (!prefix.length) {219return 0;220}221let count = 0;222let i = 0;223while (str.startsWith(prefix, i)) {224count++;225i += prefix.length;226}227return count;228}229230231