Fix CB interaction with record-mode override and add tests (#9386)

Fixes #7976.
This commit is contained in:
Kaz Wesley 2024-03-17 13:44:39 -04:00 committed by GitHub
parent 85ddbc5f24
commit 47d9e73ead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1782 additions and 297 deletions

View File

@ -277,3 +277,38 @@ test('Visualization preview: user visualization selection', async ({ page }) =>
await expect(locate.componentBrowser(page)).not.toBeVisible()
await expect(locate.graphNode(page)).toHaveCount(nodeCount)
})
test('Component browser handling of overridden record-mode', async ({ page }) => {
await actions.goToGraph(page)
const node = locate.graphNodeByBinding(page, 'data')
const ADDED_PATH = '"/home/enso/Input.txt"'
const recordModeToggle = node.getByTestId('overrideRecordingButton')
const recordModeIndicator = node.getByTestId('recordingOverriddenButton')
const MENU_UNHOVER_CLOSE_DELAY = 300
// Enable record mode for the node.
await locate.graphNodeIcon(node).hover()
await expect(recordModeToggle).not.toHaveClass(/recording-overridden/)
await recordModeToggle.click()
await page.mouse.move(100, 80)
await page.waitForTimeout(MENU_UNHOVER_CLOSE_DELAY)
await expect(recordModeIndicator).toBeVisible()
await locate.graphNodeIcon(node).hover()
await expect(recordModeToggle).toHaveClass(/recording-overridden/)
// Ensure editing in the component browser doesn't display the override expression.
await locate.graphNodeIcon(node).click({ modifiers: [CONTROL_KEY] })
await expect(locate.componentBrowser(page)).toBeVisible()
const input = locate.componentBrowserInput(page).locator('input')
await expect(input).toHaveValue('Data.read')
// Ensure committing an edit doesn't change the override state.
await page.keyboard.press('End')
await input.pressSequentially(` ${ADDED_PATH}`)
await page.keyboard.press('Enter')
await expect(locate.componentBrowser(page)).not.toBeVisible()
await page.mouse.move(100, 80)
await expect(recordModeIndicator).toBeVisible()
// Ensure after editing the node, editing still doesn't display the override expression.
await locate.graphNodeIcon(node).click({ modifiers: [CONTROL_KEY] })
await expect(locate.componentBrowser(page)).toBeVisible()
await expect(input).toHaveValue(`Data.read ${ADDED_PATH}`)
})

View File

@ -57,6 +57,7 @@ const emit = defineEmits<{
<ToggleIcon
icon="record"
class="icon-container button slot7"
data-testid="overrideRecordingButton"
:class="{ 'recording-overridden': props.isRecordingOverridden }"
:alt="`${props.isRecordingOverridden ? 'Disable' : 'Enable'} recording`"
:modelValue="props.isRecordingOverridden"

View File

@ -18,7 +18,8 @@ export { EditorView, tooltips, type TooltipView } from '@codemirror/view'
export { type Highlighter } from '@lezer/highlight'
export { minimalSetup } from 'codemirror'
export { yCollab } from 'y-codemirror.next'
import { RawAst, RawAstExtended } from '@/util/ast'
import { RawAstExtended } from '@/util/ast/extended'
import { RawAst } from '@/util/ast/raw'
import {
Language,
LanguageSupport,

View File

@ -10,10 +10,11 @@ import {
type SuggestionId,
type Typename,
} from '@/stores/suggestionDatabase/entry'
import { RawAst, RawAstExtended, astContainingChar } from '@/util/ast'
import { isOperator, type AstId } from '@/util/ast/abstract.ts'
import { AliasAnalyzer } from '@/util/ast/aliasAnalysis'
import { RawAstExtended } from '@/util/ast/extended'
import { GeneralOprApp, type OperatorChain } from '@/util/ast/opr'
import { RawAst, astContainingChar } from '@/util/ast/raw'
import { MappedSet } from '@/util/containers'
import {
qnFromSegments,

View File

@ -432,6 +432,7 @@ const documentation = computed<string | undefined>({
<button
v-if="!menuVisible && isRecordingOverridden"
class="overrideRecordButton"
data-testid="recordingOverriddenButton"
@click="isRecordingOverridden = false"
>
<SvgIcon name="record" />

View File

@ -14,7 +14,7 @@ import {
import { useProjectStore } from '@/stores/project'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { assert, bail } from '@/util/assert'
import { Ast, RawAst, visitRecursive } from '@/util/ast'
import { Ast } from '@/util/ast'
import type {
AstId,
Module,
@ -23,6 +23,7 @@ import type {
NodeMetadataFields,
} from '@/util/ast/abstract'
import { MutableModule, isIdentifier } from '@/util/ast/abstract'
import { RawAst, visitRecursive } from '@/util/ast/raw'
import { partition } from '@/util/data/array'
import type { Opt } from '@/util/data/opt'
import { Rect } from '@/util/data/rect'
@ -327,11 +328,12 @@ export const useGraphStore = defineStore('graph', () => {
const node = db.nodeIdToNode.get(id)
if (!node) return
edit((edit) => {
edit.getVersion(node.rootExpr).syncToCode(content)
const editExpr = edit.getVersion(node.innerExpr)
editExpr.syncToCode(content)
if (withImports) {
const conflicts = addMissingImports(edit, withImports)
if (conflicts == null) return
const wholeAssignment = edit.getVersion(node.rootExpr)?.mutableParent()
const wholeAssignment = editExpr.mutableParent()
if (wholeAssignment == null) {
console.error('Cannot find parent of the node expression. Conflict resolution failed.')
return

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,18 @@
import { assert } from '@/util/assert'
import { Ast } from '@/util/ast'
import {
MutableModule,
TextLiteral,
escapeTextLiteral,
substituteQualifiedName,
unescapeTextLiteral,
type Identifier,
} from '@/util/ast/abstract'
import { tryQualifiedName } from '@/util/qualifiedName'
import { fc, test } from '@fast-check/vitest'
import { initializeFFI } from 'shared/ast/ffi'
import { unwrap } from 'shared/util/data/result'
import { describe, expect, test } from 'vitest'
import { MutableModule, substituteQualifiedName, type Identifier } from '../abstract'
import { describe, expect } from 'vitest'
import { findExpressions, testCase, tryFindExpressions } from './testCase'
await initializeFFI()
@ -854,6 +862,49 @@ test.each([
expect(edit.root()?.code()).toEqual(expected)
})
test.each([
['', ''],
['\\x20', ' ', ' '],
['\\b', '\b'],
['abcdef_123', 'abcdef_123'],
['\\t\\r\\n\\v\\"\\\'\\`', '\t\r\n\v"\'`'],
['\\u00B6\\u{20}\\U\\u{D8\\xBFF}', '\xB6 \0\xD8\xBFF}', '\xB6 \\0\xD8\xBFF}'],
['\\`foo\\` \\`bar\\` \\`baz\\`', '`foo` `bar` `baz`'],
])(
'Applying and escaping text literal interpolation',
(escapedText: string, rawText: string, roundtrip?: string) => {
const actualApplied = unescapeTextLiteral(escapedText)
const actualEscaped = escapeTextLiteral(rawText)
expect(actualEscaped).toBe(roundtrip ?? escapedText)
expect(actualApplied).toBe(rawText)
},
)
const sometimesUnicodeString = fc.oneof(fc.string(), fc.unicodeString())
test.prop({ rawText: sometimesUnicodeString })('Text interpolation roundtrip', ({ rawText }) => {
expect(unescapeTextLiteral(escapeTextLiteral(rawText))).toBe(rawText)
})
test.prop({ rawText: sometimesUnicodeString })('AST text literal new', ({ rawText }) => {
const literal = TextLiteral.new(rawText)
expect(literal.rawTextContent).toBe(rawText)
})
test.prop({
boundary: fc.constantFrom('"', "'"),
rawText: sometimesUnicodeString,
})('AST text literal rawTextContent', ({ boundary, rawText }) => {
const literal = TextLiteral.new('')
literal.setBoundaries(boundary)
literal.setRawTextContent(rawText)
expect(literal.rawTextContent).toBe(rawText)
const expectInterpolated = rawText.includes('"') || boundary === "'"
const expectedCode = expectInterpolated ? `'${escapeTextLiteral(rawText)}'` : `"${rawText}"`
expect(literal.code()).toBe(expectedCode)
})
const docEditCases = [
{ code: '## Simple\nnode', documentation: 'Simple' },
{

View File

@ -1,6 +1,7 @@
import { assert } from '@/util/assert'
import { RawAst, RawAstExtended } from '@/util/ast'
import { RawAstExtended } from '@/util/ast/extended'
import { GeneralOprApp, operandsOfLeftAssocOprChain, type OperatorChain } from '@/util/ast/opr'
import { RawAst } from '@/util/ast/raw'
import { initializeFFI } from 'shared/ast/ffi'
import { expect, test } from 'vitest'

View File

@ -8,14 +8,11 @@ import {
readAstSpan,
readTokenSpan,
walkRecursive,
} from '@/util/ast'
import { fc, test } from '@fast-check/vitest'
} from '@/util/ast/raw'
import { initializeFFI } from 'shared/ast/ffi'
import { Token, Tree } from 'shared/ast/generated/ast'
import type { LazyObject } from 'shared/ast/parserSupport'
import { escapeTextLiteral, unescapeTextLiteral } from 'shared/ast/text'
import { assert, expect } from 'vitest'
import { TextLiteral } from '../abstract'
import { assert, expect, test } from 'vitest'
await initializeFFI()
@ -227,46 +224,3 @@ test.each([
expect(readAstSpan(ast, code)).toBe(expected?.repr)
}
})
test.each([
['', ''],
['\\x20', ' ', ' '],
['\\b', '\b'],
['abcdef_123', 'abcdef_123'],
['\\t\\r\\n\\v\\"\\\'\\`', '\t\r\n\v"\'`'],
['\\u00B6\\u{20}\\U\\u{D8\\xBFF}', '\xB6 \0\xD8\xBFF}', '\xB6 \\0\xD8\xBFF}'],
['\\`foo\\` \\`bar\\` \\`baz\\`', '`foo` `bar` `baz`'],
])(
'Applying and escaping text literal interpolation',
(escapedText: string, rawText: string, roundtrip?: string) => {
const actualApplied = unescapeTextLiteral(escapedText)
const actualEscaped = escapeTextLiteral(rawText)
expect(actualEscaped).toBe(roundtrip ?? escapedText)
expect(actualApplied).toBe(rawText)
},
)
const sometimesUnicodeString = fc.oneof(fc.string(), fc.unicodeString())
test.prop({ rawText: sometimesUnicodeString })('Text interpolation roundtrip', ({ rawText }) => {
expect(unescapeTextLiteral(escapeTextLiteral(rawText))).toBe(rawText)
})
test.prop({ rawText: sometimesUnicodeString })('AST text literal new', ({ rawText }) => {
const literal = TextLiteral.new(rawText)
expect(literal.rawTextContent).toBe(rawText)
})
test.prop({
boundary: fc.constantFrom('"', "'"),
rawText: sometimesUnicodeString,
})('AST text literal rawTextContent', ({ boundary, rawText }) => {
const literal = TextLiteral.new('')
literal.setBoundaries(boundary)
literal.setRawTextContent(rawText)
expect(literal.rawTextContent).toBe(rawText)
const expectInterpolated = rawText.includes('"') || boundary === "'"
const expectedCode = expectInterpolated ? `'${escapeTextLiteral(rawText)}'` : `"${rawText}"`
expect(literal.code()).toBe(expectedCode)
})

View File

@ -6,7 +6,7 @@ import {
parsedTreeOrTokenRange,
readAstOrTokenSpan,
readTokenSpan,
} from '@/util/ast'
} from '@/util/ast/raw'
import { MappedKeyMap, MappedSet, NonEmptyStack } from '@/util/containers'
import type { LazyObject } from 'shared/ast/parserSupport'
import { rangeIsBefore, sourceRangeKey, type SourceRange } from 'shared/yjsModel'

View File

@ -7,7 +7,7 @@ import {
visitGenerator,
visitRecursive,
walkRecursive,
} from '@/util/ast'
} from '@/util/ast/raw'
import type { Opt } from '@/util/data/opt'
import * as encoding from 'lib0/encoding'
import * as sha256 from 'lib0/hash/sha256'
@ -18,6 +18,8 @@ import { tryGetSoleValue } from 'shared/util/data/iterable'
import type { ExternalId, IdMap, SourceRange } from 'shared/yjsModel'
import { markRaw } from 'vue'
export { AstExtended as RawAstExtended }
type ExtractType<V, T> =
T extends ReadonlyArray<infer Ts> ? Extract<V, { type: Ts }> : Extract<V, { type: T }>
@ -27,7 +29,7 @@ type OneOrArray<T> = T | readonly T[]
* AST with additional metadata containing AST IDs and original code reference. Can only be
* constructed by parsing any enso source code string.
*/
export class AstExtended<T extends Tree | Token = Tree | Token, HasIdMap extends boolean = true> {
class AstExtended<T extends Tree | Token = Tree | Token, HasIdMap extends boolean = true> {
inner: T
private ctx: AstExtendedCtx<HasIdMap>

View File

@ -1,237 +1,2 @@
import { assert, assertDefined } from '@/util/assert'
import * as Ast from '@/util/ast/abstract'
import { parseEnso } from '@/util/ast/abstract'
import { AstExtended as RawAstExtended } from '@/util/ast/extended'
import { isResult, mapOk } from '@/util/data/result'
import * as map from 'lib0/map'
import * as RawAst from 'shared/ast/generated/ast'
import { LazyObject, LazySequence } from 'shared/ast/parserSupport'
import { tryGetSoleValue } from 'shared/util/data/iterable'
import type { SourceRange } from 'shared/yjsModel'
export { Ast, RawAst, RawAstExtended, parseEnso }
export type HasAstRange = SourceRange | RawAst.Tree | RawAst.Token
/** Read a single line of code
*
* Is meant to be a helper for tests. If the code is multiline, an exception is raised.
*/
export function parseEnsoLine(code: string): RawAst.Tree {
const block = Ast.parseEnso(code)
assert(block.type === RawAst.Tree.Type.BodyBlock)
const soleExpression = tryGetSoleValue(block.statements)?.expression
assertDefined(soleExpression)
return soleExpression
}
/**
* Read span of code represented by given AST node, not including left whitespace offset.
*
* The AST is assumed to be generated from `code` and not modified sice then.
* Otherwise an unspecified fragment of `code` may be returned.
*/
export function readAstOrTokenSpan(node: RawAst.Tree | RawAst.Token, code: string): string {
const range = parsedTreeOrTokenRange(node)
return code.substring(range[0], range[1])
}
/**
* Read span of code represented by given RawAst.Tree.
*
* The Tree is assumed to be a part of AST generated from `code`.
*/
export function readAstSpan(node: RawAst.Tree, code: string): string {
const range = parsedTreeRange(node)
return code.substring(range[0], range[1])
}
/**
* Read span of code represented by given RawAst.Token.
*
* The Token is assumed to be a part of AST generated from `code`.
*/
export function readTokenSpan(token: RawAst.Token, code: string): string {
const range = parsedTokenRange(token)
return code.substring(range[0], range[1])
}
/**
* Read direct AST children.
*/
export function childrenAstNodes(obj: LazyObject): RawAst.Tree[] {
const children: RawAst.Tree[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj)) children.push(obj)
else if (!RawAst.Token.isInstance(obj)) obj.visitChildren(visitor)
}
obj.visitChildren(visitor)
return children
}
export function childrenAstNodesOrTokens(obj: LazyObject): (RawAst.Tree | RawAst.Token)[] {
const children: (RawAst.Tree | RawAst.Token)[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj) || RawAst.Token.isInstance(obj)) {
children.push(obj)
} else {
obj.visitChildren(visitor)
}
}
obj.visitChildren(visitor)
return children
}
/** Returns all AST nodes from `root` tree containing given char, starting from the most nested. */
export function astContainingChar(charIndex: number, root: RawAst.Tree): RawAst.Tree[] {
return treePath(root, (node) => {
const begin = node.whitespaceStartInCodeParsed + node.whitespaceLengthInCodeParsed
const end = begin + node.childrenLengthInCodeParsed
return charIndex >= begin && charIndex < end
}).reverse()
}
/** Given a predicate, return a path from the root down the tree containing the
* first node at each level found to satisfy the predicate. */
function treePath(obj: LazyObject, pred: (node: RawAst.Tree) => boolean): RawAst.Tree[] {
const path: RawAst.Tree[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj)) {
const isMatch = pred(obj)
if (isMatch) path.push(obj)
return obj.visitChildren(visitor) || isMatch
} else {
return obj.visitChildren(visitor)
}
}
obj.visitChildren(visitor)
return path
}
export function findAstWithRange(
root: RawAst.Tree | RawAst.Token,
range: SourceRange,
): RawAst.Tree | RawAst.Token | undefined {
for (const child of childrenAstNodes(root)) {
const [begin, end] = parsedTreeOrTokenRange(child)
if (begin === range[0] && end === range[1]) return child
if (begin <= range[0] && end >= range[1]) return findAstWithRange(child, range)
}
}
export function* walkRecursive(
node: RawAst.Tree | RawAst.Token,
): Generator<RawAst.Tree | RawAst.Token, void, boolean | void> {
if (false === (yield node)) return
const stack: Iterator<RawAst.Tree | RawAst.Token>[] = [childrenAstNodesOrTokens(node).values()]
while (stack.length > 0) {
const next = stack[stack.length - 1]!.next()
if (next.done) stack.pop()
else if (false !== (yield next.value)) stack.push(childrenAstNodesOrTokens(next.value).values())
}
}
export function visitGenerator<T, N, R>(generator: Generator<T, R, N>, visit: (value: T) => N): R {
let next = generator.next()
while (!next.done) next = generator.next(visit(next.value))
return next.value
}
/**
* Recursively visit AST nodes in depth-first order. The children of a node will be skipped when
* `visit` callback returns `false`.
*
* @param node Root node of the tree to walk. It will be visited first.
* @param visit Callback that is called for each node. If it returns `false`, the children of that
* node will be skipped, and the walk will continue to the next sibling.
*/
export function visitRecursive(
node: RawAst.Tree | RawAst.Token,
visit: (node: RawAst.Tree | RawAst.Token) => boolean,
) {
visitGenerator(walkRecursive(node), visit)
}
/**
* Read ast span information in `String.substring` compatible way. The returned span does not
* include left whitespace offset.
*
* @returns Object with `start` and `end` properties; index of first character in the `node`
* and first character _not_ being in the `node`.
*/
export function parsedTreeRange(tree: RawAst.Tree): SourceRange {
const start = tree.whitespaceStartInCodeParsed + tree.whitespaceLengthInCodeParsed
const end = start + tree.childrenLengthInCodeParsed
return [start, end]
}
export function parsedTokenRange(token: RawAst.Token): SourceRange {
const start = token.startInCodeBuffer
const end = start + token.lengthInCodeBuffer
return [start, end]
}
export function parsedTreeOrTokenRange(node: HasAstRange): SourceRange {
if (RawAst.Tree.isInstance(node)) return parsedTreeRange(node)
else if (RawAst.Token.isInstance(node)) return parsedTokenRange(node)
else return node
}
export function astPrettyPrintType(obj: unknown): string | undefined {
if (obj instanceof LazyObject && Object.hasOwnProperty.call(obj, 'type')) {
const proto = Object.getPrototypeOf(obj)
return proto?.constructor?.name
}
}
export function debugAst(obj: unknown): unknown {
if (obj instanceof LazyObject) {
const fields = Object.fromEntries(
allGetterNames(obj).map((k) => [k, debugAst((obj as any)[k])]),
)
if (Object.hasOwnProperty.call(obj, 'type')) {
const className = astPrettyPrintType(obj)
return { type: className, ...fields }
} else {
return fields
}
} else if (obj instanceof LazySequence) {
return Array.from(obj, debugAst)
} else if (isResult(obj)) {
return mapOk(obj, debugAst)
} else {
return obj
}
}
const protoGetters = new Map()
function allGetterNames(obj: object): string[] {
let proto = Object.getPrototypeOf(obj)
return map.setIfUndefined(protoGetters, proto, () => {
const props = new Map<string, PropertyDescriptor>()
do {
for (const [name, prop] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
if (!props.has(name)) props.set(name, prop)
}
} while ((proto = Object.getPrototypeOf(proto)))
const getters = new Set<string>()
for (const [name, prop] of props.entries()) {
if (prop.get != null && prop.configurable && !debugHideFields.includes(name)) {
getters.add(name)
}
}
return [...getters]
})
}
const debugHideFields = [
'_v',
'__proto__',
'codeReprBegin',
'codeReprLen',
'leftOffsetCodeReprBegin',
'leftOffsetCodeReprLen',
'leftOffsetVisible',
'spanLeftOffsetCodeReprBegin',
'spanLeftOffsetCodeReprLen',
'spanLeftOffsetVisible',
]
export * as Ast from '@/util/ast/abstract'
export * as RawAst from 'shared/ast/generated/ast'

View File

@ -1,5 +1,6 @@
import { assert } from '@/util/assert'
import { RawAst, RawAstExtended } from '@/util/ast'
import { RawAstExtended } from '@/util/ast/extended'
import { RawAst } from '@/util/ast/raw'
import { zip } from '@/util/data/iterable'
import { mapIterator } from 'lib0/iterator'

View File

@ -0,0 +1,235 @@
import { assert, assertDefined } from '@/util/assert'
import * as map from 'lib0/map'
import * as RawAst from 'shared/ast/generated/ast'
import { parseEnso } from 'shared/ast/parse'
import { LazyObject, LazySequence } from 'shared/ast/parserSupport'
import { tryGetSoleValue } from 'shared/util/data/iterable'
import { isResult, mapOk } from 'shared/util/data/result'
import type { SourceRange } from 'shared/yjsModel'
export { RawAst, parseEnso }
export type HasAstRange = SourceRange | RawAst.Tree | RawAst.Token
/** Read a single line of code
*
* Is meant to be a helper for tests. If the code is multiline, an exception is raised.
*/
export function parseEnsoLine(code: string): RawAst.Tree {
const block = parseEnso(code)
assert(block.type === RawAst.Tree.Type.BodyBlock)
const soleExpression = tryGetSoleValue(block.statements)?.expression
assertDefined(soleExpression)
return soleExpression
}
/**
* Read span of code represented by given AST node, not including left whitespace offset.
*
* The AST is assumed to be generated from `code` and not modified sice then.
* Otherwise an unspecified fragment of `code` may be returned.
*/
export function readAstOrTokenSpan(node: RawAst.Tree | RawAst.Token, code: string): string {
const range = parsedTreeOrTokenRange(node)
return code.substring(range[0], range[1])
}
/**
* Read span of code represented by given RawAst.Tree.
*
* The Tree is assumed to be a part of AST generated from `code`.
*/
export function readAstSpan(node: RawAst.Tree, code: string): string {
const range = parsedTreeRange(node)
return code.substring(range[0], range[1])
}
/**
* Read span of code represented by given RawAst.Token.
*
* The Token is assumed to be a part of AST generated from `code`.
*/
export function readTokenSpan(token: RawAst.Token, code: string): string {
const range = parsedTokenRange(token)
return code.substring(range[0], range[1])
}
/**
* Read direct AST children.
*/
export function childrenAstNodes(obj: LazyObject): RawAst.Tree[] {
const children: RawAst.Tree[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj)) children.push(obj)
else if (!RawAst.Token.isInstance(obj)) obj.visitChildren(visitor)
}
obj.visitChildren(visitor)
return children
}
export function childrenAstNodesOrTokens(obj: LazyObject): (RawAst.Tree | RawAst.Token)[] {
const children: (RawAst.Tree | RawAst.Token)[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj) || RawAst.Token.isInstance(obj)) {
children.push(obj)
} else {
obj.visitChildren(visitor)
}
}
obj.visitChildren(visitor)
return children
}
/** Returns all AST nodes from `root` tree containing given char, starting from the most nested. */
export function astContainingChar(charIndex: number, root: RawAst.Tree): RawAst.Tree[] {
return treePath(root, (node) => {
const begin = node.whitespaceStartInCodeParsed + node.whitespaceLengthInCodeParsed
const end = begin + node.childrenLengthInCodeParsed
return charIndex >= begin && charIndex < end
}).reverse()
}
/** Given a predicate, return a path from the root down the tree containing the
* first node at each level found to satisfy the predicate. */
function treePath(obj: LazyObject, pred: (node: RawAst.Tree) => boolean): RawAst.Tree[] {
const path: RawAst.Tree[] = []
const visitor = (obj: LazyObject) => {
if (RawAst.Tree.isInstance(obj)) {
const isMatch = pred(obj)
if (isMatch) path.push(obj)
return obj.visitChildren(visitor) || isMatch
} else {
return obj.visitChildren(visitor)
}
}
obj.visitChildren(visitor)
return path
}
export function findAstWithRange(
root: RawAst.Tree | RawAst.Token,
range: SourceRange,
): RawAst.Tree | RawAst.Token | undefined {
for (const child of childrenAstNodes(root)) {
const [begin, end] = parsedTreeOrTokenRange(child)
if (begin === range[0] && end === range[1]) return child
if (begin <= range[0] && end >= range[1]) return findAstWithRange(child, range)
}
}
export function* walkRecursive(
node: RawAst.Tree | RawAst.Token,
): Generator<RawAst.Tree | RawAst.Token, void, boolean | void> {
if (false === (yield node)) return
const stack: Iterator<RawAst.Tree | RawAst.Token>[] = [childrenAstNodesOrTokens(node).values()]
while (stack.length > 0) {
const next = stack[stack.length - 1]!.next()
if (next.done) stack.pop()
else if (false !== (yield next.value)) stack.push(childrenAstNodesOrTokens(next.value).values())
}
}
export function visitGenerator<T, N, R>(generator: Generator<T, R, N>, visit: (value: T) => N): R {
let next = generator.next()
while (!next.done) next = generator.next(visit(next.value))
return next.value
}
/**
* Recursively visit AST nodes in depth-first order. The children of a node will be skipped when
* `visit` callback returns `false`.
*
* @param node Root node of the tree to walk. It will be visited first.
* @param visit Callback that is called for each node. If it returns `false`, the children of that
* node will be skipped, and the walk will continue to the next sibling.
*/
export function visitRecursive(
node: RawAst.Tree | RawAst.Token,
visit: (node: RawAst.Tree | RawAst.Token) => boolean,
) {
visitGenerator(walkRecursive(node), visit)
}
/**
* Read ast span information in `String.substring` compatible way. The returned span does not
* include left whitespace offset.
*
* @returns Object with `start` and `end` properties; index of first character in the `node`
* and first character _not_ being in the `node`.
*/
export function parsedTreeRange(tree: RawAst.Tree): SourceRange {
const start = tree.whitespaceStartInCodeParsed + tree.whitespaceLengthInCodeParsed
const end = start + tree.childrenLengthInCodeParsed
return [start, end]
}
export function parsedTokenRange(token: RawAst.Token): SourceRange {
const start = token.startInCodeBuffer
const end = start + token.lengthInCodeBuffer
return [start, end]
}
export function parsedTreeOrTokenRange(node: HasAstRange): SourceRange {
if (RawAst.Tree.isInstance(node)) return parsedTreeRange(node)
else if (RawAst.Token.isInstance(node)) return parsedTokenRange(node)
else return node
}
export function astPrettyPrintType(obj: unknown): string | undefined {
if (obj instanceof LazyObject && Object.hasOwnProperty.call(obj, 'type')) {
const proto = Object.getPrototypeOf(obj)
return proto?.constructor?.name
}
}
export function debugAst(obj: unknown): unknown {
if (obj instanceof LazyObject) {
const fields = Object.fromEntries(
allGetterNames(obj).map((k) => [k, debugAst((obj as any)[k])]),
)
if (Object.hasOwnProperty.call(obj, 'type')) {
const className = astPrettyPrintType(obj)
return { type: className, ...fields }
} else {
return fields
}
} else if (obj instanceof LazySequence) {
return Array.from(obj, debugAst)
} else if (isResult(obj)) {
return mapOk(obj, debugAst)
} else {
return obj
}
}
const protoGetters = new Map()
function allGetterNames(obj: object): string[] {
let proto = Object.getPrototypeOf(obj)
return map.setIfUndefined(protoGetters, proto, () => {
const props = new Map<string, PropertyDescriptor>()
do {
for (const [name, prop] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
if (!props.has(name)) props.set(name, prop)
}
} while ((proto = Object.getPrototypeOf(proto)))
const getters = new Set<string>()
for (const [name, prop] of props.entries()) {
if (prop.get != null && prop.configurable && !debugHideFields.includes(name)) {
getters.add(name)
}
}
return [...getters]
})
}
const debugHideFields = [
'_v',
'__proto__',
'codeReprBegin',
'codeReprLen',
'leftOffsetCodeReprBegin',
'leftOffsetCodeReprLen',
'leftOffsetVisible',
'spanLeftOffsetCodeReprBegin',
'spanLeftOffsetCodeReprLen',
'spanLeftOffsetVisible',
]