Коммит 3d182cf0 создал по автору Henning Dieterichs's avatar Henning Dieterichs
Просмотр файлов

first version of NES long distance hints

владелец 9a4e259c
......@@ -2507,7 +2507,43 @@ export abstract class ObserverNode<T extends HTMLOrSVGElement = HTMLOrSVGElement
this.keepUpdated(store);
return new LiveElement(this._element, store);
}
private _isHovered: IObservable<boolean> | undefined = undefined;
get isHovered(): IObservable<boolean> {
if (!this._isHovered) {
const hovered = observableValue<boolean>('hovered', false);
this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined));
this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined));
this._isHovered = hovered;
}
return this._isHovered;
}
private _didMouseMoveDuringHover: IObservable<boolean> | undefined = undefined;
get didMouseMoveDuringHover(): IObservable<boolean> {
if (!this._didMouseMoveDuringHover) {
let _hovering = false;
const hovered = observableValue<boolean>('didMouseMoveDuringHover', false);
this._element.addEventListener('mouseenter', (_e) => {
_hovering = true;
});
this._element.addEventListener('mousemove', (_e) => {
if (_hovering) {
hovered.set(true, undefined);
}
});
this._element.addEventListener('mouseleave', (_e) => {
_hovering = false;
hovered.set(false, undefined);
});
this._didMouseMoveDuringHover = hovered;
}
return this._didMouseMoveDuringHover;
}
}
function setClassName(domNode: HTMLOrSVGElement, className: string) {
if (isSVGElement(domNode)) {
domNode.setAttribute('class', className);
......@@ -2515,6 +2551,7 @@ function setClassName(domNode: HTMLOrSVGElement, className: string) {
domNode.className = className;
}
}
function resolve<T>(value: ValueOrList<T>, reader: IReader | undefined, cb: (val: T) => void): void {
if (isObservable(value)) {
cb(value.read(reader));
......@@ -2582,41 +2619,6 @@ export class ObserverNodeWithElement<T extends HTMLOrSVGElement = HTMLOrSVGEleme
public get element() {
return this._element;
}
private _isHovered: IObservable<boolean> | undefined = undefined;
get isHovered(): IObservable<boolean> {
if (!this._isHovered) {
const hovered = observableValue<boolean>('hovered', false);
this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined));
this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined));
this._isHovered = hovered;
}
return this._isHovered;
}
private _didMouseMoveDuringHover: IObservable<boolean> | undefined = undefined;
get didMouseMoveDuringHover(): IObservable<boolean> {
if (!this._didMouseMoveDuringHover) {
let _hovering = false;
const hovered = observableValue<boolean>('didMouseMoveDuringHover', false);
this._element.addEventListener('mouseenter', (_e) => {
_hovering = true;
});
this._element.addEventListener('mousemove', (_e) => {
if (_hovering) {
hovered.set(true, undefined);
}
});
this._element.addEventListener('mouseleave', (_e) => {
_hovering = false;
hovered.set(false, undefined);
});
this._didMouseMoveDuringHover = hovered;
}
return this._didMouseMoveDuringHover;
}
}
function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unknown) {
if (value === null || value === undefined) {
......
......@@ -825,6 +825,8 @@ export interface ICodeEditor extends editorCommon.IEditor {
*/
readonly onEndUpdate: Event<void>;
readonly onDidChangeViewZones: Event<void>;
/**
* Saves current view state of the editor in a serializable object.
*/
......
......@@ -5,7 +5,7 @@
import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js';
import { DebugLocation, IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js';
import { DebugLocation, IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableSignalFromEvent, observableValue, observableValueOpts } from '../../base/common/observable.js';
import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js';
import { LineRange } from '../common/core/ranges/lineRange.js';
import { OffsetRange } from '../common/core/ranges/offsetRange.js';
......@@ -148,6 +148,9 @@ export class ObservableCodeEditor extends Disposable {
this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth);
this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());
this.contentHeight = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentHeight());
this._onDidChangeViewZones = observableSignalFromEvent(this, this.editor.onDidChangeViewZones);
this._onDidHiddenAreasChanged = observableSignalFromEvent(this, this.editor.onDidChangeHiddenAreas);
this._onDidLineHeightChanged = observableSignalFromEvent(this, this.editor.onDidChangeLineHeight);
this._widgetCounter = 0;
this.openedPeekWidgets = observableValue(this, 0);
......@@ -456,6 +459,37 @@ export class ObservableCodeEditor extends Disposable {
});
}
private readonly _onDidChangeViewZones;
private readonly _onDidHiddenAreasChanged;
private readonly _onDidLineHeightChanged;
/**
* Get the vertical position (top offset) for the line's bottom w.r.t. to the first line.
*/
observeTopForLineNumber(lineNumber: number): IObservable<number> {
return derived(reader => {
this.layoutInfo.read(reader);
this._onDidChangeViewZones.read(reader);
this._onDidHiddenAreasChanged.read(reader);
this._onDidLineHeightChanged.read(reader);
this._versionId.read(reader);
return this.editor.getTopForLineNumber(lineNumber);
});
}
/**
* Get the vertical position (top offset) for the line's bottom w.r.t. to the first line.
*/
observeBottomForLineNumber(lineNumber: number): IObservable<number> {
return derived(reader => {
this.layoutInfo.read(reader);
this._onDidChangeViewZones.read(reader);
this._onDidHiddenAreasChanged.read(reader);
this._onDidLineHeightChanged.read(reader);
this._versionId.read(reader);
return this.editor.getBottomForLineNumber(lineNumber);
});
}
}
interface IObservableOverlayWidget {
......
......@@ -31,13 +31,14 @@ import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyO
* Make sure to add the view zones to the editor!
*/
export class HideUnchangedRegionsFeature extends Disposable {
private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>(
public static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>(
this, () => ({
dispose() {
},
getBreadcrumbItems(startRange, reader) {
return [];
},
getAt: () => [],
}));
public static setBreadcrumbsSourceFactory(factory: (textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) {
this._breadcrumbsSourceFactory.set(factory, undefined);
......@@ -491,4 +492,6 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget {
export interface IDiffEditorBreadcrumbsSource extends IDisposable {
getBreadcrumbItems(startRange: LineRange, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[];
getAt(lineNumber: number, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[];
}
......@@ -4442,6 +4442,8 @@ export interface IInlineSuggestOptions {
showCollapsed?: boolean;
showLongDistanceHint?: boolean;
/**
* @internal
*/
......@@ -4500,6 +4502,7 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
showCollapsed: false,
renderSideBySide: 'auto',
allowCodeShifting: 'always',
showLongDistanceHint: false,
},
triggerCommandOnProviderChange: false,
experimental: {
......@@ -4599,6 +4602,12 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
enum: ['always', 'horizontal', 'never'],
tags: ['nextEditSuggestions']
},
'editor.inlineSuggest.edits.showLongDistanceHint': {
type: 'boolean',
default: defaults.edits.showLongDistanceHint,
description: nls.localize('inlineSuggest.edits.showLongDistanceHint', "Controls whether long distance inline suggestions are shown."),
tags: ['nextEditSuggestions', 'experimental']
},
'editor.inlineSuggest.edits.renderSideBySide': {
type: 'string',
default: defaults.edits.renderSideBySide,
......@@ -4650,6 +4659,7 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
enabled: boolean(input.enabled, this.defaultValue.edits.enabled),
showCollapsed: boolean(input.showCollapsed, this.defaultValue.edits.showCollapsed),
allowCodeShifting: stringSet(input.allowCodeShifting, this.defaultValue.edits.allowCodeShifting, ['always', 'horizontal', 'never']),
showLongDistanceHint: boolean(input.showLongDistanceHint, this.defaultValue.edits.showLongDistanceHint),
renderSideBySide: stringSet(input.renderSideBySide, this.defaultValue.edits.renderSideBySide, ['never', 'auto']),
};
}
......
......@@ -6,6 +6,7 @@
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { OffsetRange } from '../ranges/offsetRange.js';
import { Point } from './point.js';
import { Size2D } from './size.js';
export class Rect {
public static fromPoint(point: Point): Rect {
......@@ -204,6 +205,10 @@ export class Rect {
return new Rect(this.left, this.top + delta, this.right, this.bottom + delta);
}
translate(point: Point): Rect {
return new Rect(this.left + point.x, this.top + point.y, this.right + point.x, this.bottom + point.y);
}
deltaRight(delta: number): Rect {
return new Rect(this.left, this.top, this.right + delta, this.bottom);
}
......@@ -245,4 +250,24 @@ export class Rect {
height: `${this.height}px`,
};
}
getHorizontalRange(): OffsetRange {
return new OffsetRange(this.left, this.right);
}
getVerticalRange(): OffsetRange {
return new OffsetRange(this.top, this.bottom);
}
withHorizontalRange(range: OffsetRange): Rect {
return new Rect(range.start, this.top, range.endExclusive, this.bottom);
}
withVerticalRange(range: OffsetRange): Rect {
return new Rect(this.left, range.start, this.right, range.endExclusive);
}
getSize(): Size2D {
return new Size2D(this.width, this.height);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDimension } from './dimension.js';
export class Size2D {
static equals(a: Size2D, b: Size2D): boolean {
return a.width === b.width && a.height === b.height;
}
constructor(
public readonly width: number,
public readonly height: number,
) { }
public add(other: Size2D): Size2D {
return new Size2D(this.width + other.width, this.height + other.height);
}
public deltaX(delta: number): Size2D {
return new Size2D(this.width + delta, this.height);
}
public deltaY(delta: number): Size2D {
return new Size2D(this.width, this.height + delta);
}
public toString() {
return `(${this.width},${this.height})`;
}
public subtract(other: Size2D): Size2D {
return new Size2D(this.width - other.width, this.height - other.height);
}
public scale(factor: number): Size2D {
return new Size2D(this.width * factor, this.height * factor);
}
public scaleWidth(factor: number): Size2D {
return new Size2D(this.width * factor, this.height);
}
public mapComponents(map: (value: number) => number): Size2D {
return new Size2D(map(this.width), map(this.height));
}
public isZero(): boolean {
return this.width === 0 && this.height === 0;
}
public transpose(): Size2D {
return new Size2D(this.height, this.width);
}
public toDimension(): IDimension {
return { width: this.width, height: this.height };
}
}
......@@ -208,6 +208,10 @@ export class OffsetRange implements IOffsetRange {
}
return new OffsetRange(this.start, range.endExclusive);
}
public withMargin(margin: number): OffsetRange {
return new OffsetRange(this.start - margin, this.endExclusive + margin);
}
}
export class OffsetRangeSet {
......
......@@ -55,6 +55,17 @@ class DiffEditorBreadcrumbsSource extends Disposable implements IDiffEditorBread
symbols.sort(reverseOrder(compareBy(s => s.range.endLineNumber - s.range.startLineNumber, numberComparator)));
return symbols.map(s => ({ name: s.name, kind: s.kind, startLineNumber: s.range.startLineNumber }));
}
public getAt(lineNumber: number, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[] {
const m = this._currentModel.read(reader);
if (!m) { return []; }
const symbols = m.asListOfDocumentSymbols()
.filter(s => new LineRange(s.range.startLineNumber, s.range.endLineNumber).contains(lineNumber));
if (symbols.length === 0) { return []; }
symbols.sort(reverseOrder(compareBy(s => s.range.endLineNumber - s.range.startLineNumber, numberComparator)));
return symbols.map(s => ({ name: s.name, kind: s.kind, startLineNumber: s.range.startLineNumber }));
}
}
HideUnchangedRegionsFeature.setBreadcrumbsSourceFactory((textModel, instantiationService) => {
......
......@@ -96,6 +96,10 @@ export class InlineCompletionsModel extends Disposable {
private readonly _suppressInSnippetMode;
private readonly _isInSnippetMode;
get editor() {
return this._editor;
}
constructor(
public readonly textModel: ITextModel,
private readonly _selectedSuggestItem: IObservable<SuggestItemInfo | undefined>,
......@@ -1206,13 +1210,22 @@ class FadeoutDecoration extends Disposable {
}
}
function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem): boolean {
export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem, reader: IReader | undefined = undefined): boolean {
const targetRange = suggestion.targetRange;
// TODO make getVisibleRanges reactive!
observableCodeEditor(editor).scrollTop.read(reader);
const visibleRanges = editor.getVisibleRanges();
if (visibleRanges.length < 1) {
return false;
}
const viewportRange = new Range(visibleRanges[0].startLineNumber, visibleRanges[0].startColumn, visibleRanges[visibleRanges.length - 1].endLineNumber, visibleRanges[visibleRanges.length - 1].endColumn);
const viewportRange = new Range(
visibleRanges[0].startLineNumber,
visibleRanges[0].startColumn,
visibleRanges[visibleRanges.length - 1].endLineNumber,
visibleRanges[visibleRanges.length - 1].endColumn
);
return viewportRange.containsRange(targetRange);
}
......@@ -27,7 +27,7 @@ import { asCssVariable, descriptionForeground, editorActionListForeground, edito
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
import { hideInlineCompletionId, inlineSuggestCommitId, toggleShowCollapsedId } from '../../../controller/commandIds.js';
import { IInlineEditModel } from '../inlineEditsViewInterface.js';
import { ModelPerInlineEdit } from '../inlineEditsModel.js';
import { FirstFnArg, } from '../utils/utils.js';
export class GutterIndicatorMenuContent {
......@@ -35,7 +35,7 @@ export class GutterIndicatorMenuContent {
private readonly _inlineEditsShowCollapsed: IObservable<boolean>;
constructor(
private readonly _model: IInlineEditModel,
private readonly _model: ModelPerInlineEdit,
private readonly _close: (focusEditor: boolean) => void,
private readonly _editorObs: ObservableCodeEditor,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
......
......@@ -24,7 +24,8 @@ import { EditorOption, RenderLineNumbersType } from '../../../../../../common/co
import { LineRange } from '../../../../../../common/core/ranges/lineRange.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js';
import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js';
import { InlineEditTabAction } from '../inlineEditsViewInterface.js';
import { ModelPerInlineEdit } from '../inlineEditsModel.js';
import { getEditorBlendedColor, inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryBorder, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulBorder, inlineEditIndicatorsuccessfulForeground } from '../theme.js';
import { mapOutFalsy, rectToProps } from '../utils/utils.js';
import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js';
......@@ -45,7 +46,7 @@ export class InlineEditsGutterIndicator extends Disposable {
private readonly _editorObs: ObservableCodeEditor,
private readonly _originalRange: IObservable<LineRange | undefined>,
private readonly _verticalOffset: IObservable<number>,
private readonly _model: IObservable<IInlineEditModel | undefined>,
private readonly _model: IObservable<ModelPerInlineEdit | undefined>,
private readonly _isHoveringOverInlineEdit: IObservable<boolean>,
private readonly _focusIsInMenu: ISettableObservable<boolean>,
@IHoverService private readonly _hoverService: HoverService,
......
......@@ -12,12 +12,15 @@ import { LineRange } from '../../../../../common/core/ranges/lineRange.js';
import { TextEdit } from '../../../../../common/core/edits/textEdit.js';
import { StringText } from '../../../../../common/core/text/abstractText.js';
import { Command, InlineCompletionCommand } from '../../../../../common/languages.js';
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
import { InlineCompletionsModel, isSuggestionInViewport } from '../../model/inlineCompletionsModel.js';
import { InlineCompletionItem, InlineSuggestHint } from '../../model/inlineSuggestionItem.js';
import { IInlineEditHost, IInlineEditModel, InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js';
import { IInlineEditHost, InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js';
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
export class InlineEditModel implements IInlineEditModel {
/**
* Warning: This is not per inline edit id and gets created often.
*/
export class ModelPerInlineEdit implements ModelPerInlineEdit {
readonly action: Command | undefined;
readonly displayName: string;
......@@ -27,6 +30,9 @@ export class InlineEditModel implements IInlineEditModel {
readonly displayLocation: InlineSuggestHint | undefined;
readonly showCollapsed: IObservable<boolean>;
/** Determines if the inline suggestion is fully in the view port */
readonly inViewPort: IObservable<boolean>;
constructor(
private readonly _model: InlineCompletionsModel,
readonly inlineEdit: InlineEditWithChanges,
......@@ -39,6 +45,8 @@ export class InlineEditModel implements IInlineEditModel {
this.displayLocation = this.inlineEdit.inlineCompletion.hint;
this.showCollapsed = this._model.showCollapsed;
this.inViewPort = derived(this, reader => isSuggestionInViewport(this._model.editor, this.inlineEdit.inlineCompletion, reader));
}
accept() {
......@@ -68,7 +76,7 @@ export class InlineEditHost implements IInlineEditHost {
export class GhostTextIndicator {
readonly model: InlineEditModel;
readonly model: ModelPerInlineEdit;
constructor(
editor: ICodeEditor,
......@@ -86,7 +94,7 @@ export class GhostTextIndicator {
return InlineEditTabAction.Inactive;
});
this.model = new InlineEditModel(
this.model = new ModelPerInlineEdit(
model,
new InlineEditWithChanges(
new StringText(''),
......
......@@ -10,7 +10,8 @@ import { autorun, autorunWithStore, derived, IObservable, observableValue, runOn
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js';
import { IInlineEditHost, IInlineEditModel } from './inlineEditsViewInterface.js';
import { IInlineEditHost } from './inlineEditsViewInterface.js';
import { ModelPerInlineEdit } from './inlineEditsModel.js';
import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js';
enum UserKind {
......@@ -39,7 +40,7 @@ export class InlineEditsOnboardingExperience extends Disposable {
constructor(
private readonly _host: IObservable<IInlineEditHost | undefined>,
private readonly _model: IObservable<IInlineEditModel | undefined>,
private readonly _model: IObservable<ModelPerInlineEdit | undefined>,
private readonly _indicator: IObservable<InlineEditsGutterIndicator | undefined>,
private readonly _collapsedView: InlineEditsCollapsedView,
@IStorageService private readonly _storageService: IStorageService,
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { $ } from '../../../../../../base/browser/dom.js';
import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js';
import { equalsIfDefined, itemEquals, itemsEquals } from '../../../../../../base/common/equals.js';
import { BugIndicatingError, onUnexpectedError } from '../../../../../../base/common/errors.js';
import { Event } from '../../../../../../base/common/event.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
......@@ -20,18 +20,20 @@ import { LineRange } from '../../../../../common/core/ranges/lineRange.js';
import { AbstractText, StringText } from '../../../../../common/core/text/abstractText.js';
import { TextLength } from '../../../../../common/core/text/textLength.js';
import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js';
import { ITextModel } from '../../../../../common/model.js';
import { TextModel } from '../../../../../common/model/textModel.js';
import { InlineEditItem } from '../../model/inlineSuggestionItem.js';
import { InlineEditItem, InlineSuggestionIdentity } from '../../model/inlineSuggestionItem.js';
import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js';
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
import { GhostTextIndicator, InlineEditHost, InlineEditModel } from './inlineEditsModel.js';
import { GhostTextIndicator, InlineEditHost, ModelPerInlineEdit } from './inlineEditsModel.js';
import { InlineEditsOnboardingExperience } from './inlineEditsNewUsers.js';
import { IInlineEditModel, InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js';
import { InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js';
import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js';
import { InlineEditsCustomView } from './inlineEditsViews/inlineEditsCustomView.js';
import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js';
import { InlineEditsInsertionView } from './inlineEditsViews/inlineEditsInsertionView.js';
import { InlineEditsLineReplacementView } from './inlineEditsViews/inlineEditsLineReplacementView.js';
import { ILongDistanceHint, ILongDistanceViewState, InlineEditsLongDistanceHint } from './inlineEditsViews/inlineEditsLongDistanceHint.js';
import { InlineEditsSideBySideView } from './inlineEditsViews/inlineEditsSideBySideView.js';
import { InlineEditsWordReplacementView } from './inlineEditsViews/inlineEditsWordReplacementView.js';
import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineEditsViews/originalEditorInlineDiffView.js';
......@@ -53,11 +55,12 @@ export class InlineEditsView extends Disposable {
editorWidth: number;
timestamp: number;
} | undefined;
private readonly _showLongDistanceHint: IObservable<boolean>;
constructor(
private readonly _editor: ICodeEditor,
private readonly _host: IObservable<InlineEditHost | undefined>,
private readonly _model: IObservable<InlineEditModel | undefined>,
private readonly _model: IObservable<ModelPerInlineEdit | undefined>,
private readonly _ghostTextIndicator: IObservable<GhostTextIndicator | undefined>,
private readonly _focusIsInMenu: ISettableObservable<boolean>,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
......@@ -73,6 +76,7 @@ export class InlineEditsView extends Disposable {
newText: string;
newTextLineCount: number;
isInDiffEditor: boolean;
longDistanceHint: ILongDistanceHint | undefined;
} | undefined>(this, reader => {
const model = this._model.read(reader);
const textModel = this._editorObs.model.read(reader);
......@@ -91,6 +95,8 @@ export class InlineEditsView extends Disposable {
return undefined;
}
const longDistanceHint = this._getLongDistanceHintState(model, reader);
if (state.kind === InlineCompletionViewKind.SideBySide) {
const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange, textModel.getOptions().tabSize);
newText = indentationAdjustmentEdit.applyToString(newText);
......@@ -120,6 +126,7 @@ export class InlineEditsView extends Disposable {
newText,
newTextLineCount: inlineEdit.modifiedLineRange.length,
isInDiffEditor: model.isInDiffEditor,
longDistanceHint,
};
});
this._previewTextModel = this._register(this._instantiationService.createInstance(
......@@ -160,7 +167,7 @@ export class InlineEditsView extends Disposable {
return state.edit.displayRange;
});
const modelWithGhostTextSupport = derived<InlineEditModel | undefined>(this, reader => {
const modelWithGhostTextSupport = derived<ModelPerInlineEdit | undefined>(this, reader => {
/** @description modelWithGhostTextSupport */
const model = this._model.read(reader);
if (model) {
......@@ -260,8 +267,35 @@ export class InlineEditsView extends Disposable {
this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'custom' ? m?.displayLocation : undefined),
this._tabAction,
));
this._showLongDistanceHint = this._editorObs.getOption(EditorOption.inlineSuggest).map(this, s => s.edits.showLongDistanceHint);
this._longDistanceHint = derived(this, reader => {
if (!this._showLongDistanceHint.read(reader)) {
return undefined;
}
return reader.store.add(this._instantiationService.createInstance(InlineEditsLongDistanceHint,
this._editor,
this._uiState.map<ILongDistanceViewState | undefined>(s => s?.longDistanceHint ? ({
hint: s.longDistanceHint,
newTextLineCount: s.newTextLineCount,
edit: s.edit,
diff: s.diff,
}) : undefined),
this._previewTextModel,
this._tabAction,
this._model,
));
}).recomputeInitiallyAndOnChange(this._store);
this._inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel));
this._wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => {
const wordReplacements = derivedOpts({
equalsFn: itemsEquals(itemEquals())
}, reader => {
const s = this._uiState.read(reader);
return s?.state?.kind === 'wordReplacements' ? s.state.replacements : [];
});
this._wordReplacementViews = mapObservableArrayCached(this, wordReplacements, (e, store) => {
return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, this._tabAction));
});
this._lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView,
......@@ -348,6 +382,24 @@ export class InlineEditsView extends Disposable {
this._constructorDone.set(true, undefined); // TODO: remove and use correct initialization order
}
private _currentInlineEditCache: {
inlineSuggestionIdentity: InlineSuggestionIdentity;
firstCursorLineNumber: number;
} | undefined = undefined;
private _getLongDistanceHintState(model: ModelPerInlineEdit, reader: IReader): ILongDistanceHint | undefined {
if (this._currentInlineEditCache?.inlineSuggestionIdentity !== model.inlineEdit.inlineCompletion.identity) {
this._currentInlineEditCache = {
inlineSuggestionIdentity: model.inlineEdit.inlineCompletion.identity,
firstCursorLineNumber: model.inlineEdit.cursorPosition.lineNumber,
};
}
return model.inViewPort.read(reader) ? undefined : {
lineNumber: this._currentInlineEditCache.firstCursorLineNumber,
};
}
private readonly _constructorDone;
private readonly _uiState;
......@@ -372,7 +424,8 @@ export class InlineEditsView extends Disposable {
protected readonly _inlineCollapsedView;
protected readonly _customView;
private readonly _customView;
protected readonly _longDistanceHint;
protected readonly _inlineDiffView;
......@@ -380,11 +433,11 @@ export class InlineEditsView extends Disposable {
protected readonly _lineReplacementView;
private getCacheId(model: IInlineEditModel) {
private getCacheId(model: ModelPerInlineEdit) {
return model.inlineEdit.inlineCompletion.identity.id;
}
private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): InlineCompletionViewKind {
private determineView(model: ModelPerInlineEdit, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): InlineCompletionViewKind {
// Check if we can use the previous view if it is the same InlineCompletion as previously shown
const inlineEdit = model.inlineEdit;
const canUseCache = this._previousView?.id === this.getCacheId(model) && !model.displayLocation?.jumpToEdit;
......@@ -478,11 +531,10 @@ export class InlineEditsView extends Disposable {
return InlineCompletionViewKind.SideBySide;
}
private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) {
private determineRenderState(model: ModelPerInlineEdit, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) {
const inlineEdit = model.inlineEdit;
let view = this.determineView(model, reader, diff, newText);
if (this._willRenderAboveCursor(reader, inlineEdit, view)) {
switch (view) {
case InlineCompletionViewKind.LineReplacement:
......@@ -491,7 +543,6 @@ export class InlineEditsView extends Disposable {
break;
}
}
this._previousView = { id: this.getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() };
const inner = diff.flatMap(d => d.innerChanges ?? []);
......@@ -503,18 +554,7 @@ export class InlineEditsView extends Disposable {
modified: newText.getValueOfRange(m.modifiedRange)
}));
const cursorPosition = inlineEdit.cursorPosition;
const startsWithEOL = stringChanges.length === 0 ? false : stringChanges[0].modified.startsWith(textModel.getEOL());
const viewData: InlineCompletionViewData = {
cursorColumnDistance: inlineEdit.edit.replacements.length === 0 ? 0 : inlineEdit.edit.replacements[0].range.getStartPosition().column - cursorPosition.column,
cursorLineDistance: inlineEdit.lineEdit.lineRange.startLineNumber - cursorPosition.lineNumber + (startsWithEOL && inlineEdit.lineEdit.lineRange.startLineNumber >= cursorPosition.lineNumber ? 1 : 0),
lineCountOriginal: inlineEdit.lineEdit.lineRange.length,
lineCountModified: inlineEdit.lineEdit.newLines.length,
characterCountOriginal: stringChanges.reduce((acc, r) => acc + r.original.length, 0),
characterCountModified: stringChanges.reduce((acc, r) => acc + r.modified.length, 0),
disjointReplacements: stringChanges.length,
sameShapeReplacements: stringChanges.every(r => r.original === stringChanges[0].original && r.modified === stringChanges[0].modified),
};
const viewData = getViewData(inlineEdit, stringChanges, textModel);
switch (view) {
case InlineCompletionViewKind.InsertionInline: return { kind: InlineCompletionViewKind.InsertionInline as const, viewData };
......@@ -611,6 +651,22 @@ export class InlineEditsView extends Disposable {
}
}
function getViewData(inlineEdit: InlineEditWithChanges, stringChanges: { originalRange: Range; modifiedRange: Range; original: string; modified: string }[], textModel: ITextModel) {
const cursorPosition = inlineEdit.cursorPosition;
const startsWithEOL = stringChanges.length === 0 ? false : stringChanges[0].modified.startsWith(textModel.getEOL());
const viewData: InlineCompletionViewData = {
cursorColumnDistance: inlineEdit.edit.replacements.length === 0 ? 0 : inlineEdit.edit.replacements[0].range.getStartPosition().column - cursorPosition.column,
cursorLineDistance: inlineEdit.lineEdit.lineRange.startLineNumber - cursorPosition.lineNumber + (startsWithEOL && inlineEdit.lineEdit.lineRange.startLineNumber >= cursorPosition.lineNumber ? 1 : 0),
lineCountOriginal: inlineEdit.lineEdit.lineRange.length,
lineCountModified: inlineEdit.lineEdit.newLines.length,
characterCountOriginal: stringChanges.reduce((acc, r) => acc + r.original.length, 0),
characterCountModified: stringChanges.reduce((acc, r) => acc + r.modified.length, 0),
disjointReplacements: stringChanges.length,
sameShapeReplacements: stringChanges.every(r => r.original === stringChanges[0].original && r.modified === stringChanges[0].modified),
};
return viewData;
}
function isSingleLineInsertion(diff: DetailedLineRangeMapping[]) {
return diff.every(m => m.innerChanges!.every(r => isWordInsertion(r)));
......
......@@ -6,9 +6,6 @@
import { IMouseEvent } from '../../../../../../base/browser/mouseEvent.js';
import { Event } from '../../../../../../base/common/event.js';
import { IObservable } from '../../../../../../base/common/observable.js';
import { Command, InlineCompletionCommand } from '../../../../../common/languages.js';
import { InlineSuggestHint } from '../../model/inlineSuggestionItem.js';
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
export enum InlineEditTabAction {
Jump = 'jump',
......@@ -27,21 +24,6 @@ export interface IInlineEditHost {
inAcceptFlow: IObservable<boolean>;
}
export interface IInlineEditModel {
displayName: string;
action: Command | undefined;
extensionCommands: InlineCompletionCommand[];
isInDiffEditor: boolean;
inlineEdit: InlineEditWithChanges;
tabAction: IObservable<InlineEditTabAction>;
showCollapsed: IObservable<boolean>;
displayLocation: InlineSuggestHint | undefined;
handleInlineEditShown(viewKind: string, viewData?: InlineCompletionViewData): void;
accept(): void;
jump(): void;
}
// TODO: Move this out of here as it is also includes ghosttext
export enum InlineCompletionViewKind {
GhostText = 'ghostText',
......
......@@ -16,7 +16,7 @@ import { TextModelText } from '../../../../../common/model/textModelText.js';
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
import { InlineEdit } from '../../model/inlineEdit.js';
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
import { GhostTextIndicator, InlineEditHost, InlineEditModel } from './inlineEditsModel.js';
import { GhostTextIndicator, InlineEditHost, ModelPerInlineEdit } from './inlineEditsModel.js';
import { InlineEditsView } from './inlineEditsView.js';
import { InlineEditTabAction } from './inlineEditsViewInterface.js';
......@@ -50,7 +50,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.read(undefined), model.allPositions.read(undefined), inlineEdit.commands, inlineEdit.inlineCompletion);
});
private readonly _inlineEditModel = derived<InlineEditModel | undefined>(this, reader => {
private readonly _inlineEditModel = derived<ModelPerInlineEdit | undefined>(this, reader => {
const model = this._model.read(reader);
if (!model) { return undefined; }
const edit = this._inlineEdit.read(reader);
......@@ -65,7 +65,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
return InlineEditTabAction.Inactive;
});
return new InlineEditModel(model, edit, tabAction);
return new ModelPerInlineEdit(model, edit, tabAction);
});
private readonly _inlineEditHost = derived<InlineEditHost | undefined>(this, reader => {
......
......@@ -16,7 +16,15 @@ export function setVisualization(data: object, visualization: IVisualizationEffe
(data as any)['$$visualization'] = visualization;
}
export function debugLogRects(rects: Record<string, Rect>, elem: HTMLElement): object {
export function debugLogRects(rects: Record<string, Rect> | Rect[], elem: HTMLElement): object {
if (Array.isArray(rects)) {
const record: Record<string, Rect> = {};
rects.forEach((rect, index) => {
record[index.toString()] = rect;
});
rects = record;
}
setVisualization(rects, new ManyRectVisualizer(rects, elem));
return rects;
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IFlexBoxPartGrowthRule extends IFlexBoxPartExtensionRule {
min?: number;
rules?: IFlexBoxPartExtensionRule[];
}
export interface IFlexBoxPartExtensionRule {
max?: number;
priority?: number;
share?: number;
}
/**
* Distributes a total size into parts that each have a list of growth rules.
* Returns `null` if the layout is not possible.
* The sum of all returned sizes will be equal to `totalSize`.
*
* First, each part gets its minimum size.
* Then, remaining space is distributed to the rules with the highest priority, as long as the max constraint allows it (considering share).
* This continues with next lower priority rules until no space is left.
*/
export function distributeFlexBoxLayout<T extends Record<string, IFlexBoxPartGrowthRule | IFlexBoxPartGrowthRule[]>>(
totalSize: number,
parts: T & Record<string, IFlexBoxPartGrowthRule | IFlexBoxPartGrowthRule[]>
): Record<keyof T, number> | null {
// Normalize parts to always have array of rules
const normalizedParts: Record<string, { min: number; rules: IFlexBoxPartExtensionRule[] }> = {};
for (const [key, part] of Object.entries(parts)) {
if (Array.isArray(part)) {
normalizedParts[key] = { min: 0, rules: part };
} else {
normalizedParts[key] = {
min: part.min ?? 0,
rules: part.rules ?? [{ max: part.max, priority: part.priority, share: part.share }]
};
}
}
// Initialize result with minimum sizes
const result: Record<string, number> = {};
let usedSize = 0;
for (const [key, part] of Object.entries(normalizedParts)) {
result[key] = part.min;
usedSize += part.min;
}
// Check if we can satisfy minimum constraints
if (usedSize > totalSize) {
return null;
}
let remainingSize = totalSize - usedSize;
// Distribute remaining space by priority levels
while (remainingSize > 0) {
// Find all rules at current highest priority that can still grow
const candidateRules: Array<{
partKey: string;
ruleIndex: number;
rule: IFlexBoxPartExtensionRule;
priority: number;
share: number;
}> = [];
for (const [key, part] of Object.entries(normalizedParts)) {
for (let i = 0; i < part.rules.length; i++) {
const rule = part.rules[i];
const currentUsage = result[key];
const maxSize = rule.max ?? Infinity;
if (currentUsage < maxSize) {
candidateRules.push({
partKey: key,
ruleIndex: i,
rule,
priority: rule.priority ?? 0,
share: rule.share ?? 1
});
}
}
}
if (candidateRules.length === 0) {
// No rules can grow anymore, but we have remaining space
break;
}
// Find the highest priority among candidates
const maxPriority = Math.max(...candidateRules.map(c => c.priority));
const highestPriorityCandidates = candidateRules.filter(c => c.priority === maxPriority);
// Calculate total share
const totalShare = highestPriorityCandidates.reduce((sum, c) => sum + c.share, 0);
// Distribute space proportionally by share
let distributedThisRound = 0;
const distributions: Array<{ partKey: string; ruleIndex: number; amount: number }> = [];
for (const candidate of highestPriorityCandidates) {
const rule = candidate.rule;
const currentUsage = result[candidate.partKey];
const maxSize = rule.max ?? Infinity;
const availableForThisRule = maxSize - currentUsage;
// Calculate ideal share
const idealShare = (remainingSize * candidate.share) / totalShare;
const actualAmount = Math.min(idealShare, availableForThisRule);
distributions.push({
partKey: candidate.partKey,
ruleIndex: candidate.ruleIndex,
amount: actualAmount
});
distributedThisRound += actualAmount;
}
if (distributedThisRound === 0) {
// No progress can be made
break;
}
// Apply distributions
for (const dist of distributions) {
result[dist.partKey] += dist.amount;
}
remainingSize -= distributedThisRound;
// Break if remaining is negligible (floating point precision)
if (remainingSize < 0.0001) {
break;
}
}
return result as Record<keyof T, number>;
}
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать