Selections Refactor (#4381)

* selection stuff

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* trigger CI

* fix bugs

* some edge cut stuff

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* fix sketch mode issues

* fix more tests, selection in sketch related

* more test fixing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger ci

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* Trigger ci

* more sketch mode selection fixes

* fix unit tests

* rename function

* remove .only

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* lint

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* fix bad pathToNode issue

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)

* fix sketch on face

* migrate a more selections types

* migrate a more selections types

* fix code selection of fillets

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* migrate a more selections types

* fix bad path to node, looks like a race

* migrate a more selections types

* migrate a more selections types

* fix cmd bar selections

* fix cmd bar selections

* fix display issues

* migrate a more selections types

* Revert "migrate a more selections types"

This reverts commit 0d0e453bbb.

* migrate a more selections types

* clean up1

* clean up 2

* fix types after main merge

* review tweaks

* fix wall selection bug

* Update src/lang/std/engineConnection.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* add franks TODO comment

* fix type after main merge, plus a touch of clean up

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
Kurt Hutten
2024-11-21 15:04:30 +11:00
committed by GitHub
parent c17cb1067f
commit 59e0df7879
49 changed files with 885 additions and 763 deletions

View File

@ -5,7 +5,7 @@ import {
kclManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm'
import { CallExpression, SourceRange, Expr } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { isNonNullable, uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -15,6 +15,7 @@ import { Program } from 'lang/wasm'
import {
doesPipeHaveCallExp,
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
isSingleCursorInPipe,
} from 'lang/queryAst'
@ -28,12 +29,15 @@ import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
import { PathToNodeMap } from 'lang/std/sketchcombos'
import { err } from 'lib/trap'
import {
Artifact,
getArtifactOfTypes,
getArtifactsOfTypes,
getCapCodeRef,
getSweepEdgeCodeRef,
getSolid2dCodeRef,
getWallCodeRef,
CodeRef,
getCodeRefsByArtifactId,
ArtifactId,
} from 'lang/std/artifactGraph'
import { Node } from 'wasm-lib/kcl/bindings/Node'
@ -43,7 +47,8 @@ export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection =
/** @deprecated Use {@link Artifact} instead. */
type Selection__old =
| {
type:
| 'default'
@ -67,9 +72,88 @@ export type Selection =
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
secondaryRange: SourceRange
}
export type Selections = {
/** @deprecated Use {@link Selection} instead. */
export type Selections__old = {
otherSelections: Axis[]
codeBasedSelections: Selection[]
codeBasedSelections: Selection__old[]
}
export interface Selection {
artifact?: Artifact
codeRef: CodeRef
}
export type Selections = {
otherSelections: Array<Axis>
graphSelections: Array<Selection>
}
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
* this function should only be used for backwards compatibility with old functions.
*/
function convertSelectionToOld(selection: Selection): Selection__old | null {
// return {} as Selection__old
// TODO implementation
const _artifact = selection.artifact
if (_artifact?.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return { range: codeRef.range, type: 'solid2D' }
}
if (_artifact?.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
if (err(codeRef)) return null
return {
range: codeRef.range,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
}
}
if (_artifact?.type === 'wall') {
const codeRef = getWallCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return { range: codeRef.range, type: 'extrude-wall' }
}
if (_artifact?.type === 'segment' || _artifact?.type === 'path') {
return { range: _artifact.codeRef.range, type: 'default' }
}
if (_artifact?.type === 'sweepEdge') {
const codeRef = getSweepEdgeCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
if (_artifact?.subType === 'adjacent') {
return { range: codeRef.range, type: 'adjacent-edge' }
}
return { range: codeRef.range, type: 'edge' }
}
if (_artifact?.type === 'edgeCut') {
const codeRef = _artifact.codeRef
return { range: codeRef.range, type: 'default' }
}
if (selection?.codeRef?.range) {
return { range: selection.codeRef.range, type: 'default' }
}
return null
}
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
* this function should only be used for backwards compatibility with old functions.
*/
export function convertSelectionsToOld(selection: Selections): Selections__old {
const selections: Selection__old[] = []
for (const artifact of selection.graphSelections) {
const converted = convertSelectionToOld(artifact)
if (converted) selections.push(converted)
}
const selectionsOld: Selections__old = {
otherSelections: selection.otherSelections,
codeBasedSelections: selections,
}
return selectionsOld
}
export async function getEventForSelectWithPoint({
@ -94,127 +178,18 @@ export async function getEventForSelectWithPoint({
}
}
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
if (!_artifact)
return {
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
}
if (_artifact.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'solid2D' },
},
}
}
if (_artifact.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
if (err(codeRef)) return null
const codeRefs = getCodeRefsByArtifactId(
data.entity_id,
engineCommandManager.artifactGraph
)
if (_artifact && codeRefs) {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: codeRef.range,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
},
},
}
}
if (_artifact.type === 'wall') {
const codeRef = getWallCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'extrude-wall' },
},
}
}
if (_artifact.type === 'segment' || _artifact.type === 'path') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: _artifact.codeRef.range, type: 'default' },
},
}
}
if (_artifact.type === 'sweepEdge') {
const codeRef = getSweepEdgeCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
if (_artifact?.subType === 'adjacent') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'adjacent-edge' },
},
}
}
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'edge' },
},
}
}
if (_artifact.type === 'edgeCut') {
const consumedEdge = getArtifactOfTypes(
{ key: _artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] },
engineCommandManager.artifactGraph
)
if (err(consumedEdge))
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: _artifact.codeRef.range, type: 'default' },
},
}
if (consumedEdge.type === 'segment') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type: 'base-edgeCut',
secondaryRange: consumedEdge.codeRef.range,
},
},
}
}
const segment = getArtifactOfTypes(
{ key: consumedEdge.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(segment)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type:
consumedEdge.subType === 'adjacent'
? 'adjacent-edgeCut'
: 'opposite-edgeCut',
secondaryRange: segment.codeRef.range,
artifact: _artifact,
codeRef: codeRefs[0],
},
},
}
@ -237,28 +212,55 @@ export function getEventForSegmentSelection(
},
}
}
const pathToNode = group?.userData?.pathToNode
if (!pathToNode) return null
// previous drags don't update ast for efficiency reasons
// So we want to make sure we have and updated ast with
// accurate source ranges
const updatedAst = parse(codeManager.code)
if (err(updatedAst)) return null
// id does not match up with the artifact graph when in sketch mode, because mock executions
// do not update the artifact graph, therefore we match up the pathToNode instead
// we can reliably use `type === 'segment'` since it's in sketch mode and we're concerned with segments
const segWithMatchingPathToNode__Id = [
...engineCommandManager.artifactGraph,
].find((entry) => {
return (
entry[1].type === 'segment' &&
JSON.stringify(entry[1].codeRef.pathToNode) ===
JSON.stringify(group?.userData?.pathToNode)
)
})?.[0]
const nodeMeta = getNodeFromPath<Node<CallExpression>>(
updatedAst,
pathToNode,
'CallExpression'
const id = segWithMatchingPathToNode__Id
if (!id && group) {
const node = getNodeFromPath<Expr>(
kclManager.ast,
group.userData.pathToNode
)
if (err(node)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
codeRef: {
range: [node.node.start, node.node.end],
pathToNode: group.userData.pathToNode,
},
},
},
}
}
if (!id || !group) return null
const artifact = engineCommandManager.artifactGraph.get(id)
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (err(nodeMeta)) return null
const node = nodeMeta.node
const range: SourceRange = [node.start, node.end]
if (!artifact || !codeRefs) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range, type: 'default' },
selection: {
artifact,
codeRef: codeRefs[0],
},
},
}
}
@ -274,13 +276,24 @@ export function handleSelectionBatch({
updateSceneObjectColors: () => void
} {
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionToEngine: SelectionToEngine[] = []
selections.graphSelections.forEach(({ artifact }) => {
artifact?.id &&
selectionToEngine.push({
type: 'default',
id: artifact?.id,
range: getCodeRefsByArtifactId(
artifact.id,
engineCommandManager.artifactGraph
)?.[0].range || [0, 0],
})
})
const engineEvents: Models['WebSocketRequest_type'][] =
resetAndSetEngineEntitySelectionCmds(
codeToIdSelections(selections.codeBasedSelections)
)
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
resetAndSetEngineEntitySelectionCmds(selectionToEngine)
selections.graphSelections.forEach(({ codeRef }) => {
if (codeRef.range?.[1]) {
ranges.push(EditorSelection.cursor(codeRef.range[1]))
}
})
if (ranges.length)
@ -288,11 +301,11 @@ export function handleSelectionBatch({
engineEvents,
codeMirrorSelection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
selections.graphSelections.length - 1
),
otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.codeBasedSelections),
updateSceneObjectColors(selections.graphSelections),
}
return {
@ -303,43 +316,75 @@ export function handleSelectionBatch({
engineEvents,
otherSelections: selections.otherSelections,
updateSceneObjectColors: () =>
updateSceneObjectColors(selections.codeBasedSelections),
updateSceneObjectColors(selections.graphSelections),
}
}
type SelectionToEngine = { type: Selection['type']; id: string }
type SelectionToEngine = {
type: Selection__old['type']
id?: string
range: SourceRange
}
export function processCodeMirrorRanges({
codeMirrorRanges,
selectionRanges,
isShiftDown,
ast,
}: {
codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections
isShiftDown: boolean
ast: Program
}): null | {
modelingEvent: ModelingMachineEvent
engineEvents: Models['WebSocketRequest_type'][]
} {
const isChange =
codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length ||
codeMirrorRanges.length !== selectionRanges?.graphSelections?.length ||
codeMirrorRanges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
from !== selectionRanges.graphSelections[i]?.codeRef?.range[0] ||
to !== selectionRanges.graphSelections[i]?.codeRef?.range[1]
)
})
if (!isChange) return null
const codeBasedSelections: Selections['codeBasedSelections'] =
const codeBasedSelections: Selections['graphSelections'] =
codeMirrorRanges.map(({ from, to }) => {
const pathToNode = getNodePathFromSourceRange(ast, [from, to])
return {
type: 'default',
range: [from, to],
codeRef: {
range: [from, to],
pathToNode,
},
}
})
const idBasedSelections: SelectionToEngine[] =
codeToIdSelections(codeBasedSelections)
const selections: Selection[] = []
for (const { id, range } of idBasedSelections) {
if (!id) {
const pathToNode = getNodePathFromSourceRange(ast, range)
selections.push({
codeRef: {
range,
pathToNode,
},
})
continue
}
const artifact = engineCommandManager.artifactGraph.get(id)
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (artifact && codeRefs) {
selections.push({ artifact, codeRef: codeRefs[0] })
} else if (codeRefs) {
selections.push({ codeRef: codeRefs[0] })
}
}
if (!selectionRanges) return null
updateSceneObjectColors(codeBasedSelections)
@ -350,11 +395,13 @@ export function processCodeMirrorRanges({
selectionType: 'mirrorCodeMirrorSelections',
selection: {
otherSelections: isShiftDown ? selectionRanges.otherSelections : [],
codeBasedSelections,
graphSelections: selections,
},
},
},
engineEvents: resetAndSetEngineEntitySelectionCmds(idBasedSelections),
engineEvents: resetAndSetEngineEntitySelectionCmds(
idBasedSelections.filter(({ id }) => !!id)
),
}
}
@ -371,7 +418,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
if (err(nodeMeta)) return
const node = nodeMeta.node
const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection.range, [node.start, node.end])
return isOverlap(selection?.codeRef?.range, [node.start, node.end])
})
const color = groupHasCursor
@ -406,7 +453,7 @@ function resetAndSetEngineEntitySelectionCmds(
type: 'modeling_cmd_req',
cmd: {
type: 'select_add',
entities: selections.map(({ id }) => id),
entities: selections.map(({ id }) => id).filter(isNonNullable),
},
cmd_id: uuidv4(),
},
@ -426,14 +473,14 @@ export function isSelectionLastLine(
code: string,
i = 0
) {
return selectionRanges.codeBasedSelections[i].range[1] === code.length
return selectionRanges.graphSelections[i]?.codeRef?.range[1] === code.length
}
export function isRangeBetweenCharacters(selectionRanges: Selections) {
return (
selectionRanges.codeBasedSelections.length === 1 &&
selectionRanges.codeBasedSelections[0].range[0] === 0 &&
selectionRanges.codeBasedSelections[0].range[1] === 0
selectionRanges.graphSelections.length === 1 &&
selectionRanges.graphSelections[0]?.codeRef?.range[0] === 0 &&
selectionRanges.graphSelections[0]?.codeRef?.range[1] === 0
)
}
@ -444,7 +491,7 @@ export type CommonASTNode = {
function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
return {
selection: selectionRanges.codeBasedSelections[i],
selection: selectionRanges.graphSelections[i],
ast: kclManager.ast,
}
}
@ -476,7 +523,7 @@ function nodeHasCircle(node: CommonASTNode) {
}
export function canSweepSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return (
@ -488,33 +535,8 @@ export function canSweepSelection(selection: Selections) {
)
}
export function canFilletSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER
return (
!!isSketchPipe(selection) &&
commonNodes.every((n) => nodeHasClose(n)) &&
commonNodes.every((n) => !nodeHasExtrude(n))
)
}
function canExtrudeSelectionItem(selection: Selections, i: number) {
const isolatedSelection = {
...selection,
codeBasedSelections: [selection.codeBasedSelections[i]],
}
const commonNode = buildCommonNodeFromSelection(selection, i)
return (
!!isSketchPipe(isolatedSelection) &&
(nodeHasClose(commonNode) || nodeHasCircle(commonNode)) &&
!nodeHasExtrude(commonNode)
)
}
// This accounts for non-geometry selections under "other"
export type ResolvedSelectionType = [Selection['type'] | 'other', number]
export type ResolvedSelectionType = [Artifact['type'] | 'other', number]
/**
* In the future, I'd like this function to properly return the type of each selected entity based on
@ -527,17 +549,16 @@ export function getSelectionType(
selection?: Selections
): ResolvedSelectionType[] {
if (!selection) return []
const extrudableCount = selection.codeBasedSelections.filter((_, i) => {
const singleSelection = {
...selection,
codeBasedSelections: [selection.codeBasedSelections[i]],
}
return canExtrudeSelectionItem(singleSelection, 0)
}).length
return extrudableCount === selection.codeBasedSelections.length
? [['extrude-wall', extrudableCount]]
: [['other', selection.codeBasedSelections.length]]
const selectionsWithArtifacts = selection.graphSelections.filter(
(s) => !!s.artifact
)
const firstSelection = selectionsWithArtifacts[0]
const firstSelectionType = firstSelection?.artifact?.type
if (!firstSelectionType) return []
const selectionsWithSameType = selectionsWithArtifacts.filter(
(s) => s.artifact?.type === firstSelection.artifact?.type
)
return [[firstSelectionType, selectionsWithSameType.length]]
}
export function getSelectionTypeDisplayText(
@ -549,9 +570,10 @@ export function getSelectionTypeDisplayText(
.map(
// Hack for showing "face" instead of "extrude-wall" in command bar text
([type, count]) =>
`${count} ${type.replace('extrude-wall', 'face')}${
count > 1 ? 's' : ''
}`
`${count} ${type
.replace('wall', 'face')
.replace('solid2D', 'face')
.replace('segment', 'face')}${count > 1 ? 's' : ''}`
)
.join(', ')
}
@ -572,10 +594,14 @@ export function canSubmitSelectionArg(
)
}
function codeToIdSelections(
codeBasedSelections: Selection[]
export function codeToIdSelections(
selections: Selection[]
): SelectionToEngine[] {
return codeBasedSelections
const selectionsOld = convertSelectionsToOld({
graphSelections: selections,
otherSelections: [],
}).codeBasedSelections
return selectionsOld
.flatMap((selection): null | SelectionToEngine[] => {
const { type } = selection
// TODO #868: loops over all artifacts will become inefficient at a large scale
@ -607,19 +633,26 @@ function codeToIdSelections(
| {
id: ArtifactId
artifact: unknown
selection: Selection
selection: Selection__old
}
| undefined
overlappingEntries.forEach((entry) => {
// TODO probably need to remove much of the `type === 'xyz'` below
if (type === 'default' && entry.artifact.type === 'segment') {
bestCandidate = entry
return
}
if (type === 'solid2D' && entry.artifact.type === 'path') {
const solid = engineCommandManager.artifactGraph.get(
if (entry.artifact.type === 'path') {
const artifact = engineCommandManager.artifactGraph.get(
entry.artifact.solid2dId || ''
)
if (solid?.type !== 'solid2D') return
if (artifact?.type !== 'solid2D') {
bestCandidate = {
artifact: entry.artifact,
selection,
id: entry.id,
}
}
if (!entry.artifact.solid2dId) {
console.error(
'Expected PathArtifact to have solid2dId, but none found'
@ -627,7 +660,7 @@ function codeToIdSelections(
return
}
bestCandidate = {
artifact: solid,
artifact: artifact,
selection,
id: entry.artifact.solid2dId,
}
@ -752,10 +785,11 @@ function codeToIdSelections(
{
type,
id: bestCandidate.id,
range: bestCandidate.selection.range,
},
]
}
return null
return [selection]
})
.filter(isNonNullable)
}
@ -798,32 +832,58 @@ export function updateSelections(
const newSelections = Object.entries(pathToNodeMap)
.map(([index, pathToNode]): Selection | undefined => {
const previousSelection =
prevSelectionRanges.graphSelections[Number(index)]
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
const selection = prevSelectionRanges.codeBasedSelections[Number(index)]
if (
selection?.type === 'base-edgeCut' ||
selection?.type === 'adjacent-edgeCut' ||
selection?.type === 'opposite-edgeCut'
)
return {
range: [node.start, node.end],
type: selection?.type,
secondaryRange: selection?.secondaryRange,
let artifact: Artifact | null = null
for (const [id, a] of engineCommandManager.artifactGraph) {
if (previousSelection?.artifact?.type === a.type) {
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (!codeRefs) continue
if (
JSON.stringify(codeRefs[0].pathToNode) ===
JSON.stringify(pathToNode)
) {
artifact = a
console.log('found artifact', a)
break
}
}
}
if (!artifact) return undefined
return {
range: [node.start, node.end],
type: selection?.type,
artifact: artifact,
codeRef: {
range: [node.start, node.end],
pathToNode: pathToNode,
},
}
})
.filter((x?: Selection) => x !== undefined) as Selection[]
// for when there is no artifact (sketch mode since mock execute does not update artifactGraph)
const pathToNodeBasedSelections: Selections['graphSelections'] = []
for (const pathToNode of Object.values(pathToNodeMap)) {
const node = getNodeFromPath<Expr>(ast, pathToNode)
if (err(node)) return node
pathToNodeBasedSelections.push({
codeRef: {
range: [node.node.start, node.node.end],
pathToNode: pathToNode,
},
})
}
return {
codeBasedSelections:
newSelections.length > 0
graphSelections:
newSelections.length >= pathToNodeBasedSelections.length
? newSelections
: prevSelectionRanges.codeBasedSelections,
: pathToNodeBasedSelections,
otherSelections: prevSelectionRanges.otherSelections,
}
}