diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 77e3b3fc1b4..a7afb26ac4c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -8,22 +8,25 @@ import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconL import { Codicon } from '../../../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, constObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js'; +import { IObservable, ISettableObservable, autorun, constObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js'; import { debouncedObservable } from '../../../../../../../base/common/observableInternal/utils.js'; import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; +import { IEditorMouseEvent } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; +import { Point } from '../../../../../../browser/point.js'; import { Rect } from '../../../../../../browser/rect.js'; import { HoverService } from '../../../../../../browser/services/hoverService/hoverService.js'; import { HoverWidget } from '../../../../../../browser/services/hoverService/hoverWidget.js'; -import { EditorOption } from '../../../../../../common/config/editorOptions.js'; +import { EditorOption, RenderLineNumbersType } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulForeground } from '../theme.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'; @@ -35,8 +38,7 @@ export class InlineEditsGutterIndicator extends Disposable { return model; } - private readonly _gutterIndicatorBackgroundColor: IObservable; - private readonly _gutterIndicatorForegroundColor: IObservable; + private readonly _gutterIndicatorStyles: IObservable<{ background: string; foreground: string; border: string }>; private readonly _isHoveredOverInlineEditDebounced: IObservable; constructor( @@ -49,21 +51,27 @@ export class InlineEditsGutterIndicator extends Disposable { @IHoverService private readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAccessibilityService accessibilityService: IAccessibilityService, + @IThemeService themeService: IThemeService, ) { super(); - this._gutterIndicatorBackgroundColor = this._tabAction.map(v => { + this._gutterIndicatorStyles = this._tabAction.map((v, reader) => { switch (v) { - case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryBackground); - case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryBackground); - case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulBackground); - } - }); - this._gutterIndicatorForegroundColor = this._tabAction.map(v => { - switch (v) { - case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryForeground); - case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryForeground); - case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulForeground); + case InlineEditTabAction.Inactive: return { + background: getEditorBlendedColor(inlineEditIndicatorSecondaryBackground, themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorSecondaryForeground, themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorSecondaryBorder, themeService).read(reader).toString(), + }; + case InlineEditTabAction.Jump: return { + background: getEditorBlendedColor(inlineEditIndicatorPrimaryBackground, themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorPrimaryForeground, themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorPrimaryBorder, themeService).read(reader).toString() + }; + case InlineEditTabAction.Accept: return { + background: getEditorBlendedColor(inlineEditIndicatorsuccessfulBackground, themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorsuccessfulForeground, themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorsuccessfulBorder, themeService).read(reader).toString() + }; } }); @@ -74,6 +82,18 @@ export class InlineEditsGutterIndicator extends Disposable { minContentWidthInPx: constObservable(0), })); + this._register(this._editorObs.editor.onMouseMove((e: IEditorMouseEvent) => { + const el = this._iconRef.element; + const rect = el.getBoundingClientRect(); + const rectangularArea = Rect.fromLeftTopWidthHeight(rect.left, rect.top, rect.width, rect.height); + const point = new Point(e.event.posx, e.event.posy); + this._isHoveredOverIcon.set(rectangularArea.containsPoint(point), undefined); + })); + + this._register(this._editorObs.editor.onDidScrollChange(() => { + this._isHoveredOverIcon.set(false, undefined); + })); + this._isHoveredOverInlineEditDebounced = debouncedObservable(this._isHoveringOverInlineEdit, 100); if (!accessibilityService.isMotionReduced()) { @@ -95,7 +115,7 @@ export class InlineEditsGutterIndicator extends Disposable { // PULSE ANIMATION: this._iconRef.element.animate([ { - outline: `2px solid ${this._gutterIndicatorBackgroundColor.get()}`, + outline: `2px solid ${this._gutterIndicatorStyles.map(v => v.border).get()}`, outlineOffset: '-1px', offset: 0 }, @@ -107,6 +127,13 @@ export class InlineEditsGutterIndicator extends Disposable { ], { duration: 500 }); })); } + + this._register(autorun(reader => { + this._indicator.readEffect(reader); + if (this._indicator.element) { + this._editorObs.editor.applyFontInfo(this._indicator.element); + } + })); } private readonly _originalRangeObs = mapOutFalsy(this._originalRange); @@ -125,38 +152,95 @@ export class InlineEditsGutterIndicator extends Disposable { ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); + private readonly _lineNumberToRender = derived(this, reader => { + if (this._verticalOffset.read(reader) !== 0) { + return ''; + } + + const lineNumber = this._originalRange.read(reader)?.startLineNumber; + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + + if (lineNumber === undefined || lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Interval) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (lineNumber % 10 === 0 || cursorPosition && cursorPosition.lineNumber === lineNumber) { + return lineNumber.toString(); + } + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (!cursorPosition) { + return ''; + } + const relativeLineNumber = Math.abs(lineNumber - cursorPosition.lineNumber); + if (relativeLineNumber === 0) { + return lineNumber.toString(); + } + return relativeLineNumber.toString(); + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Custom) { + if (lineNumberOptions.renderFn) { + return lineNumberOptions.renderFn(lineNumber); + } + return ''; + } + + return lineNumber.toString(); + }); + private readonly _layout = derived(this, reader => { const s = this._state.read(reader); if (!s) { return undefined; } const layout = this._editorObs.layoutInfo.read(reader); + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); const bottomPadding = 1; + const leftPadding = 1; + const rightPadding = 1; + + // Entire editor area without sticky scroll const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height - bottomPadding); const viewPortWithStickyScroll = fullViewPort.withTop(this._stickyScrollHeight.read(reader)); + // The glyph margin area across all relevant lines const targetVertRange = s.lineOffsetRange.read(reader); + const targetRect = Rect.fromRanges(OffsetRange.fromTo(leftPadding + layout.glyphMarginLeft, layout.decorationsLeft + layout.decorationsWidth - rightPadding), targetVertRange); - const space = 1; - - const targetRect = Rect.fromRanges(OffsetRange.fromTo(space + layout.glyphMarginLeft, layout.lineNumbersLeft + layout.lineNumbersWidth + 4), targetVertRange); - - - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + // The gutter view container (pill) const pillOffset = this._verticalOffset.read(reader); - const pillRect = targetRect.withHeight(lineHeight).withWidth(22).translateY(pillOffset); + let pillRect = targetRect.withHeight(lineHeight).withWidth(22).translateY(pillOffset); const pillRectMoved = pillRect.moveToBeContainedIn(viewPortWithStickyScroll); const rect = targetRect; - const iconRect = (targetRect.containsRect(pillRectMoved)) + // Move pill to be in viewport if it is not + pillRect = (targetRect.containsRect(pillRectMoved)) ? pillRectMoved : pillRectMoved.moveToBeContainedIn(fullViewPort.intersect(targetRect.union(fullViewPort.withHeight(lineHeight)))!); //viewPortWithStickyScroll.intersect(rect)!; + // docked = pill was already in the viewport + const docked = rect.containsRect(pillRect) && viewPortWithStickyScroll.containsRect(pillRect); + let iconDirecion = targetRect.containsRect(pillRect) ? + 'right' as const + : pillRect.top > targetRect.top ? + 'top' as const : + 'bottom' as const; - const docked = rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect); - let iconDirecion = (targetRect.containsRect(iconRect) ? 'right' as const - : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const); + // Grow icon the the whole glyph margin area if it is docked + let lineNumberRect = pillRect.withWidth(0); + let iconRect = pillRect; + if (docked && pillRect.top === targetRect.top + pillOffset) { + pillRect = pillRect.withWidth(layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - leftPadding - rightPadding); + lineNumberRect = pillRect.intersectHorizontal(new OffsetRange(0, layout.lineNumbersLeft + layout.lineNumbersWidth - leftPadding - 1)); + iconRect = iconRect.translateX(lineNumberRect.width); + } let icon; if (docked && (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader))) { @@ -179,6 +263,9 @@ export class InlineEditsGutterIndicator extends Disposable { rotation, docked, iconRect, + pillRect, + lineHeight, + lineNumberRect, }; }); @@ -217,6 +304,7 @@ export class InlineEditsGutterIndicator extends Disposable { }) as HoverWidget | undefined; if (h) { this._hoverVisible.set(true, undefined); + disposableStore.add(this._editorObs.editor.onDidScrollChange(() => h.dispose())); disposableStore.add(h.onDispose(() => { this._hoverVisible.set(false, undefined); disposableStore.dispose(); @@ -262,23 +350,37 @@ export class InlineEditsGutterIndicator extends Disposable { ref: this._iconRef, onmouseenter: () => { // TODO show hover when hovering ghost text etc. - this._isHoveredOverIcon.set(true, undefined); this._showHover(); }, - onmouseleave: () => { this._isHoveredOverIcon.set(false, undefined); }, style: { cursor: 'pointer', zIndex: '1000', position: 'absolute', - backgroundColor: this._gutterIndicatorBackgroundColor, - ['--vscodeIconForeground' as any]: this._gutterIndicatorForegroundColor, + backgroundColor: this._gutterIndicatorStyles.map(v => v.background), + ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), + border: this._gutterIndicatorStyles.map(v => `1px solid ${v.border}`), + boxSizing: 'border-box', borderRadius: '4px', display: 'flex', justifyContent: 'center', - transition: 'background-color 0.2s ease-in-out', - ...rectToProps(reader => layout.read(reader).iconRect), + transition: 'background-color 0.2s ease-in-out, width 0.2s ease-in-out', + ...rectToProps(reader => layout.read(reader).pillRect), } }, [ + n.div({ + className: 'line-number', + style: { + lineHeight: layout.map(l => `${l.lineHeight}px`), + display: layout.map(l => l.lineNumberRect.width > 0 ? 'flex' : 'none'), + alignItems: 'center', + justifyContent: 'flex-end', + width: layout.map(l => l.lineNumberRect.width), + height: '100%', + color: this._gutterIndicatorStyles.map(v => v.foreground), + } + }, + this._lineNumberToRender + ), n.div({ style: { rotate: layout.map(i => `${i.rotation}deg`), @@ -286,6 +388,8 @@ export class InlineEditsGutterIndicator extends Disposable { display: 'flex', alignItems: 'center', justifyContent: 'center', + height: '100%', + width: layout.map(l => `${l.iconRect.width}px`), } }, [ layout.map(i => i.icon), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index 8588439409b..656469803f8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -66,9 +66,19 @@ export const inlineEditIndicatorPrimaryForeground = registerColor( buttonForeground, localize('inlineEdit.gutterIndicator.primaryForeground', 'Foreground color for the primary inline edit gutter indicator.') ); +export const inlineEditIndicatorPrimaryBorder = registerColor( + 'inlineEdit.gutterIndicator.primaryBorder', + buttonBackground, + localize('inlineEdit.gutterIndicator.primaryBorder', 'Border color for the primary inline edit gutter indicator.') +); export const inlineEditIndicatorPrimaryBackground = registerColor( 'inlineEdit.gutterIndicator.primaryBackground', - buttonBackground, + { + light: transparent(inlineEditIndicatorPrimaryBorder, 0.5), + dark: transparent(inlineEditIndicatorPrimaryBorder, 0.4), + hcDark: transparent(inlineEditIndicatorPrimaryBorder, 0.4), + hcLight: transparent(inlineEditIndicatorPrimaryBorder, 0.5), + }, localize('inlineEdit.gutterIndicator.primaryBackground', 'Background color for the primary inline edit gutter indicator.') ); @@ -77,9 +87,14 @@ export const inlineEditIndicatorSecondaryForeground = registerColor( buttonSecondaryForeground, localize('inlineEdit.gutterIndicator.secondaryForeground', 'Foreground color for the secondary inline edit gutter indicator.') ); +export const inlineEditIndicatorSecondaryBorder = registerColor( + 'inlineEdit.gutterIndicator.secondaryBorder', + buttonSecondaryBackground, + localize('inlineEdit.gutterIndicator.secondaryBorder', 'Border color for the secondary inline edit gutter indicator.') +); export const inlineEditIndicatorSecondaryBackground = registerColor( 'inlineEdit.gutterIndicator.secondaryBackground', - buttonSecondaryBackground, + inlineEditIndicatorSecondaryBorder, localize('inlineEdit.gutterIndicator.secondaryBackground', 'Background color for the secondary inline edit gutter indicator.') ); @@ -88,9 +103,14 @@ export const inlineEditIndicatorsuccessfulForeground = registerColor( buttonForeground, localize('inlineEdit.gutterIndicator.successfulForeground', 'Foreground color for the successful inline edit gutter indicator.') ); +export const inlineEditIndicatorsuccessfulBorder = registerColor( + 'inlineEdit.gutterIndicator.successfulBorder', + buttonBackground, + localize('inlineEdit.gutterIndicator.successfulBorder', 'Border color for the successful inline edit gutter indicator.') +); export const inlineEditIndicatorsuccessfulBackground = registerColor( 'inlineEdit.gutterIndicator.successfulBackground', - { light: '#2e825c', dark: '#2e825c', hcLight: '#2e825c', hcDark: '#2e825c' }, + inlineEditIndicatorsuccessfulBorder, localize('inlineEdit.gutterIndicator.successfulBackground', 'Background color for the successful inline edit gutter indicator.') );