Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useCustomLocator() for non-DOM locators #302

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {isWindows} from '@solid-primitives/platform'
import {LOCATION_ATTRIBUTE_NAME, NodeID, WINDOW_PROJECTPATH_PROPERTY} from '../types'
import {LOCATION_ATTRIBUTE_NAME, NodeID, WINDOW_PROJECTPATH_PROPERTY} from '../../types'
import {SourceElementType, SourceLocation} from '../types'
import {parseLocationString} from '../utils'

export type LocationAttr = `${string}:${number}:${number}`

Expand All @@ -12,15 +14,9 @@ export type LocatorComponent = {

export type TargetIDE = 'vscode' | 'webstorm' | 'atom' | 'vscode-insiders'

export type SourceLocation = {
file: string
line: number
column: number
}

export type SourceCodeData = SourceLocation & {
projectPath: string
element: HTMLElement | string | undefined
element: SourceElementType<HTMLElement>
}

export type TargetURLFunction = (data: SourceCodeData) => string | void
Expand Down Expand Up @@ -66,24 +62,6 @@ export function getSourceCodeData(
return {...parsed, projectPath, element}
}

/**
* Validates and parses a location string to a {@link SourceLocation} object
*/
export function parseLocationString(location: string): SourceLocation | undefined {
// eslint-disable-next-line prefer-const
let [filePath, line, column] = location.split(':') as [string, string | number, string | number]
if (
filePath &&
line &&
column &&
typeof filePath === 'string' &&
!isNaN((line = Number(line))) &&
!isNaN((column = Number(column)))
) {
return {file: filePath, line, column}
}
}

export function openSourceCode(target: TargetIDE | TargetURLFunction, data: SourceCodeData): void {
const url = getTargetURL(target, data)
if (typeof url === 'string') window.open(url, '_blank')
Expand Down
157 changes: 157 additions & 0 deletions packages/debugger/src/locator/DOMLocator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {makeHoverElementListener} from '@solid-devtools/shared/primitives'
import {warn} from '@solid-devtools/shared/utils'
import {makeEventListener} from '@solid-primitives/event-listener'
import {createKeyHold} from '@solid-primitives/keyboard'
import {scheduleIdle} from '@solid-primitives/scheduled'
import {defer} from '@solid-primitives/utils'
import {createEffect, createMemo, createSignal, onCleanup} from 'solid-js'
import * as registry from '../../main/component-registry'
import {ObjectType, getObjectById} from '../../main/id'
import {NodeID} from '../../main/types'
import {HighlightElementPayload, LocatorFactory, SourceElementType, SourceLocation} from '../types'
import {createElementsOverlay} from './element-overlay'
import {
LocatorComponent,
TargetIDE,
TargetURLFunction,
getLocationAttr,
getProjectPath,
getSourceCodeData,
openSourceCode,
} from './find-components'
import {LocatorOptions} from './types'

export function createDOMLocatorFactory(options: LocatorOptions): LocatorFactory<HTMLElement> {
return props => {
const [enabledByPressingSignal, setEnabledByPressingSignal] = createSignal(
(): boolean => false,
)
props.setLocatorEnabledSignal(createMemo(() => enabledByPressingSignal()()))

const [hoverTarget, setHoverTarget] = createSignal<HTMLElement | null>(null)
const [devtoolsTarget, setDevtoolsTarget] = createSignal<HighlightElementPayload>(null)

const [highlightedComponents, setHighlightedComponents] = createSignal<LocatorComponent[]>(
[],
)

const calcHighlightedComponents = (
target: HTMLElement | HighlightElementPayload,
): LocatorComponent[] => {
if (!target) return []

// target is an elementId
if ('type' in target && target.type === 'element') {
const element = getObjectById(target.id, ObjectType.Element)
if (!(element instanceof HTMLElement)) return []
target = element
}

// target is an element
if (target instanceof HTMLElement) {
const comp = registry.findComponent(target)
if (!comp) return []
return [
{
location: getLocationAttr(target),
element: target,
id: comp.id,
name: comp.name,
},
]
}

// target is a component or an element of a component (in DOM walker mode)
const comp = registry.getComponent(target.id)
if (!comp) return []
return comp.elements.map(element => ({
element,
id: comp.id,
name: comp.name,
}))
}

createEffect(
defer(
() => hoverTarget() ?? devtoolsTarget(),
scheduleIdle(target =>
setHighlightedComponents(() => calcHighlightedComponents(target)),
),
),
)

createElementsOverlay(highlightedComponents)

// notify of component hovered by using the debugger
createEffect((prev: NodeID | undefined) => {
const target = hoverTarget()
const comp = target && registry.findComponent(target)
if (prev) props.emit('HoveredComponent', {nodeId: prev, state: false})
if (comp) {
const {id} = comp
props.emit('HoveredComponent', {nodeId: id, state: true})
return id
}
})

let targetIDE: TargetIDE | TargetURLFunction | undefined

createEffect(() => {
if (!props.locatorEnabled()) return

// set hovered element as target
makeHoverElementListener(el => setHoverTarget(el))
onCleanup(() => setHoverTarget(null))

// go to selected component source code on click
makeEventListener(
window,
'click',
e => {
const {target} = e
if (!(target instanceof HTMLElement)) return
const highlighted = highlightedComponents()
const comp =
highlighted.find(({element}) => target.contains(element)) ?? highlighted[0]
if (!comp) return
const sourceCodeData =
comp.location && getSourceCodeData(comp.location, comp.element)

// intercept on-page components clicks and send them to the devtools overlay
props.onComponentClick(comp.id, () => {
if (!targetIDE || !sourceCodeData) return
e.preventDefault()
e.stopPropagation()
openSourceCode(targetIDE, sourceCodeData)
})
},
true,
)
})

if (options.targetIDE) targetIDE = options.targetIDE
if (options.key !== false) {
const isHoldingKey = createKeyHold(options.key ?? 'Alt', {preventDefault: true})
setEnabledByPressingSignal(() => isHoldingKey)
}

return {
setDevtoolsHighlightTarget(target: HighlightElementPayload) {
setDevtoolsTarget(target)
},
openElementSourceCode(
location: SourceLocation,
element: SourceElementType<HTMLElement>,
) {
if (!targetIDE) return warn('Please set `targetIDE` it in useLocator options.')
const projectPath = getProjectPath()
if (!projectPath) return warn('projectPath is not set.')
openSourceCode(targetIDE, {
...location,
projectPath,
element,
})
},
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ vi.mock('@solid-primitives/platform', () => ({
},
}))

const fetchFunction = async () => (await import('../find-components')).parseLocationString
const fetchFunction = async () => (await import('../../utils')).parseLocationString

describe('locator attribute pasting', () => {
beforeEach(() => {
Expand Down
18 changes: 18 additions & 0 deletions packages/debugger/src/locator/DOMLocator/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {KbdKey} from '@solid-primitives/keyboard'
import type {TargetIDE, TargetURLFunction} from './find-components'

export type {LocationAttr, LocatorComponent, TargetIDE, TargetURLFunction} from './find-components'

export type LocatorOptions = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not make LocatorOptions DOM-agnostic without changing the api. targetIDE is fine, but key assumes the browser's implementation of keyboard keys

/** Choose in which IDE the component source code should be revealed. */
targetIDE?: false | TargetIDE | TargetURLFunction
/**
* Holding which key should enable the locator overlay?
* @default 'Alt'
*/
key?: false | KbdKey
}

// used by the transform
export const WINDOW_PROJECTPATH_PROPERTY = '$sdt_projectPath'
export const LOCATION_ATTRIBUTE_NAME = 'data-source-loc'
Loading