11import { Position , Range } from '@sourcegraph/extension-api-types'
22import { isEqual } from 'lodash'
33import {
4+ animationFrameScheduler ,
45 combineLatest ,
56 concat ,
67 EMPTY ,
@@ -21,15 +22,18 @@ import {
2122 delay ,
2223 distinctUntilChanged ,
2324 filter ,
25+ first ,
2426 map ,
27+ mapTo ,
28+ observeOn ,
2529 share ,
2630 switchMap ,
2731 takeUntil ,
2832 withLatestFrom ,
2933} from 'rxjs/operators'
3034import { Key } from 'ts-key-enum'
3135import { asError , ErrorLike , isErrorLike } from './errors'
32- import { scrollIntoCenterIfNeeded } from './helpers'
36+ import { elementOverlaps , scrollIntoCenterIfNeeded } from './helpers'
3337import { calculateOverlayPosition } from './overlay_position'
3438import { DiffPart , PositionEvent , SupportedMouseEvent } from './positions'
3539import { createObservableStateContainer } from './state'
@@ -178,6 +182,15 @@ export interface EventOptions<C extends object> {
178182 adjustPosition ?: PositionAdjuster < C >
179183 dom : DOMFunctions
180184 codeViewId : symbol
185+
186+ /**
187+ * An array of elements used to hide the hover overlay if any of them
188+ * overlap with the hovered token. Overlapping is checked in reaction to scroll events.
189+ *
190+ * scrollBoundaries are typically elements with a lower z-index than the hover overlay
191+ * but a higher z-index than the code view, such as a sticky file header.
192+ */
193+ scrollBoundaries ?: HTMLElement [ ]
181194}
182195
183196/**
@@ -607,6 +620,84 @@ export function createHoverifier<C extends object, D, A>({
607620 distinctUntilChanged ( ( a , b ) => isEqual ( a . position , b . position ) )
608621 )
609622
623+ /**
624+ * An Observable of scroll events on the document.
625+ */
626+ const scrollEvents = fromEvent ( document , 'scroll' ) . pipe (
627+ observeOn ( animationFrameScheduler ) ,
628+ share ( )
629+ )
630+
631+ /**
632+ * Returns the highlighted range for the given hover result and position.
633+ *
634+ * Returns `undefined` if the hover result is not successful.
635+ *
636+ * Uses the range specified by the hover result if present, or `position` oherwise,
637+ * which will be expanded into a full token in getTokenAtPosition().
638+ */
639+ const getHighlightedRange = ( {
640+ hoverOrError,
641+ position,
642+ } : {
643+ hoverOrError ?: typeof LOADING | ( HoverAttachment & D ) | ErrorLike | null
644+ position : Position | undefined
645+ } ) : Range | undefined => {
646+ if ( hoverOrError && ! isErrorLike ( hoverOrError ) && hoverOrError !== LOADING ) {
647+ if ( hoverOrError . range ) {
648+ // The result is 0-indexed; the code view is treated as 1-indexed.
649+ return {
650+ start : {
651+ line : hoverOrError . range . start . line + 1 ,
652+ character : hoverOrError . range . start . character + 1 ,
653+ } ,
654+ end : {
655+ line : hoverOrError . range . end . line + 1 ,
656+ character : hoverOrError . range . end . character + 1 ,
657+ } ,
658+ }
659+ }
660+ if ( position ) {
661+ return { start : position , end : position }
662+ }
663+ }
664+ return undefined
665+ }
666+
667+ /**
668+ * Returns an Observable that emits the hover result immediately,
669+ * and will emit a result resetting the hover when the hoveredTokenElement intersects
670+ * with the scrollBoundaries.
671+ */
672+ const resetOnBoundaryIntersection = ( {
673+ hoveredTokenElement,
674+ scrollBoundaries,
675+ ...rest
676+ } : Omit < InternalHoverifierState < C , D , A > , 'mouseIsMoving' | 'hoverOverlayIsFixed' > &
677+ Omit < EventOptions < C > , 'resolveContext' | 'dom' > & { codeView : HTMLElement } ) : Observable <
678+ Omit < InternalHoverifierState < C , D , A > , 'mouseIsMoving' | 'hoverOverlayIsFixed' > & { codeView : HTMLElement }
679+ > => {
680+ const result = of ( { hoveredTokenElement, ...rest } )
681+ if ( ! hoveredTokenElement || ! scrollBoundaries ) {
682+ return result
683+ }
684+ return merge (
685+ result ,
686+ scrollEvents . pipe (
687+ filter ( ( ) => scrollBoundaries . some ( elementOverlaps ( hoveredTokenElement ) ) ) ,
688+ first ( ) ,
689+ mapTo ( {
690+ ...rest ,
691+ hoveredTokenElement,
692+ hoverOverlayIsFixed : false ,
693+ hoverOrError : undefined ,
694+ hoveredToken : undefined ,
695+ actionsOrError : undefined ,
696+ } )
697+ )
698+ )
699+ }
700+
610701 /**
611702 * For every position, emits an Observable with new values for the `hoverOrError` state.
612703 * This is a higher-order Observable (Observable that emits Observables).
@@ -619,6 +710,7 @@ export function createHoverifier<C extends object, D, A>({
619710 adjustPosition ?: PositionAdjuster < C >
620711 codeView : HTMLElement
621712 codeViewId : symbol
713+ scrollBoundaries ?: HTMLElement [ ]
622714 hoverOrError ?: typeof LOADING | ( HoverAttachment & D ) | ErrorLike | null
623715 position ?: HoveredToken & C
624716 part ?: DiffPart
@@ -662,7 +754,7 @@ export function createHoverifier<C extends object, D, A>({
662754 hoverObservables
663755 . pipe (
664756 switchMap ( hoverObservable => hoverObservable ) ,
665- switchMap ( ( { hoverOrError, position, adjustPosition, ...rest } ) => {
757+ switchMap ( ( { hoverOrError, position, adjustPosition, codeView , part , ...rest } ) => {
666758 let pos =
667759 hoverOrError &&
668760 hoverOrError !== LOADING &&
@@ -673,7 +765,13 @@ export function createHoverifier<C extends object, D, A>({
673765 : position
674766
675767 if ( ! pos ) {
676- return of ( { hoverOrError, position : undefined as Position | undefined , ...rest } )
768+ return of ( {
769+ hoverOrError,
770+ codeView,
771+ part,
772+ position : undefined as Position | undefined ,
773+ ...rest ,
774+ } )
677775 }
678776
679777 // The requested position is is 0-indexed; the code here is currently 1-indexed
@@ -683,66 +781,51 @@ export function createHoverifier<C extends object, D, A>({
683781 const adjustingPosition = adjustPosition
684782 ? from (
685783 adjustPosition ( {
686- codeView : rest . codeView ,
784+ codeView,
687785 direction : AdjustmentDirection . ActualToCodeView ,
688786 position : {
689787 ...pos ,
690- part : rest . part ,
788+ part,
691789 } ,
692790 } )
693791 )
694792 : of ( pos )
695793
696- return adjustingPosition . pipe ( map ( position => ( { position, hoverOrError, ...rest } ) ) )
794+ return adjustingPosition . pipe (
795+ map ( position => ( { position, hoverOrError, codeView, part, ...rest } ) )
796+ )
797+ } ) ,
798+ switchMap ( ( { scrollBoundaries, hoverOrError, position, codeView, codeViewId, dom, part } ) => {
799+ const highlightedRange = getHighlightedRange ( { hoverOrError, position } )
800+ const hoveredTokenElement = highlightedRange
801+ ? getTokenAtPosition ( codeView , highlightedRange . start , dom , part , tokenize )
802+ : undefined
803+ return resetOnBoundaryIntersection ( {
804+ scrollBoundaries,
805+ codeViewId,
806+ codeView,
807+ highlightedRange,
808+ hoverOrError,
809+ hoveredTokenElement,
810+ hoverOverlayPosition : undefined ,
811+ } )
697812 } )
698813 )
699- . subscribe ( ( { hoverOrError, position, codeView, codeViewId, dom, part } ) => {
700- // Update the highlighted token if the hover result is successful. If the hover result specifies a
701- // range, use that; otherwise use the hover position (which will be expanded into a full token in
702- // getTokenAtPosition).
703- let highlightedRange : Range | undefined
704- if ( hoverOrError && ! isErrorLike ( hoverOrError ) && hoverOrError !== LOADING ) {
705- if ( hoverOrError . range ) {
706- // The result is 0-indexed; the code view is treated as 1-indexed.
707- highlightedRange = {
708- start : {
709- line : hoverOrError . range . start . line + 1 ,
710- character : hoverOrError . range . start . character + 1 ,
711- } ,
712- end : {
713- line : hoverOrError . range . end . line + 1 ,
714- character : hoverOrError . range . end . character + 1 ,
715- } ,
716- }
717- } else if ( position ) {
718- highlightedRange = { start : position , end : position }
719- }
720- }
721-
814+ . subscribe ( ( { codeView, highlightedRange, hoveredTokenElement, ...rest } ) => {
722815 container . update ( {
723- codeViewId,
724- hoverOrError,
725816 highlightedRange,
726- // Reset the hover position, it's gonna be repositioned after the hover was rendered
727- hoverOverlayPosition : undefined ,
817+ hoveredTokenElement ,
818+ ... rest ,
728819 } )
729-
730820 // Ensure the previously highlighted range is not highlighted and the new highlightedRange (if any)
731821 // is highlighted.
732822 const currentHighlighted = codeView . querySelector ( '.selection-highlight' )
733823 if ( currentHighlighted ) {
734824 currentHighlighted . classList . remove ( 'selection-highlight' )
735825 }
736- if ( ! highlightedRange ) {
737- container . update ( { hoveredTokenElement : undefined } )
738- return
739- }
740- const token = getTokenAtPosition ( codeView , highlightedRange . start , dom , part , tokenize )
741- container . update ( { hoveredTokenElement : token } )
742- if ( ! token ) {
743- return
826+ if ( hoveredTokenElement ) {
827+ hoveredTokenElement . classList . add ( 'selection-highlight' )
744828 }
745- token . classList . add ( 'selection-highlight' )
746829 } )
747830 )
748831
0 commit comments