Skip to content
This repository was archived by the owner on Nov 25, 2021. It is now read-only.

Commit 178b32e

Browse files
authored
feat: add optional position adjuster to HoverifierOptions (#38)
This commit adds the option to adjust positions used in codeintellify. This is needed because some code hosts change whitespace to fit their needs in the UI. In order to get correct code intelligence we need to be able to map the position that we found in the DOM to the actual position of that token in it's source.
1 parent 849914f commit 178b32e

File tree

2 files changed

+192
-9
lines changed

2 files changed

+192
-9
lines changed

src/hoverifier.test.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { Position } from 'vscode-languageserver-types'
66

77
import { noop } from 'lodash'
88
import { propertyIsDefined } from './helpers'
9-
import { createHoverifier, LOADER_DELAY, TOOLTIP_DISPLAY_DELAY } from './hoverifier'
9+
import {
10+
AdjustmentDirection,
11+
createHoverifier,
12+
LOADER_DELAY,
13+
PositionAdjuster,
14+
TOOLTIP_DISPLAY_DELAY,
15+
} from './hoverifier'
1016
import { HoverOverlayProps } from './HoverOverlay'
1117
import { findPositionsFromEvents } from './positions'
1218
import { CodeViewProps, DOM } from './testutils/dom'
@@ -108,4 +114,83 @@ describe('Hoverifier', () => {
108114
})
109115
}
110116
})
117+
118+
/**
119+
* This test ensures that the adjustPosition options is being called in the ways we expect. This test is actually not the best way to ensure the feature
120+
* works as expected. This is a good example of a bad side effect of how the main `hoverifier.ts` file is too tightly integrated with itself. Ideally, I'd be able to assert
121+
* that the effected positions have actually been adjusted as intended but this is impossible with the current implementation. We can assert that the `HoverFetcher` and `JumpURLFetcher`s
122+
* have the adjusted positions (AdjustmentDirection.CodeViewToActual). However, we cannot reliably assert that the code "highlighting" the token has the position adjusted (AdjustmentDirection.ActualToCodeView).
123+
*/
124+
it('PositionAdjuster gets called when expected', () => {
125+
for (const codeView of testcases) {
126+
const scheduler = new TestScheduler((a, b) => chai.assert.deepEqual(a, b))
127+
128+
scheduler.run(({ cold, expectObservable }) => {
129+
const adjustmentDirections = new Subject<AdjustmentDirection>()
130+
131+
const fetchHover = createStubHoverFetcher({})
132+
const fetchJumpURL = createStubJumpURLFetcher('def')
133+
134+
const adjustPosition: PositionAdjuster = ({ direction, position }) => {
135+
adjustmentDirections.next(direction)
136+
137+
return of(position)
138+
}
139+
140+
const hoverifier = createHoverifier({
141+
closeButtonClicks: new Observable<MouseEvent>(),
142+
goToDefinitionClicks: new Observable<MouseEvent>(),
143+
hoverOverlayElements: of(null),
144+
hoverOverlayRerenders: EMPTY,
145+
fetchHover,
146+
fetchJumpURL,
147+
pushHistory: noop,
148+
logTelemetryEvent: noop,
149+
})
150+
151+
const positionJumps = new Subject<{
152+
position: Position
153+
codeView: HTMLElement
154+
scrollElement: HTMLElement
155+
}>()
156+
157+
const positionEvents = of(codeView.codeView).pipe(findPositionsFromEvents(codeView))
158+
159+
const subscriptions = new Subscription()
160+
161+
subscriptions.add(hoverifier)
162+
subscriptions.add(
163+
hoverifier.hoverify({
164+
dom: codeView,
165+
positionEvents,
166+
positionJumps,
167+
adjustPosition,
168+
resolveContext: () => codeView.revSpec,
169+
})
170+
)
171+
172+
const inputDiagram = 'ab'
173+
// There is probably a bug in code that is unrelated to this feature that is causing the PositionAdjuster to be called an extra time.
174+
// It should look like '-(ba)'. That is, we adjust the position from CodeViewToActual for the LSP fetches and then back from CodeViewToActual
175+
// for highlighting the token in the DOM.
176+
const outputDiagram = 'a(ba)'
177+
178+
const outputValues: {
179+
[key: string]: AdjustmentDirection
180+
} = {
181+
a: AdjustmentDirection.ActualToCodeView,
182+
b: AdjustmentDirection.CodeViewToActual,
183+
}
184+
185+
cold(inputDiagram).subscribe(() =>
186+
clickPositionImpure(codeView, {
187+
line: 1,
188+
character: 1,
189+
})
190+
)
191+
192+
expectObservable(adjustmentDirections).toBe(outputDiagram, outputValues)
193+
})
194+
}
195+
})
111196
})

src/hoverifier.ts

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,37 @@ export interface PositionJump {
115115
scrollElement: HTMLElement
116116
}
117117

118+
/**
119+
* The possible directions to adjust a position in.
120+
*/
121+
export enum AdjustmentDirection {
122+
/** Adjusting the position from what is found on the page to what it would be in the actual file. */
123+
CodeViewToActual,
124+
/** Adjusting the position from what is in the actual file to what would be found on the page. */
125+
ActualToCodeView,
126+
}
127+
128+
export interface AdjustPositionProps {
129+
/** The code view the token is in. */
130+
codeView: HTMLElement
131+
/** The position the token is at. */
132+
position: HoveredToken & HoveredTokenContext
133+
/** The direction the adjustment should go. */
134+
direction: AdjustmentDirection
135+
}
136+
137+
/**
138+
* Function to adjust positions coming into and out of hoverifier. It can be used to correct the position used in HoverFetcher and
139+
* JumpURLFetcher requests and the position of th etoken to highlight in the code view. This is useful for code hosts that convert whitespace.
140+
*/
141+
export type PositionAdjuster = (props: AdjustPositionProps) => Observable<Position>
142+
118143
/**
119144
* HoverifyOptions that need to be included internally with every event
120145
*/
121146
export interface EventOptions {
122147
resolveContext: ContextResolver
148+
adjustPosition?: PositionAdjuster
123149
dom: DOMFunctions
124150
}
125151

@@ -296,6 +322,24 @@ export const createHoverifier = ({
296322
debounceTime(50),
297323
// Do not consider mouseovers while overlay is pinned
298324
filter(() => !container.values.hoverOverlayIsFixed),
325+
switchMap(
326+
({ adjustPosition, codeView, resolveContext, position, ...rest }) =>
327+
adjustPosition && position
328+
? adjustPosition({
329+
codeView,
330+
position: { ...position, ...resolveContext(position) },
331+
direction: AdjustmentDirection.CodeViewToActual,
332+
}).pipe(
333+
map(({ line, character }) => ({
334+
codeView,
335+
resolveContext,
336+
position: { ...position, line, character },
337+
adjustPosition,
338+
...rest,
339+
}))
340+
)
341+
: of({ adjustPosition, codeView, resolveContext, position, ...rest })
342+
),
299343
share()
300344
)
301345

@@ -305,6 +349,24 @@ export const createHoverifier = ({
305349
target: event.target as HTMLElement,
306350
...rest,
307351
})),
352+
switchMap(
353+
({ adjustPosition, codeView, resolveContext, position, ...rest }) =>
354+
adjustPosition && position
355+
? adjustPosition({
356+
codeView,
357+
position: { ...position, ...resolveContext(position) },
358+
direction: AdjustmentDirection.CodeViewToActual,
359+
}).pipe(
360+
map(({ line, character }) => ({
361+
codeView,
362+
resolveContext,
363+
position: { ...position, line, character },
364+
adjustPosition,
365+
...rest,
366+
}))
367+
)
368+
: of({ adjustPosition, codeView, resolveContext, position, ...rest })
369+
),
308370
share()
309371
)
310372

@@ -370,7 +432,13 @@ export const createHoverifier = ({
370432
const hoverObservables = resolvedPositions.pipe(
371433
map(({ position, ...rest }) => {
372434
if (!position) {
373-
return of({ ...rest, hoverOrError: null, part: undefined })
435+
return of({
436+
// Typescript seems to give up on type inference if we don't explicitely declare the types here.
437+
hoverOrError: null as 'loading' | HoverMerged | Error | null | undefined,
438+
position: undefined as (HoveredToken & HoveredTokenContext) | undefined,
439+
part: undefined,
440+
...rest,
441+
})
374442
}
375443
// Fetch the hover for that position
376444
const hoverFetch = fetchHover(position).pipe(
@@ -392,15 +460,46 @@ export const createHoverifier = ({
392460
takeUntil(hoverFetch)
393461
),
394462
hoverFetch
395-
).pipe(map(hoverOrError => ({ ...rest, hoverOrError, part: position.part })))
463+
).pipe(
464+
map(hoverOrError => ({
465+
...rest,
466+
position,
467+
hoverOrError,
468+
part: position.part,
469+
}))
470+
)
396471
}),
397472
share()
398473
)
399474
// Highlight the hover range returned by the language server
400475
subscription.add(
401476
hoverObservables
402-
.pipe(switchMap(hoverObservable => hoverObservable))
403-
.subscribe(({ hoverOrError, codeView, dom, part }) => {
477+
.pipe(
478+
switchMap(hoverObservable => hoverObservable),
479+
switchMap(({ hoverOrError, position, adjustPosition, ...rest }) => {
480+
if (!HoverMerged.is(hoverOrError) || !hoverOrError.range || !position) {
481+
return of({ hoverOrError, position: undefined as Position | undefined, ...rest })
482+
}
483+
484+
// LSP is 0-indexed, the code here is currently 1-indexed
485+
const { line, character } = hoverOrError.range.start
486+
const pos = { line: line + 1, character: character + 1, ...position }
487+
488+
if (!adjustPosition) {
489+
return of({ hoverOrError, position: pos, ...rest })
490+
}
491+
492+
return adjustPosition({
493+
codeView: rest.codeView,
494+
direction: AdjustmentDirection.ActualToCodeView,
495+
position: {
496+
...position,
497+
part: rest.part,
498+
},
499+
}).pipe(map(position => ({ position, hoverOrError, ...rest })))
500+
})
501+
)
502+
.subscribe(({ hoverOrError, position, codeView, dom, part }) => {
404503
container.update({
405504
hoverOrError,
406505
// Reset the hover position, it's gonna be repositioned after the hover was rendered
@@ -410,12 +509,11 @@ export const createHoverifier = ({
410509
if (currentHighlighted) {
411510
currentHighlighted.classList.remove('selection-highlight')
412511
}
413-
if (!HoverMerged.is(hoverOrError) || !hoverOrError.range) {
512+
if (!position) {
414513
return
415514
}
416-
// LSP is 0-indexed, the code here is currently 1-indexed
417-
const { line, character } = hoverOrError.range.start
418-
const token = getTokenAtPosition(codeView, { line: line + 1, character: character + 1 }, dom, part)
515+
516+
const token = getTokenAtPosition(codeView, position, dom, part)
419517
if (!token) {
420518
return
421519
}

0 commit comments

Comments
 (0)