@@ -10,36 +10,39 @@ import {
1010 share ,
1111 switchMap ,
1212 takeUntil ,
13- tap ,
1413 withLatestFrom ,
1514} from 'rxjs/operators'
1615import { Key } from 'ts-key-enum'
1716import { Position } from 'vscode-languageserver-types'
1817import { asError , ErrorLike } from './errors'
19- import { propertyIsDefined } from './helpers'
18+ import { isDefined } from './helpers'
2019import { overlayUIHasContent , scrollIntoCenterIfNeeded } from './helpers'
2120import { HoverOverlayProps , isJumpURL } from './HoverOverlay'
2221import { calculateOverlayPosition } from './overlay_position'
22+ import { PositionEvent , SupportedMouseEvent } from './positions'
2323import { createObservableStateContainer } from './state'
2424import {
25- convertCodeCellIdempotent ,
2625 convertNode ,
2726 findElementWithOffset ,
2827 getRowInCodeElement ,
2928 getRowsInRange ,
30- getTableDataCell ,
3129 getTokenAtPosition ,
3230 HoveredToken ,
3331 locateTarget ,
3432} from './token_position'
3533import { EMODENOTFOUND , HoverMerged , LOADING } from './types'
3634import { FileSpec , LineOrPositionOrRange , RepoSpec , ResolvedRevSpec , RevSpec } from './url'
3735
36+ export { HoveredToken }
37+
3838interface HoverifierOptions {
3939 /**
4040 * Emit the HoverOverlay element on this after it was rerendered when its content changed and it needs to be repositioned.
4141 */
42- hoverOverlayRerenders : Observable < { hoverOverlayElement : HTMLElement ; scrollElement : HTMLElement } >
42+ hoverOverlayRerenders : Observable < {
43+ hoverOverlayElement : HTMLElement
44+ scrollElement : HTMLElement
45+ } >
4346
4447 /**
4548 * Emit on this Observable when the Go-To-Definition button in the HoverOverlay was clicked
@@ -92,9 +95,7 @@ export interface Hoverifier {
9295}
9396
9497export interface HoverifyOptions {
95- codeMouseMoves : Observable < MouseEvent >
96- codeMouseOvers : Observable < MouseEvent >
97- codeClicks : Observable < MouseEvent >
98+ positionEvents : Observable < PositionEvent >
9899
99100 /**
100101 * Emit on this Observable to trigger the overlay on a position in this code view.
@@ -222,15 +223,20 @@ export const createHoverifier = ({
222223 selectedPosition : undefined ,
223224 } )
224225
225- interface MouseEventTrigger {
226- event : MouseEvent
226+ interface MouseEventTrigger extends PositionEvent {
227227 resolveContext : ContextResolver
228228 }
229229
230230 // These Subjects aggregate all events from all hoverified code views
231- const allCodeMouseMoves = new Subject < MouseEventTrigger > ( )
232- const allCodeMouseOvers = new Subject < MouseEventTrigger > ( )
233- const allCodeClicks = new Subject < MouseEventTrigger > ( )
231+ const allPositionsFromEvents = new Subject < MouseEventTrigger > ( )
232+
233+ const isEventType = < T extends SupportedMouseEvent > ( type : T ) => (
234+ event : MouseEventTrigger
235+ ) : event is MouseEventTrigger & { eventType : T } => event . eventType === type
236+ const allCodeMouseMoves = allPositionsFromEvents . pipe ( filter ( isEventType ( 'mousemove' ) ) )
237+ const allCodeMouseOvers = allPositionsFromEvents . pipe ( filter ( isEventType ( 'mouseover' ) ) )
238+ const allCodeClicks = allPositionsFromEvents . pipe ( filter ( isEventType ( 'click' ) ) )
239+
234240 const allPositionJumps = new Subject < {
235241 position : LineOrPositionOrRange
236242 codeElement : HTMLElement
@@ -270,21 +276,10 @@ export const createHoverifier = ({
270276 )
271277
272278 const codeMouseOverTargets = allCodeMouseOvers . pipe (
273- filter ( ( { event } ) => event . currentTarget !== null ) ,
274279 map ( ( { event, ...rest } ) => ( {
275280 target : event . target as HTMLElement ,
276- codeElement : event . currentTarget as HTMLElement ,
277281 ...rest ,
278282 } ) ) ,
279- // SIDE EFFECT (but idempotent)
280- // If not done for this cell, wrap the tokens in this cell to enable finding the precise positioning.
281- // This may be possible in other ways (looking at mouse position and rendering characters), but it works
282- tap ( ( { target, codeElement } ) => {
283- const td = getTableDataCell ( target , codeElement )
284- if ( td !== undefined ) {
285- convertCodeCellIdempotent ( td )
286- }
287- } ) ,
288283 debounceTime ( 50 ) ,
289284 // Do not consider mouseovers while overlay is pinned
290285 filter ( ( ) => ! container . values . hoverOverlayIsFixed ) ,
@@ -295,18 +290,13 @@ export const createHoverifier = ({
295290 filter ( ( { event } ) => event . currentTarget !== null ) ,
296291 map ( ( { event, ...rest } ) => ( {
297292 target : event . target as HTMLElement ,
298- codeElement : event . currentTarget as HTMLElement ,
299293 ...rest ,
300294 } ) ) ,
301295 share ( )
302296 )
303297
304298 /** Emits DOM elements at new positions found in the URL */
305- const jumpTargets : Observable < {
306- target : HTMLElement
307- codeElement : HTMLElement
308- resolveContext : ContextResolver
309- } > = allPositionJumps . pipe (
299+ const jumpTargets = allPositionJumps . pipe (
310300 // Only use line and character for comparison
311301 map ( ( { position : { line, character } , ...rest } ) => ( { position : { line, character } , ...rest } ) ) ,
312302 // Ignore same values
@@ -318,16 +308,25 @@ export const createHoverifier = ({
318308 map ( ( { position, codeElement, ...rest } ) => {
319309 const row = getRowInCodeElement ( codeElement , position . line )
320310 if ( ! row ) {
321- return { target : undefined , codeElement , ... rest }
311+ return undefined
322312 }
323313 const cell = row . cells [ 1 ] !
324314 const target = findElementWithOffset ( cell , position . character )
325315 if ( ! target ) {
326316 console . warn ( 'Could not find target for position in file' , position )
317+ return undefined
318+ }
319+ // TODO locateTarget is purely needed here to get `hoveredToken.part` for diffs
320+ // We should define a function that takes care of _only_ figuring out the `part`
321+ // so we don't have to use locateTarget
322+ const hoveredToken = locateTarget ( target , codeElement , false )
323+ if ( ! Position . is ( hoveredToken ) ) {
324+ console . warn ( 'Could not find target for position in file' , position )
325+ return undefined
327326 }
328- return { target, codeElement , ... rest }
327+ return { ... rest , eventType : 'jump' as 'jump' , target, position : hoveredToken , codeElement }
329328 } ) ,
330- filter ( propertyIsDefined ( 'target' ) )
329+ filter ( isDefined )
331330 )
332331
333332 // REPOSITIONING
@@ -349,46 +348,21 @@ export const createHoverifier = ({
349348 container . update ( { hoverOverlayPosition } )
350349 } )
351350 )
352- /** Emits new positions at which a tooltip needs to be shown from clicks, mouseovers and URL changes. */
353- const positions : Observable < {
354- /**
355- * The 1-indexed position at which a new tooltip is to be shown,
356- * or undefined when a target was hovered/clicked that does not correspond to a position (e.g. after the end of the line)
357- */
358- position ?: HoveredToken & HoveredTokenContext
359- /**
360- * True if the tooltip should be pinned once the hover came back and is non-empty.
361- * This depends on what triggered the new position.
362- * We remember it because the pinning is deferred to when we have a result,
363- * so we don't pin empty (i.e. invisible) hovers.
364- */
365- pinIfNonEmpty : boolean
366- codeElement : HTMLElement
367- } > = merge (
368- // Should unpin the tooltip even if hover cames back non-empty
369- codeMouseOverTargets . pipe ( map ( data => ( { ...data , pinIfNonEmpty : false } ) ) ) ,
370- // When the location changes and includes a line/column pair, use that target
371- // Should pin the tooltip if hover cames back non-empty
372- jumpTargets . pipe ( map ( data => ( { ...data , pinIfNonEmpty : true } ) ) ) ,
373- // Should pin the tooltip if hover cames back non-empty
374- codeClickTargets . pipe ( map ( data => ( { ...data , pinIfNonEmpty : true } ) ) )
375- ) . pipe (
376- // Find out the position that was hovered over
377- map ( ( { target, codeElement, resolveContext, ...rest } ) => {
378- const hoveredToken = locateTarget ( target , codeElement , false )
379- const position = Position . is ( hoveredToken )
380- ? { ...hoveredToken , ...resolveContext ( hoveredToken ) }
381- : undefined
382- return { position, codeElement, ...rest }
383- } ) ,
351+
352+ /** Emits new positions including context at which a tooltip needs to be shown from clicks, mouseovers and URL changes. */
353+ const resolvedPositions = merge ( codeMouseOverTargets , jumpTargets , codeClickTargets ) . pipe (
354+ map ( ( { position, resolveContext, ...rest } ) => ( {
355+ ...rest ,
356+ position : Position . is ( position ) ? { ...position , ...resolveContext ( position ) } : undefined ,
357+ } ) ) ,
384358 share ( )
385359 )
386360
387361 /**
388362 * For every position, emits an Observable with new values for the `hoverOrError` state.
389363 * This is a higher-order Observable (Observable that emits Observables).
390364 */
391- const hoverObservables = positions . pipe (
365+ const hoverObservables = resolvedPositions . pipe (
392366 map ( ( { position, codeElement } ) => {
393367 if ( ! position ) {
394368 return of ( { codeElement, hoverOrError : undefined } )
@@ -445,7 +419,7 @@ export const createHoverifier = ({
445419 )
446420 // Telemetry for hovers
447421 subscription . add (
448- zip ( positions , hoverObservables )
422+ zip ( resolvedPositions , hoverObservables )
449423 . pipe (
450424 distinctUntilChanged ( ( [ positionA ] , [ positionB ] ) => isEqual ( positionA , positionB ) ) ,
451425 switchMap ( ( [ position , hoverObservable ] ) => hoverObservable ) ,
@@ -460,7 +434,7 @@ export const createHoverifier = ({
460434 * For every position, emits an Observable that emits new values for the `definitionURLOrError` state.
461435 * This is a higher-order Observable (Observable that emits Observables).
462436 */
463- const definitionObservables = positions . pipe (
437+ const definitionObservables = resolvedPositions . pipe (
464438 // Fetch the definition location for that position
465439 map ( ( { position } ) => {
466440 if ( ! position ) {
@@ -506,11 +480,11 @@ export const createHoverifier = ({
506480 //
507481 // zip together a position and the hover and definition fetches it triggered
508482 subscription . add (
509- zip ( positions , hoverObservables , definitionObservables )
483+ zip ( resolvedPositions , hoverObservables , definitionObservables )
510484 . pipe (
511- switchMap ( ( [ { pinIfNonEmpty } , hoverObservable , definitionObservable ] ) => {
485+ switchMap ( ( [ { eventType } , hoverObservable , definitionObservable ] ) => {
512486 // If the position was triggered by a mouseover, never pin
513- if ( ! pinIfNonEmpty ) {
487+ if ( eventType !== 'click' && eventType !== 'jump' ) {
514488 return [ false ]
515489 }
516490 // combine the latest values for them, so we have access to both values
@@ -521,8 +495,7 @@ export const createHoverifier = ({
521495 overlayUIHasContent ( { hoverOrError, definitionURLOrError } )
522496 )
523497 )
524- } ) ,
525- share ( )
498+ } )
526499 )
527500 . subscribe ( hoverOverlayIsFixed => {
528501 container . update ( { hoverOverlayIsFixed } )
@@ -579,7 +552,7 @@ export const createHoverifier = ({
579552 } )
580553 )
581554 subscription . add (
582- positions . subscribe ( ( { position } ) => {
555+ resolvedPositions . subscribe ( ( { position } ) => {
583556 container . update ( {
584557 hoveredToken : position ,
585558 // On every new target (from mouseover or click) hide the j2d loader/error/not found UI again
@@ -601,19 +574,14 @@ export const createHoverifier = ({
601574 map ( internalToExternalState ) ,
602575 distinctUntilChanged ( ( a , b ) => isEqual ( a , b ) )
603576 ) ,
604- hoverify ( {
605- codeMouseMoves,
606- codeMouseOvers,
607- codeClicks,
608- positionJumps,
609- resolveContext,
610- } : HoverifyOptions ) : Subscription {
577+ hoverify ( { positionEvents, positionJumps, resolveContext } : HoverifyOptions ) : Subscription {
611578 const subscription = new Subscription ( )
612- const eventWithContextResolver = map ( ( event : MouseEvent ) => ( { event, resolveContext } ) )
579+ const eventWithContextResolver = map ( ( event : PositionEvent ) => ( {
580+ ...event ,
581+ resolveContext,
582+ } ) )
613583 // Broadcast all events from this code view
614- subscription . add ( codeMouseMoves . pipe ( eventWithContextResolver ) . subscribe ( allCodeMouseMoves ) )
615- subscription . add ( codeMouseOvers . pipe ( eventWithContextResolver ) . subscribe ( allCodeMouseOvers ) )
616- subscription . add ( codeClicks . pipe ( eventWithContextResolver ) . subscribe ( allCodeClicks ) )
584+ subscription . add ( positionEvents . pipe ( eventWithContextResolver ) . subscribe ( allPositionsFromEvents ) )
617585 subscription . add ( positionJumps . pipe ( map ( jump => ( { ...jump , resolveContext } ) ) ) . subscribe ( allPositionJumps ) )
618586 return subscription
619587 } ,
0 commit comments