mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Display argument placeholders for autoscoped constructors (#9737)
Fixes #9635 <img width="925" alt="Screenshot 2024-04-18 at 2 43 09 PM" src="https://github.com/enso-org/enso/assets/6566674/fbdce484-ac0b-4e30-8577-1c9dc419dffe">
This commit is contained in:
parent
70985932b1
commit
717f6bb330
@ -5,7 +5,7 @@ import { mockCollapsedFunctionInfo } from './expressionUpdates'
|
||||
import { CONTROL_KEY } from './keyboard'
|
||||
import * as locate from './locate'
|
||||
|
||||
const MAIN_FILE_NODES = 11
|
||||
const MAIN_FILE_NODES = 12
|
||||
|
||||
const COLLAPSE_SHORTCUT = `${CONTROL_KEY}+G`
|
||||
|
||||
@ -120,6 +120,7 @@ async function expectInsideMain(page: Page) {
|
||||
await expect(locate.graphNodeByBinding(page, 'data')).toExist()
|
||||
await expect(locate.graphNodeByBinding(page, 'aggregated')).toExist()
|
||||
await expect(locate.graphNodeByBinding(page, 'filtered')).toExist()
|
||||
await expect(locate.graphNodeByBinding(page, 'autoscoped')).toExist()
|
||||
}
|
||||
|
||||
async function expectInsideFunc1(page: Page) {
|
||||
|
@ -101,7 +101,7 @@ test('Graph Editor pans to Component Browser', async ({ page }) => {
|
||||
await expect(locate.graphNodeByBinding(page, 'five')).toBeInViewport()
|
||||
const outputPort = await locate.outputPortCoordinates(locate.graphNodeByBinding(page, 'final'))
|
||||
await page.mouse.click(outputPort.x, outputPort.y)
|
||||
await page.mouse.click(100, 1550)
|
||||
await page.mouse.click(100, 1700)
|
||||
await expect(locate.graphNodeByBinding(page, 'five')).not.toBeInViewport()
|
||||
await expectAndCancelBrowser(page, 'final.')
|
||||
})
|
||||
|
@ -12,7 +12,7 @@ test('Existence of edges between nodes', async ({ page }) => {
|
||||
|
||||
await expect(await edgesFromNodeWithBinding(page, 'aggregated')).toHaveCount(0)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'filtered')).toHaveCount(0)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'data')).toHaveCount(3 * EDGE_PARTS)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'data')).toHaveCount(4 * EDGE_PARTS)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'list')).toHaveCount(0)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'final')).toHaveCount(0)
|
||||
await expect(await edgesFromNodeWithBinding(page, 'selected')).toHaveCount(0)
|
||||
|
@ -440,3 +440,39 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
||||
// '"',
|
||||
// ])
|
||||
})
|
||||
|
||||
// Test that autoscoped constructors provide argument placeholders.
|
||||
// This test can be removed when `aggregate` inserts autoscoped constructors by default,
|
||||
// so this behavior will be tested in regular `aggregate` tests.
|
||||
test('Autoscoped constructors', async ({ page }) => {
|
||||
await actions.goToGraph(page)
|
||||
await mockMethodCallInfo(page, 'autoscoped', {
|
||||
methodPointer: {
|
||||
module: 'Standard.Table.Table',
|
||||
definedOnType: 'Standard.Table.Table.Table',
|
||||
name: 'aggregate',
|
||||
},
|
||||
notAppliedArguments: [2, 3],
|
||||
})
|
||||
await mockMethodCallInfo(
|
||||
page,
|
||||
{ binding: 'autoscoped', expr: '..Group_By' },
|
||||
{
|
||||
methodPointer: {
|
||||
module: 'Standard.Table.Aggregate_Column',
|
||||
definedOnType: 'Standard.Table.Aggregate_Column.Aggregate_Column',
|
||||
name: 'Group_By',
|
||||
},
|
||||
notAppliedArguments: [0, 1],
|
||||
},
|
||||
)
|
||||
const node = locate.graphNodeByBinding(page, 'autoscoped')
|
||||
const topLevelArgs = node.locator('.WidgetTopLevelArgument')
|
||||
// Wait for hidden arguments to appear after selecting the node.
|
||||
await node.click()
|
||||
await expect(topLevelArgs).toHaveCount(3)
|
||||
|
||||
const groupBy = node.locator('.item').nth(0)
|
||||
await expect(groupBy).toBeVisible()
|
||||
await expect(groupBy.locator('.WidgetArgumentName')).toContainText(['column', 'new_name'])
|
||||
})
|
||||
|
@ -17,6 +17,7 @@ const conf = [
|
||||
'templates',
|
||||
'.histoire',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
],
|
||||
},
|
||||
...compat.extends('plugin:vue/vue3-recommended'),
|
||||
|
@ -70,6 +70,7 @@ main =
|
||||
data = Data.read
|
||||
filtered = data.filter
|
||||
aggregated = data.aggregate
|
||||
autoscoped = data.aggregate [..Group_By]
|
||||
selected = data.select_columns
|
||||
`
|
||||
|
||||
|
@ -48,7 +48,7 @@ export default defineConfig({
|
||||
use: {
|
||||
headless: !DEBUG,
|
||||
trace: 'retain-on-failure',
|
||||
viewport: { width: 1920, height: 1600 },
|
||||
viewport: { width: 1920, height: 1750 },
|
||||
...(DEBUG ?
|
||||
{}
|
||||
: {
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
App,
|
||||
Assignment,
|
||||
Ast,
|
||||
AutoscopedIdentifier,
|
||||
BodyBlock,
|
||||
Documented,
|
||||
Function,
|
||||
@ -203,6 +204,12 @@ class Abstractor {
|
||||
}
|
||||
break
|
||||
}
|
||||
case RawAst.Tree.Type.AutoscopedIdentifier: {
|
||||
const opr = this.abstractToken(tree.opr)
|
||||
const ident = this.abstractToken(tree.ident)
|
||||
node = AutoscopedIdentifier.concrete(this.module, opr, ident)
|
||||
break
|
||||
}
|
||||
case RawAst.Tree.Type.OprApp: {
|
||||
const lhs = tree.lhs ? this.abstractTree(tree.lhs) : undefined
|
||||
const opr =
|
||||
|
@ -77,6 +77,7 @@ export interface IdentifierToken extends Token {
|
||||
|
||||
declare const qualifiedNameBrand: unique symbol
|
||||
declare const identifierBrand: unique symbol
|
||||
declare const typeOrConsIdentifierBrand: unique symbol
|
||||
declare const operatorBrand: unique symbol
|
||||
|
||||
/** A string representing a valid qualified name of our language.
|
||||
@ -89,6 +90,9 @@ export type QualifiedName = string & { [qualifiedNameBrand]: never }
|
||||
/** A string representing a lexical identifier. */
|
||||
export type Identifier = string & { [identifierBrand]: never; [qualifiedNameBrand]: never }
|
||||
|
||||
/** A specific subtype of capitalized identifier, used for type and constructor names. */
|
||||
export type TypeOrConstructorIdentifier = Identifier & { [typeOrConsIdentifierBrand]: never }
|
||||
|
||||
/** A string representing a lexical operator. */
|
||||
export type Operator = string & { [operatorBrand]: never; [qualifiedNameBrand]: never }
|
||||
|
||||
@ -121,6 +125,11 @@ export function isIdentifier(code: string): code is Identifier {
|
||||
return is_ident_or_operator(code) === 1
|
||||
}
|
||||
|
||||
export function isTypeOrConsIdentifier(code: string): code is TypeOrConstructorIdentifier {
|
||||
const isUppercase = (s: string) => s.toUpperCase() === s && s.toLowerCase() !== s
|
||||
return isIdentifier(code) && code.length > 0 && isUppercase(code[0]!)
|
||||
}
|
||||
|
||||
export function identifier(code: string): Identifier | undefined {
|
||||
if (isIdentifier(code)) return code
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
RawNodeChild,
|
||||
SpanMap,
|
||||
SyncTokenId,
|
||||
TypeOrConstructorIdentifier,
|
||||
} from '.'
|
||||
import {
|
||||
MutableModule,
|
||||
@ -763,6 +764,61 @@ export interface MutableUnaryOprApp extends UnaryOprApp, MutableAst {
|
||||
}
|
||||
applyMixins(MutableUnaryOprApp, [MutableAst])
|
||||
|
||||
interface AutoscopedIdentifierFields {
|
||||
operator: NodeChild<SyncTokenId>
|
||||
identifier: NodeChild<SyncTokenId>
|
||||
}
|
||||
export class AutoscopedIdentifier extends Ast {
|
||||
declare fields: FixedMapView<AstFields & AutoscopedIdentifierFields>
|
||||
constructor(module: Module, fields: FixedMapView<AstFields & AutoscopedIdentifierFields>) {
|
||||
super(module, fields)
|
||||
}
|
||||
|
||||
static tryParse(
|
||||
source: string,
|
||||
module?: MutableModule,
|
||||
): Owned<MutableAutoscopedIdentifier> | undefined {
|
||||
const parsed = parse(source, module)
|
||||
if (parsed instanceof MutableAutoscopedIdentifier) return parsed
|
||||
}
|
||||
|
||||
static concrete(module: MutableModule, operator: NodeChild<Token>, identifier: NodeChild<Token>) {
|
||||
const base = module.baseObject('AutoscopedIdentifier')
|
||||
const fields = composeFieldData(base, {
|
||||
operator,
|
||||
identifier,
|
||||
})
|
||||
return asOwned(new MutableAutoscopedIdentifier(module, fields))
|
||||
}
|
||||
|
||||
static new(
|
||||
identifier: TypeOrConstructorIdentifier,
|
||||
module?: MutableModule,
|
||||
): Owned<MutableAutoscopedIdentifier> {
|
||||
const module_ = module || MutableModule.Transient()
|
||||
const operator = Token.new('..')
|
||||
const ident = Token.new(identifier, RawAst.Token.Type.Ident)
|
||||
return this.concrete(module_, unspaced(operator), unspaced(ident))
|
||||
}
|
||||
|
||||
*concreteChildren(_verbatim?: boolean): IterableIterator<RawNodeChild> {
|
||||
const { operator, identifier } = getAll(this.fields)
|
||||
yield operator
|
||||
yield identifier
|
||||
}
|
||||
}
|
||||
export class MutableAutoscopedIdentifier extends AutoscopedIdentifier implements MutableAst {
|
||||
declare readonly module: MutableModule
|
||||
declare readonly fields: FixedMap<AstFields & AutoscopedIdentifierFields>
|
||||
|
||||
setIdentifier(value: TypeOrConstructorIdentifier) {
|
||||
const token = Token.new(value, RawAst.Token.Type.Ident)
|
||||
this.fields.set('identifier', unspaced(token))
|
||||
}
|
||||
}
|
||||
export interface MutableAutoscopedIdentifier extends AutoscopedIdentifier, MutableAst {}
|
||||
applyMixins(MutableAutoscopedIdentifier, [MutableAst])
|
||||
|
||||
interface NegationAppFields {
|
||||
operator: NodeChild<SyncTokenId>
|
||||
argument: NodeChild<AstId>
|
||||
@ -2369,6 +2425,8 @@ export function materializeMutable(module: MutableModule, fields: FixedMap<AstFi
|
||||
return new MutableTextLiteral(module, fieldsForType)
|
||||
case 'UnaryOprApp':
|
||||
return new MutableUnaryOprApp(module, fieldsForType)
|
||||
case 'AutoscopedIdentifier':
|
||||
return new MutableAutoscopedIdentifier(module, fieldsForType)
|
||||
case 'Vector':
|
||||
return new MutableVector(module, fieldsForType)
|
||||
case 'Wildcard':
|
||||
@ -2413,6 +2471,8 @@ export function materialize(module: Module, fields: FixedMapView<AstFields>): As
|
||||
return new TextLiteral(module, fields_)
|
||||
case 'UnaryOprApp':
|
||||
return new UnaryOprApp(module, fields_)
|
||||
case 'AutoscopedIdentifier':
|
||||
return new AutoscopedIdentifier(module, fields_)
|
||||
case 'Vector':
|
||||
return new Vector(module, fields_)
|
||||
case 'Wildcard':
|
||||
|
@ -65,7 +65,8 @@ export namespace WidgetInput {
|
||||
input.value instanceof Ast.App ||
|
||||
input.value instanceof Ast.Ident ||
|
||||
input.value instanceof Ast.PropertyAccess ||
|
||||
input.value instanceof Ast.OprApp
|
||||
input.value instanceof Ast.OprApp ||
|
||||
input.value instanceof Ast.AutoscopedIdentifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -636,7 +636,11 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
exprId = nodeId
|
||||
}
|
||||
|
||||
if (exprId == null) bail(`Cannot find expression located by ${locator}`)
|
||||
if (exprId == null) {
|
||||
const locatorStr =
|
||||
typeof locator === 'string' ? locator : `${locator.binding}/${locator.expr}`
|
||||
bail(`Cannot find expression located by ${locatorStr}`)
|
||||
}
|
||||
|
||||
const update_: ExpressionUpdate = {
|
||||
expressionId: db.idToExternal(exprId)!,
|
||||
|
Loading…
Reference in New Issue
Block a user