mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Fix CB interaction with record-mode override and add tests (#9386)
Fixes #7976.
This commit is contained in:
parent
85ddbc5f24
commit
47d9e73ead
@ -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}`)
|
||||
})
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
1435
app/gui2/src/util/ast/__tests__/__snapshots__/raw.test.ts.snap
Normal file
1435
app/gui2/src/util/ast/__tests__/__snapshots__/raw.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -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' },
|
||||
{
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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)
|
||||
})
|
@ -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'
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
||||
|
235
app/gui2/src/util/ast/raw.ts
Normal file
235
app/gui2/src/util/ast/raw.ts
Normal 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',
|
||||
]
|
Loading…
Reference in New Issue
Block a user