mirror of
https://github.com/enso-org/enso.git
synced 2024-12-25 11:23:55 +03:00
Vue dependency update, better selection performance, visible quotes in text inputs (#9204)
- Improved performance by batching simulatenous node edits, including metadata updates when dragging many selected nodes together. - Updated Vue to new version, allowing us to use `defineModel`. - Fixed #9161 - Unified all handling of auto-blur by making `useAutoBlur` cheap to register - all logic goes through a single window event handler. - Combined all `ResizeObserver`s into one. - Fixed the behaviour of repeated toast messages. Now only the latest compilation status is visible at any given time, and the errors disappear once compilation passes. - Actually fixed broken interaction of node and visualization widths. There no longer is a style feedback loop and the visible node backdrop width no longer jumps or randomly fails to update.
This commit is contained in:
parent
d4b2390fc1
commit
b7a8909818
@ -1 +1 @@
|
|||||||
18.14.1
|
20.11.1
|
||||||
|
@ -233,8 +233,8 @@ class TableVisualization extends Visualization {
|
|||||||
parsedData.data && parsedData.data.length > 0
|
parsedData.data && parsedData.data.length > 0
|
||||||
? parsedData.data[0].length
|
? parsedData.data[0].length
|
||||||
: parsedData.indices && parsedData.indices.length > 0
|
: parsedData.indices && parsedData.indices.length > 0
|
||||||
? parsedData.indices[0].length
|
? parsedData.indices[0].length
|
||||||
: 0
|
: 0
|
||||||
rowData = Array.apply(null, Array(rows)).map((_, i) => {
|
rowData = Array.apply(null, Array(rows)).map((_, i) => {
|
||||||
const row = {}
|
const row = {}
|
||||||
const shift = parsedData.indices ? parsedData.indices.length : 0
|
const shift = parsedData.indices ? parsedData.indices.length : 0
|
||||||
|
@ -6,5 +6,6 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"organizeImportsSkipDestructiveCodeActions": true
|
"organizeImportsSkipDestructiveCodeActions": true,
|
||||||
|
"experimentalTernaries": true
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,8 @@ onMounted(() => {
|
|||||||
</MockProjectStoreWrapper>
|
</MockProjectStoreWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
:is(.viewport) {
|
:deep(.viewport) {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: var(--font-code);
|
font-family: var(--font-code);
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { expect, type Page } from '@playwright/test'
|
import { type Page } from '@playwright/test'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
import { graphNodeByBinding } from './locate'
|
import { graphNodeByBinding } from './locate'
|
||||||
|
|
||||||
@ -12,14 +12,25 @@ export async function goToGraph(page: Page) {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await expect(page.locator('.App')).toBeVisible()
|
await expect(page.locator('.App')).toBeVisible()
|
||||||
// Wait until nodes are loaded.
|
// Wait until nodes are loaded.
|
||||||
await customExpect.toExist(locate.graphNode(page))
|
await expect(locate.graphNode(page)).toExist()
|
||||||
// Wait for position initialization
|
// Wait for position initialization
|
||||||
|
await expectNodePositionsInitialized(page, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectNodePositionsInitialized(page: Page, yPos: number) {
|
||||||
|
// TODO: The yPos should not need to be a variable. Instead, first automatically positioned nodes
|
||||||
|
// should always have constant known position. This is a bug caused by incorrect layout after
|
||||||
|
// entering a function. To be fixed with #9255
|
||||||
await expect(locate.graphNode(page).first()).toHaveCSS(
|
await expect(locate.graphNode(page).first()).toHaveCSS(
|
||||||
'transform',
|
'transform',
|
||||||
'matrix(1, 0, 0, 1, -16, -16)',
|
`matrix(1, 0, 0, 1, -16, ${yPos})`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function exitFunction(page: Page, x = 300, y = 300) {
|
||||||
|
await page.mouse.dblclick(x, y, { delay: 10 })
|
||||||
|
}
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
// === Drag Node ===
|
// === Drag Node ===
|
||||||
// =================
|
// =================
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { expect, test, type Page } from '@playwright/test'
|
import { test, type Page } from '@playwright/test'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import { mockCollapsedFunctionInfo } from './expressionUpdates'
|
import { mockCollapsedFunctionInfo } from './expressionUpdates'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
@ -27,19 +27,19 @@ test('Leaving entered nodes', async ({ page }) => {
|
|||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
await enterToFunc2(page)
|
await enterToFunc2(page)
|
||||||
|
|
||||||
await page.mouse.dblclick(100, 100)
|
await actions.exitFunction(page)
|
||||||
await expectInsideFunc1(page)
|
await expectInsideFunc1(page)
|
||||||
|
|
||||||
await page.mouse.dblclick(100, 100)
|
await actions.exitFunction(page)
|
||||||
await expectInsideMain(page)
|
await expectInsideMain(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Using breadcrumbs to navigate', async ({ page }) => {
|
test('Using breadcrumbs to navigate', async ({ page }) => {
|
||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
await enterToFunc2(page)
|
await enterToFunc2(page)
|
||||||
await page.mouse.dblclick(100, 100)
|
await actions.exitFunction(page)
|
||||||
await expectInsideFunc1(page)
|
await expectInsideFunc1(page)
|
||||||
await page.mouse.dblclick(100, 100)
|
await actions.exitFunction(page)
|
||||||
await expectInsideMain(page)
|
await expectInsideMain(page)
|
||||||
// Breadcrumbs still have all the crumbs, but the last two are dimmed.
|
// Breadcrumbs still have all the crumbs, but the last two are dimmed.
|
||||||
await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1', 'func2'])
|
await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1', 'func2'])
|
||||||
@ -85,9 +85,9 @@ test('Collapsing nodes', async ({ page }) => {
|
|||||||
|
|
||||||
await collapsedNode.dblclick()
|
await collapsedNode.dblclick()
|
||||||
await expect(locate.graphNode(page)).toHaveCount(4)
|
await expect(locate.graphNode(page)).toHaveCount(4)
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'ten'))
|
await expect(locate.graphNodeByBinding(page, 'ten')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'sum'))
|
await expect(locate.graphNodeByBinding(page, 'sum')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'prod'))
|
await expect(locate.graphNodeByBinding(page, 'prod')).toExist()
|
||||||
|
|
||||||
await locate
|
await locate
|
||||||
.graphNodeByBinding(page, 'ten')
|
.graphNodeByBinding(page, 'ten')
|
||||||
@ -103,31 +103,34 @@ test('Collapsing nodes', async ({ page }) => {
|
|||||||
await mockCollapsedFunctionInfo(page, 'ten', 'collapsed1')
|
await mockCollapsedFunctionInfo(page, 'ten', 'collapsed1')
|
||||||
await secondCollapsedNode.dblclick()
|
await secondCollapsedNode.dblclick()
|
||||||
await expect(locate.graphNode(page)).toHaveCount(2)
|
await expect(locate.graphNode(page)).toHaveCount(2)
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'ten'))
|
await expect(locate.graphNodeByBinding(page, 'ten')).toExist()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function expectInsideMain(page: Page) {
|
async function expectInsideMain(page: Page) {
|
||||||
|
await actions.expectNodePositionsInitialized(page, 64)
|
||||||
await expect(locate.graphNode(page)).toHaveCount(10)
|
await expect(locate.graphNode(page)).toHaveCount(10)
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'five'))
|
await expect(locate.graphNodeByBinding(page, 'five')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'ten'))
|
await expect(locate.graphNodeByBinding(page, 'ten')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'sum'))
|
await expect(locate.graphNodeByBinding(page, 'sum')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'prod'))
|
await expect(locate.graphNodeByBinding(page, 'prod')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'final'))
|
await expect(locate.graphNodeByBinding(page, 'final')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'list'))
|
await expect(locate.graphNodeByBinding(page, 'list')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'data'))
|
await expect(locate.graphNodeByBinding(page, 'data')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'aggregated'))
|
await expect(locate.graphNodeByBinding(page, 'aggregated')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'filtered'))
|
await expect(locate.graphNodeByBinding(page, 'filtered')).toExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectInsideFunc1(page: Page) {
|
async function expectInsideFunc1(page: Page) {
|
||||||
|
await actions.expectNodePositionsInitialized(page, 192)
|
||||||
await expect(locate.graphNode(page)).toHaveCount(3)
|
await expect(locate.graphNode(page)).toHaveCount(3)
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'f2'))
|
await expect(locate.graphNodeByBinding(page, 'f2')).toExist()
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'result'))
|
await expect(locate.graphNodeByBinding(page, 'result')).toExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectInsideFunc2(page: Page) {
|
async function expectInsideFunc2(page: Page) {
|
||||||
|
await actions.expectNodePositionsInitialized(page, 128)
|
||||||
await expect(locate.graphNode(page)).toHaveCount(2)
|
await expect(locate.graphNode(page)).toHaveCount(2)
|
||||||
await customExpect.toExist(locate.graphNodeByBinding(page, 'r'))
|
await expect(locate.graphNodeByBinding(page, 'r')).toExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterToFunc2(page: Page) {
|
async function enterToFunc2(page: Page) {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { expect, test, type Page } from '@playwright/test'
|
import { test, type Page } from '@playwright/test'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
const CONTROL_KEY = os.platform() === 'darwin' ? 'Meta' : 'Control'
|
const CONTROL_KEY = os.platform() === 'darwin' ? 'Meta' : 'Control'
|
||||||
@ -18,8 +18,8 @@ test('Different ways of opening Component Browser', async ({ page }) => {
|
|||||||
const nodeCount = await locate.graphNode(page).count()
|
const nodeCount = await locate.graphNode(page).count()
|
||||||
|
|
||||||
async function expectAndCancelBrowser(expectedInput: string) {
|
async function expectAndCancelBrowser(expectedInput: string) {
|
||||||
await customExpect.toExist(locate.componentBrowser(page))
|
await expect(locate.componentBrowser(page)).toExist()
|
||||||
await customExpect.toExist(locate.componentBrowserEntry(page))
|
await expect(locate.componentBrowserEntry(page)).toExist()
|
||||||
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(expectedInput)
|
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(expectedInput)
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(locate.componentBrowser(page)).not.toBeVisible()
|
await expect(locate.componentBrowser(page)).not.toBeVisible()
|
||||||
@ -79,7 +79,7 @@ test('Accepting suggestion', async ({ page }) => {
|
|||||||
'.',
|
'.',
|
||||||
'read_text',
|
'read_text',
|
||||||
])
|
])
|
||||||
await customExpect.toBeSelected(locate.graphNode(page).last())
|
await expect(locate.graphNode(page).last()).toBeSelected()
|
||||||
|
|
||||||
// Clicking at highlighted entry
|
// Clicking at highlighted entry
|
||||||
nodeCount = await locate.graphNode(page).count()
|
nodeCount = await locate.graphNode(page).count()
|
||||||
@ -93,7 +93,7 @@ test('Accepting suggestion', async ({ page }) => {
|
|||||||
'.',
|
'.',
|
||||||
'read',
|
'read',
|
||||||
])
|
])
|
||||||
await customExpect.toBeSelected(locate.graphNode(page).last())
|
await expect(locate.graphNode(page).last()).toBeSelected()
|
||||||
|
|
||||||
// Accepting with Enter
|
// Accepting with Enter
|
||||||
nodeCount = await locate.graphNode(page).count()
|
nodeCount = await locate.graphNode(page).count()
|
||||||
@ -107,7 +107,7 @@ test('Accepting suggestion', async ({ page }) => {
|
|||||||
'.',
|
'.',
|
||||||
'read',
|
'read',
|
||||||
])
|
])
|
||||||
await customExpect.toBeSelected(locate.graphNode(page).last())
|
await expect(locate.graphNode(page).last()).toBeSelected()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Accepting any written input', async ({ page }) => {
|
test('Accepting any written input', async ({ page }) => {
|
||||||
@ -127,14 +127,14 @@ test('Filling input with suggestions', async ({ page }) => {
|
|||||||
|
|
||||||
// Entering module
|
// Entering module
|
||||||
await locate.componentBrowserEntryByLabel(page, 'Standard.Base.Data').click()
|
await locate.componentBrowserEntryByLabel(page, 'Standard.Base.Data').click()
|
||||||
await customExpect.toExist(locate.componentBrowser(page))
|
await expect(locate.componentBrowser(page)).toExist()
|
||||||
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(
|
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(
|
||||||
'Standard.Base.Data.',
|
'Standard.Base.Data.',
|
||||||
)
|
)
|
||||||
|
|
||||||
// Applying suggestion
|
// Applying suggestion
|
||||||
await page.keyboard.press('Tab')
|
await page.keyboard.press('Tab')
|
||||||
await customExpect.toExist(locate.componentBrowser(page))
|
await expect(locate.componentBrowser(page)).toExist()
|
||||||
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(
|
await expect(locate.componentBrowserInput(page).locator('input')).toHaveValue(
|
||||||
'Standard.Base.Data.read ',
|
'Standard.Base.Data.read ',
|
||||||
)
|
)
|
||||||
@ -167,8 +167,8 @@ test('Editing existing nodes', async ({ page }) => {
|
|||||||
await expect(input).toHaveValue(`Data.read ${ADDED_PATH}`)
|
await expect(input).toHaveValue(`Data.read ${ADDED_PATH}`)
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await expect(locate.componentBrowser(page)).not.toBeVisible()
|
await expect(locate.componentBrowser(page)).not.toBeVisible()
|
||||||
await expect(node.locator('.WidgetToken')).toHaveText(['Data', '.', 'read'])
|
await expect(node.locator('.WidgetToken')).toHaveText(['Data', '.', 'read', '"', '"'])
|
||||||
await expect(node.locator('.WidgetText input')).toHaveValue(ADDED_PATH)
|
await expect(node.locator('.WidgetText input')).toHaveValue(ADDED_PATH.replaceAll('"', ''))
|
||||||
|
|
||||||
// Edit again, using "edit" button
|
// Edit again, using "edit" button
|
||||||
await locate.graphNodeIcon(node).click()
|
await locate.graphNodeIcon(node).click()
|
||||||
@ -187,12 +187,12 @@ test('Visualization preview: type-based visualization selection', async ({ page
|
|||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
const nodeCount = await locate.graphNode(page).count()
|
const nodeCount = await locate.graphNode(page).count()
|
||||||
await locate.addNewNodeButton(page).click()
|
await locate.addNewNodeButton(page).click()
|
||||||
await customExpect.toExist(locate.componentBrowser(page))
|
await expect(locate.componentBrowser(page)).toExist()
|
||||||
await customExpect.toExist(locate.componentBrowserEntry(page))
|
await expect(locate.componentBrowserEntry(page)).toExist()
|
||||||
const input = locate.componentBrowserInput(page).locator('input')
|
const input = locate.componentBrowserInput(page).locator('input')
|
||||||
await input.fill('4')
|
await input.fill('4')
|
||||||
await expect(input).toHaveValue('4')
|
await expect(input).toHaveValue('4')
|
||||||
await customExpect.toExist(locate.jsonVisualization(page))
|
await expect(locate.jsonVisualization(page)).toExist()
|
||||||
await input.fill('Table.ne')
|
await input.fill('Table.ne')
|
||||||
await expect(input).toHaveValue('Table.ne')
|
await expect(input).toHaveValue('Table.ne')
|
||||||
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
|
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
|
||||||
@ -207,12 +207,12 @@ test('Visualization preview: user visualization selection', async ({ page }) =>
|
|||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
const nodeCount = await locate.graphNode(page).count()
|
const nodeCount = await locate.graphNode(page).count()
|
||||||
await locate.addNewNodeButton(page).click()
|
await locate.addNewNodeButton(page).click()
|
||||||
await customExpect.toExist(locate.componentBrowser(page))
|
await expect(locate.componentBrowser(page)).toExist()
|
||||||
await customExpect.toExist(locate.componentBrowserEntry(page))
|
await expect(locate.componentBrowserEntry(page)).toExist()
|
||||||
const input = locate.componentBrowserInput(page).locator('input')
|
const input = locate.componentBrowserInput(page).locator('input')
|
||||||
await input.fill('4')
|
await input.fill('4')
|
||||||
await expect(input).toHaveValue('4')
|
await expect(input).toHaveValue('4')
|
||||||
await customExpect.toExist(locate.jsonVisualization(page))
|
await expect(locate.jsonVisualization(page)).toExist()
|
||||||
await locate.showVisualizationSelectorButton(page).click()
|
await locate.showVisualizationSelectorButton(page).click()
|
||||||
await page.getByRole('button', { name: 'Table' }).click()
|
await page.getByRole('button', { name: 'Table' }).click()
|
||||||
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
|
// The table visualization is not currently working with `executeExpression` (#9194), but we can test that the JSON
|
||||||
|
@ -1,19 +1,52 @@
|
|||||||
import { expect, type Locator } from 'playwright/test'
|
import { expect as baseExpect, type Locator } from 'playwright/test'
|
||||||
|
|
||||||
/** Ensures that at least one of the elements that the Locator points to,
|
export const expect = baseExpect.extend({
|
||||||
* is an attached and visible DOM node. */
|
/** Ensures that at least one of the elements that the Locator points to,
|
||||||
export function toExist(locator: Locator) {
|
* is an attached and visible DOM node. */
|
||||||
// Counter-intuitive, but correct:
|
async toExist(locator: Locator) {
|
||||||
// https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-visible
|
// Counter-intuitive, but correct:
|
||||||
return expect(locator.first()).toBeVisible()
|
// https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-visible
|
||||||
}
|
const assertionName = 'toExist'
|
||||||
|
let pass: boolean
|
||||||
|
try {
|
||||||
|
await expect(locator.first()).toBeVisible()
|
||||||
|
pass = true
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(e)
|
||||||
|
pass = false
|
||||||
|
}
|
||||||
|
|
||||||
export function toBeSelected(locator: Locator) {
|
const message = () =>
|
||||||
return expect(locator).toHaveClass(/(?<=^| )selected(?=$| )/)
|
this.utils.matcherHint(assertionName, locator, '', {
|
||||||
}
|
isNot: this.isNot,
|
||||||
|
})
|
||||||
|
|
||||||
export module not {
|
return {
|
||||||
export function toBeSelected(locator: Locator) {
|
message,
|
||||||
return expect(locator).not.toHaveClass(/(?<=^| )selected(?=$| )/)
|
pass,
|
||||||
}
|
name: assertionName,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeSelected(locator: Locator) {
|
||||||
|
const assertionName = 'toBeSelected'
|
||||||
|
let pass: boolean
|
||||||
|
try {
|
||||||
|
await baseExpect(locator).toHaveClass(/(?<=^| )selected(?=$| )/, { timeout: 50 })
|
||||||
|
pass = true
|
||||||
|
} catch (e: any) {
|
||||||
|
pass = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = () =>
|
||||||
|
this.utils.matcherHint(assertionName, locator, '', {
|
||||||
|
isNot: this.isNot,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
pass,
|
||||||
|
name: assertionName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { expect, test, type Page } from '@playwright/test'
|
import { test } from '@playwright/test'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
import { graphNodeByBinding } from './locate'
|
import { graphNodeByBinding } from './locate'
|
||||||
|
|
||||||
@ -17,11 +17,11 @@ test('Load Fullscreen Visualisation', async ({ page }) => {
|
|||||||
const fullscreenButton = locate.enterFullscreenButton(aggregatedNode)
|
const fullscreenButton = locate.enterFullscreenButton(aggregatedNode)
|
||||||
await fullscreenButton.click()
|
await fullscreenButton.click()
|
||||||
const vis = locate.jsonVisualization(page)
|
const vis = locate.jsonVisualization(page)
|
||||||
await customExpect.toExist(vis)
|
await expect(vis).toExist()
|
||||||
await customExpect.toExist(locate.exitFullscreenButton(page))
|
await expect(locate.exitFullscreenButton(page)).toExist()
|
||||||
const visBoundingBox = await vis.boundingBox()
|
const visBoundingBox = await vis.boundingBox()
|
||||||
expect(visBoundingBox!.height).toBe(808)
|
expect(visBoundingBox?.height).toBeGreaterThan(600)
|
||||||
expect(visBoundingBox!.width).toBe(1920)
|
expect(visBoundingBox?.width).toBe(1920)
|
||||||
const jsonContent = await vis.textContent().then((text) => JSON.parse(text!))
|
const jsonContent = await vis.textContent().then((text) => JSON.parse(text!))
|
||||||
expect(jsonContent).toEqual({
|
expect(jsonContent).toEqual({
|
||||||
axis: {
|
axis: {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { test } from '@playwright/test'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
test('node can open and load visualization', async ({ page }) => {
|
test('node can open and load visualization', async ({ page }) => {
|
||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
const node = locate.graphNode(page).last()
|
const node = locate.graphNode(page).last()
|
||||||
await node.click({ position: { x: 8, y: 8 } })
|
await node.click({ position: { x: 8, y: 8 } })
|
||||||
await customExpect.toExist(locate.circularMenu(page))
|
await expect(locate.circularMenu(page)).toExist()
|
||||||
await locate.toggleVisualizationButton(page).click()
|
await locate.toggleVisualizationButton(page).click()
|
||||||
await customExpect.toExist(locate.anyVisualization(page))
|
await expect(locate.anyVisualization(page)).toExist()
|
||||||
await locate.showVisualizationSelectorButton(page).click()
|
await locate.showVisualizationSelectorButton(page).click()
|
||||||
await page.getByText('JSON').click()
|
await page.getByText('JSON').click()
|
||||||
await customExpect.toExist(locate.jsonVisualization(page))
|
await expect(locate.jsonVisualization(page)).toExist()
|
||||||
// The default JSON viz data contains an object.
|
// The default JSON viz data contains an object.
|
||||||
await expect(locate.jsonVisualization(page)).toContainText('{')
|
await expect(locate.jsonVisualization(page)).toContainText('{')
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { test } from '@playwright/test'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
test('graph can open and render nodes', async ({ page }) => {
|
test('graph can open and render nodes', async ({ page }) => {
|
||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
await customExpect.toExist(locate.graphEditor(page))
|
await expect(locate.graphEditor(page)).toExist()
|
||||||
await customExpect.toExist(locate.graphNode(page))
|
await expect(locate.graphNode(page)).toExist()
|
||||||
|
|
||||||
// check simple node's content (without input widgets)
|
// check simple node's content (without input widgets)
|
||||||
const sumNode = locate.graphNodeByBinding(page, 'sum')
|
const sumNode = locate.graphNodeByBinding(page, 'sum')
|
||||||
|
@ -132,10 +132,9 @@ export function graphNodeIcon(node: Node) {
|
|||||||
|
|
||||||
// === Data locators ===
|
// === Data locators ===
|
||||||
|
|
||||||
type SanitizeClassName<T extends string> = T extends `${infer A}.${infer B}`
|
type SanitizeClassName<T extends string> =
|
||||||
? SanitizeClassName<`${A}${B}`>
|
T extends `${infer A}.${infer B}` ? SanitizeClassName<`${A}${B}`>
|
||||||
: T extends `${infer A} ${infer B}`
|
: T extends `${infer A} ${infer B}` ? SanitizeClassName<`${A}${B}`>
|
||||||
? SanitizeClassName<`${A}${B}`>
|
|
||||||
: T
|
: T
|
||||||
|
|
||||||
function componentLocator<T extends string>(className: SanitizeClassName<T>) {
|
function componentLocator<T extends string>(className: SanitizeClassName<T>) {
|
||||||
|
@ -3,7 +3,7 @@ import { createPinia } from 'pinia'
|
|||||||
import { initializeFFI } from 'shared/ast/ffi'
|
import { initializeFFI } from 'shared/ast/ffi'
|
||||||
import { createApp, ref } from 'vue'
|
import { createApp, ref } from 'vue'
|
||||||
import { mockDataHandler, mockLSHandler } from '../mock/engine'
|
import { mockDataHandler, mockLSHandler } from '../mock/engine'
|
||||||
import '../src/assets/base.css'
|
import '../src/assets/main.css'
|
||||||
import { provideGuiConfig } from '../src/providers/guiConfig'
|
import { provideGuiConfig } from '../src/providers/guiConfig'
|
||||||
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
||||||
import { Vec2 } from '../src/util/data/vec2'
|
import { Vec2 } from '../src/util/data/vec2'
|
||||||
|
@ -1,44 +1,44 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { test } from '@playwright/test'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
test('Selecting nodes by click', async ({ page }) => {
|
test('Selecting nodes by click', async ({ page }) => {
|
||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
const node1 = locate.graphNodeByBinding(page, 'five')
|
const node1 = locate.graphNodeByBinding(page, 'five')
|
||||||
const node2 = locate.graphNodeByBinding(page, 'ten')
|
const node2 = locate.graphNodeByBinding(page, 'ten')
|
||||||
await customExpect.not.toBeSelected(node1)
|
await expect(node1).not.toBeSelected()
|
||||||
await customExpect.not.toBeSelected(node2)
|
await expect(node2).not.toBeSelected()
|
||||||
|
|
||||||
await locate.graphNodeIcon(node1).click()
|
await locate.graphNodeIcon(node1).click()
|
||||||
await customExpect.toBeSelected(node1)
|
await expect(node1).toBeSelected()
|
||||||
await customExpect.not.toBeSelected(node2)
|
await expect(node2).not.toBeSelected()
|
||||||
|
|
||||||
await locate.graphNodeIcon(node2).click()
|
await locate.graphNodeIcon(node2).click()
|
||||||
await customExpect.not.toBeSelected(node1)
|
await expect(node1).not.toBeSelected()
|
||||||
await customExpect.toBeSelected(node2)
|
await expect(node2).toBeSelected()
|
||||||
|
|
||||||
await page.waitForTimeout(600) // Avoid double clicks
|
await page.waitForTimeout(300) // Avoid double clicks
|
||||||
await locate.graphNodeIcon(node1).click({ modifiers: ['Shift'] })
|
await locate.graphNodeIcon(node1).click({ modifiers: ['Shift'] })
|
||||||
await customExpect.toBeSelected(node1)
|
await expect(node1).toBeSelected()
|
||||||
await customExpect.toBeSelected(node2)
|
await expect(node2).toBeSelected()
|
||||||
|
|
||||||
await locate.graphNodeIcon(node2).click()
|
await locate.graphNodeIcon(node2).click()
|
||||||
await customExpect.not.toBeSelected(node1)
|
await expect(node1).not.toBeSelected()
|
||||||
await customExpect.toBeSelected(node2)
|
await expect(node2).toBeSelected()
|
||||||
|
|
||||||
await page.mouse.click(200, 200)
|
await page.mouse.click(600, 200)
|
||||||
await customExpect.not.toBeSelected(node1)
|
await expect(node1).not.toBeSelected()
|
||||||
await customExpect.not.toBeSelected(node2)
|
await expect(node2).not.toBeSelected()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Selecting nodes by area drag', async ({ page }) => {
|
test('Selecting nodes by area drag', async ({ page }) => {
|
||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
const node1 = locate.graphNodeByBinding(page, 'five')
|
const node1 = locate.graphNodeByBinding(page, 'five')
|
||||||
const node2 = locate.graphNodeByBinding(page, 'ten')
|
const node2 = locate.graphNodeByBinding(page, 'ten')
|
||||||
await customExpect.not.toBeSelected(node1)
|
await expect(node1).not.toBeSelected()
|
||||||
await customExpect.not.toBeSelected(node2)
|
await expect(node2).not.toBeSelected()
|
||||||
|
|
||||||
const node1BBox = await node1.locator('.selection').boundingBox()
|
const node1BBox = await node1.locator('.selection').boundingBox()
|
||||||
const node2BBox = await node2.boundingBox()
|
const node2BBox = await node2.boundingBox()
|
||||||
@ -49,9 +49,9 @@ test('Selecting nodes by area drag', async ({ page }) => {
|
|||||||
await page.mouse.move(node1BBox.x - 49, node1BBox.y - 49)
|
await page.mouse.move(node1BBox.x - 49, node1BBox.y - 49)
|
||||||
await expect(page.locator('.SelectionBrush')).toBeVisible()
|
await expect(page.locator('.SelectionBrush')).toBeVisible()
|
||||||
await page.mouse.move(node2BBox.x + node2BBox.width, node2BBox.y + node2BBox.height)
|
await page.mouse.move(node2BBox.x + node2BBox.width, node2BBox.y + node2BBox.height)
|
||||||
await customExpect.toBeSelected(node1)
|
await expect(node1).toBeSelected()
|
||||||
await customExpect.toBeSelected(node2)
|
await expect(node2).toBeSelected()
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
await customExpect.toBeSelected(node1)
|
await expect(node1).toBeSelected()
|
||||||
await customExpect.toBeSelected(node2)
|
await expect(node2).toBeSelected()
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { expect, test, type Page } from '@playwright/test'
|
import { test, type Page } from '@playwright/test'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
import * as customExpect from './customExpect'
|
import { expect } from './customExpect'
|
||||||
import { mockExpressionUpdate } from './expressionUpdates'
|
import { mockExpressionUpdate } from './expressionUpdates'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
import { graphNodeByBinding } from './locate'
|
import { graphNodeByBinding } from './locate'
|
||||||
@ -26,7 +26,7 @@ test('Load Table Visualisation', async ({ page }) => {
|
|||||||
await page.keyboard.press('Space')
|
await page.keyboard.press('Space')
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
const tableVisualization = locate.tableVisualization(page)
|
const tableVisualization = locate.tableVisualization(page)
|
||||||
await customExpect.toExist(tableVisualization)
|
await expect(tableVisualization).toExist()
|
||||||
await expect(tableVisualization).toContainText('10 rows.')
|
await expect(tableVisualization).toContainText('10 rows.')
|
||||||
await expect(tableVisualization).toContainText('0,0')
|
await expect(tableVisualization).toContainText('0,0')
|
||||||
await expect(tableVisualization).toContainText('1,0')
|
await expect(tableVisualization).toContainText('1,0')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import test, { expect, type Locator, type Page } from 'playwright/test'
|
import test, { type Locator, type Page } from 'playwright/test'
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
|
import { expect } from './customExpect'
|
||||||
import { mockMethodCallInfo } from './expressionUpdates'
|
import { mockMethodCallInfo } from './expressionUpdates'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ test('Widget in plain AST', async ({ page }) => {
|
|||||||
const numberNode = locate.graphNodeByBinding(page, 'five')
|
const numberNode = locate.graphNodeByBinding(page, 'five')
|
||||||
const numberWidget = numberNode.locator('.WidgetNumber')
|
const numberWidget = numberNode.locator('.WidgetNumber')
|
||||||
await expect(numberWidget).toBeVisible()
|
await expect(numberWidget).toBeVisible()
|
||||||
await expect(numberWidget.locator('.value')).toHaveValue('5')
|
await expect(numberWidget.locator('input')).toHaveValue('5')
|
||||||
|
|
||||||
const listNode = locate.graphNodeByBinding(page, 'list')
|
const listNode = locate.graphNodeByBinding(page, 'list')
|
||||||
const listWidget = listNode.locator('.WidgetVector')
|
const listWidget = listNode.locator('.WidgetVector')
|
||||||
@ -41,7 +42,7 @@ test('Widget in plain AST', async ({ page }) => {
|
|||||||
const textNode = locate.graphNodeByBinding(page, 'text')
|
const textNode = locate.graphNodeByBinding(page, 'text')
|
||||||
const textWidget = textNode.locator('.WidgetText')
|
const textWidget = textNode.locator('.WidgetText')
|
||||||
await expect(textWidget).toBeVisible()
|
await expect(textWidget).toBeVisible()
|
||||||
await expect(textWidget.locator('.value')).toHaveValue("'test'")
|
await expect(textWidget.locator('input')).toHaveValue('test')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Selection widgets in Data.read node', async ({ page }) => {
|
test('Selection widgets in Data.read node', async ({ page }) => {
|
||||||
@ -71,7 +72,7 @@ test('Selection widgets in Data.read node', async ({ page }) => {
|
|||||||
await onProblemsArg.click()
|
await onProblemsArg.click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['Ignore', 'Report_Warning', 'Report_Error'])
|
await dropDown.expectVisibleWithOptions(page, ['Ignore', 'Report_Warning', 'Report_Error'])
|
||||||
await dropDown.clickOption(page, 'Report_Error')
|
await dropDown.clickOption(page, 'Report_Error')
|
||||||
await expect(onProblemsArg.locator('.WidgetToken')).toHaveText([
|
await expect(onProblemsArg.locator('.WidgetToken')).toContainText([
|
||||||
'Problem_Behavior',
|
'Problem_Behavior',
|
||||||
'.',
|
'.',
|
||||||
'Report_Error',
|
'Report_Error',
|
||||||
@ -89,7 +90,7 @@ test('Selection widgets in Data.read node', async ({ page }) => {
|
|||||||
await page.getByText('Report_Error').click()
|
await page.getByText('Report_Error').click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['Ignore', 'Report_Warning', 'Report_Error'])
|
await dropDown.expectVisibleWithOptions(page, ['Ignore', 'Report_Warning', 'Report_Error'])
|
||||||
await dropDown.clickOption(page, 'Report_Warning')
|
await dropDown.clickOption(page, 'Report_Warning')
|
||||||
await expect(onProblemsArg.locator('.WidgetToken')).toHaveText([
|
await expect(onProblemsArg.locator('.WidgetToken')).toContainText([
|
||||||
'Problem_Behavior',
|
'Problem_Behavior',
|
||||||
'.',
|
'.',
|
||||||
'Report_Warning',
|
'Report_Warning',
|
||||||
@ -101,7 +102,7 @@ test('Selection widgets in Data.read node', async ({ page }) => {
|
|||||||
await expect(page.locator('.dropdownContainer')).toBeVisible()
|
await expect(page.locator('.dropdownContainer')).toBeVisible()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['"File 1"', '"File 2"'])
|
await dropDown.expectVisibleWithOptions(page, ['"File 1"', '"File 2"'])
|
||||||
await dropDown.clickOption(page, '"File 2"')
|
await dropDown.clickOption(page, '"File 2"')
|
||||||
await expect(pathArg.locator('.EnsoTextInputWidget > input')).toHaveValue('"File 2"')
|
await expect(pathArg.locator('.WidgetText > input')).toHaveValue('File 2')
|
||||||
|
|
||||||
// Change value on `path` (dynamic config)
|
// Change value on `path` (dynamic config)
|
||||||
await mockMethodCallInfo(page, 'data', {
|
await mockMethodCallInfo(page, 'data', {
|
||||||
@ -115,7 +116,7 @@ test('Selection widgets in Data.read node', async ({ page }) => {
|
|||||||
await page.getByText('path').click()
|
await page.getByText('path').click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['"File 1"', '"File 2"'])
|
await dropDown.expectVisibleWithOptions(page, ['"File 1"', '"File 2"'])
|
||||||
await dropDown.clickOption(page, '"File 1"')
|
await dropDown.clickOption(page, '"File 1"')
|
||||||
await expect(pathArg.locator('.EnsoTextInputWidget > input')).toHaveValue('"File 1"')
|
await expect(pathArg.locator('.WidgetText > input')).toHaveValue('File 1')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
||||||
@ -142,7 +143,11 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
|||||||
// Add first aggregate
|
// Add first aggregate
|
||||||
const columnsArg = argumentNames.filter({ has: page.getByText('columns') })
|
const columnsArg = argumentNames.filter({ has: page.getByText('columns') })
|
||||||
await columnsArg.locator('.add-item').click()
|
await columnsArg.locator('.add-item').click()
|
||||||
await expect(columnsArg.locator('.WidgetToken')).toHaveText(['Aggregate_Column', '.', 'Group_By'])
|
await expect(columnsArg.locator('.WidgetToken')).toContainText([
|
||||||
|
'Aggregate_Column',
|
||||||
|
'.',
|
||||||
|
'Group_By',
|
||||||
|
])
|
||||||
await mockMethodCallInfo(
|
await mockMethodCallInfo(
|
||||||
page,
|
page,
|
||||||
{
|
{
|
||||||
@ -164,7 +169,7 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
|||||||
await firstItem.click()
|
await firstItem.click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['Group_By', 'Count', 'Count_Distinct'])
|
await dropDown.expectVisibleWithOptions(page, ['Group_By', 'Count', 'Count_Distinct'])
|
||||||
await dropDown.clickOption(page, 'Count_Distinct')
|
await dropDown.clickOption(page, 'Count_Distinct')
|
||||||
await expect(columnsArg.locator('.WidgetToken')).toHaveText([
|
await expect(columnsArg.locator('.WidgetToken')).toContainText([
|
||||||
'Aggregate_Column',
|
'Aggregate_Column',
|
||||||
'.',
|
'.',
|
||||||
'Count_Distinct',
|
'Count_Distinct',
|
||||||
@ -190,16 +195,16 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
|||||||
await columnArg.click()
|
await columnArg.click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['"column 1"', '"column 2"'])
|
await dropDown.expectVisibleWithOptions(page, ['"column 1"', '"column 2"'])
|
||||||
await dropDown.clickOption(page, '"column 1"')
|
await dropDown.clickOption(page, '"column 1"')
|
||||||
await expect(columnsArg.locator('.WidgetToken')).toHaveText([
|
await expect(columnsArg.locator('.WidgetToken')).toContainText([
|
||||||
'Aggregate_Column',
|
'Aggregate_Column',
|
||||||
'.',
|
'.',
|
||||||
'Count_Distinct',
|
'Count_Distinct',
|
||||||
])
|
])
|
||||||
await expect(columnsArg.locator('.EnsoTextInputWidget > input').first()).toHaveValue('"column 1"')
|
await expect(columnsArg.locator('.WidgetText > input').first()).toHaveValue('column 1')
|
||||||
|
|
||||||
// Add another aggregate
|
// Add another aggregate
|
||||||
await columnsArg.locator('.add-item').click()
|
await columnsArg.locator('.add-item').click()
|
||||||
await expect(columnsArg.locator('.WidgetToken')).toHaveText([
|
await expect(columnsArg.locator('.WidgetToken')).toContainText([
|
||||||
'Aggregate_Column',
|
'Aggregate_Column',
|
||||||
'.',
|
'.',
|
||||||
'Count_Distinct',
|
'Count_Distinct',
|
||||||
@ -229,8 +234,12 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
|||||||
await secondColumnArg.click()
|
await secondColumnArg.click()
|
||||||
await dropDown.expectVisibleWithOptions(page, ['"column 1"', '"column 2"'])
|
await dropDown.expectVisibleWithOptions(page, ['"column 1"', '"column 2"'])
|
||||||
await dropDown.clickOption(page, '"column 2"')
|
await dropDown.clickOption(page, '"column 2"')
|
||||||
await expect(secondItem.locator('.WidgetToken')).toHaveText(['Aggregate_Column', '.', 'Group_By'])
|
await expect(secondItem.locator('.WidgetToken')).toContainText([
|
||||||
await expect(secondItem.locator('.EnsoTextInputWidget > input').first()).toHaveValue('"column 2"')
|
'Aggregate_Column',
|
||||||
|
'.',
|
||||||
|
'Group_By',
|
||||||
|
])
|
||||||
|
await expect(secondItem.locator('.WidgetText > input').first()).toHaveValue('column 2')
|
||||||
|
|
||||||
// Switch aggregates
|
// Switch aggregates
|
||||||
//TODO[ao] I have no idea how to emulate drag. Simple dragTo does not work (some element seem to capture event).
|
//TODO[ao] I have no idea how to emulate drag. Simple dragTo does not work (some element seem to capture event).
|
||||||
@ -244,7 +253,7 @@ test('Managing aggregates in `aggregate` node', async ({ page }) => {
|
|||||||
// await columnsArg.locator('.item > .handle').nth(0).hover({ force: true })
|
// await columnsArg.locator('.item > .handle').nth(0).hover({ force: true })
|
||||||
// await columnsArg.locator('.item > .handle').nth(0).hover()
|
// await columnsArg.locator('.item > .handle').nth(0).hover()
|
||||||
// await page.mouse.up()
|
// await page.mouse.up()
|
||||||
// await expect(columnsArg.locator('.WidgetToken')).toHaveText([
|
// await expect(columnsArg.locator('.WidgetToken')).toContainText([
|
||||||
// 'Aggregate_Column',
|
// 'Aggregate_Column',
|
||||||
// '.',
|
// '.',
|
||||||
// 'Group_By',
|
// 'Group_By',
|
||||||
|
@ -15,6 +15,7 @@ const conf = [
|
|||||||
'dist',
|
'dist',
|
||||||
'shared/ast/generated',
|
'shared/ast/generated',
|
||||||
'templates',
|
'templates',
|
||||||
|
'.histoire',
|
||||||
'playwright-report',
|
'playwright-report',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -43,13 +43,14 @@ export default defineConfig({
|
|||||||
order(a, b) {
|
order(a, b) {
|
||||||
const aIndex = order.indexOf(a)
|
const aIndex = order.indexOf(a)
|
||||||
const bIndex = order.indexOf(b)
|
const bIndex = order.indexOf(b)
|
||||||
return aIndex != null
|
return (
|
||||||
? bIndex != null
|
aIndex != null ?
|
||||||
? aIndex - bIndex
|
bIndex != null ?
|
||||||
|
aIndex - bIndex
|
||||||
: -1
|
: -1
|
||||||
: bIndex != null
|
: bIndex != null ? 1
|
||||||
? 1
|
|
||||||
: a.localeCompare(b)
|
: a.localeCompare(b)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
|
@ -323,15 +323,15 @@ function createId(id: Uuid) {
|
|||||||
function sendVizData(id: Uuid, config: VisualizationConfiguration) {
|
function sendVizData(id: Uuid, config: VisualizationConfiguration) {
|
||||||
const vizDataHandler =
|
const vizDataHandler =
|
||||||
mockVizData[
|
mockVizData[
|
||||||
typeof config.expression === 'string'
|
typeof config.expression === 'string' ?
|
||||||
? `${config.visualizationModule}.${config.expression}`
|
`${config.visualizationModule}.${config.expression}`
|
||||||
: `${config.expression.definedOnType}.${config.expression.name}`
|
: `${config.expression.definedOnType}.${config.expression.name}`
|
||||||
]
|
]
|
||||||
if (!vizDataHandler || !sendData) return
|
if (!vizDataHandler || !sendData) return
|
||||||
const vizData =
|
const vizData =
|
||||||
vizDataHandler instanceof Uint8Array
|
vizDataHandler instanceof Uint8Array ? vizDataHandler : (
|
||||||
? vizDataHandler
|
vizDataHandler(config.positionalArgumentsExpressions ?? [])
|
||||||
: vizDataHandler(config.positionalArgumentsExpressions ?? [])
|
)
|
||||||
const builder = new Builder()
|
const builder = new Builder()
|
||||||
const exprId = visualizationExprIds.get(id)
|
const exprId = visualizationExprIds.get(id)
|
||||||
const visualizationContextOffset = VisualizationContext.createVisualizationContext(
|
const visualizationContextOffset = VisualizationContext.createVisualizationContext(
|
||||||
|
@ -62,13 +62,13 @@
|
|||||||
"lib0": "^0.2.85",
|
"lib0": "^0.2.85",
|
||||||
"magic-string": "^0.30.3",
|
"magic-string": "^0.30.3",
|
||||||
"murmurhash": "^2.0.1",
|
"murmurhash": "^2.0.1",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.7",
|
||||||
"postcss-inline-svg": "^6.0.0",
|
"postcss-inline-svg": "^6.0.0",
|
||||||
"postcss-nesting": "^12.0.1",
|
"postcss-nesting": "^12.0.1",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"sucrase": "^3.34.0",
|
"sucrase": "^3.34.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.4.19",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
"y-codemirror.next": "^0.3.2",
|
"y-codemirror.next": "^0.3.2",
|
||||||
"y-protocols": "^1.0.5",
|
"y-protocols": "^1.0.5",
|
||||||
@ -79,9 +79,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@danmarshall/deckgl-typings": "^4.9.28",
|
"@danmarshall/deckgl-typings": "^4.9.28",
|
||||||
"@eslint/eslintrc": "^2.1.2",
|
"@eslint/eslintrc": "^3.0.2",
|
||||||
"@eslint/js": "^8.49.0",
|
"@eslint/js": "^8.57.0",
|
||||||
"@histoire/plugin-vue": "^0.17.1",
|
"@histoire/plugin-vue": "^0.17.12",
|
||||||
"@open-rpc/server-js": "^1.9.4",
|
"@open-rpc/server-js": "^1.9.4",
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
@ -92,45 +92,45 @@
|
|||||||
"@types/hash-sum": "^1.0.0",
|
"@types/hash-sum": "^1.0.0",
|
||||||
"@types/jsdom": "^21.1.1",
|
"@types/jsdom": "^21.1.1",
|
||||||
"@types/mapbox-gl": "^2.7.13",
|
"@types/mapbox-gl": "^2.7.13",
|
||||||
"@types/node": "^18.17.5",
|
"@types/node": "^20.11.21",
|
||||||
"@types/shuffle-seed": "^1.1.0",
|
"@types/shuffle-seed": "^1.1.0",
|
||||||
"@types/unbzip2-stream": "^1.4.3",
|
"@types/unbzip2-stream": "^1.4.3",
|
||||||
"@types/wicg-file-system-access": "^2023.10.2",
|
"@types/wicg-file-system-access": "^2023.10.2",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitest/coverage-v8": "^0.34.6",
|
"@vitest/coverage-v8": "^1.3.1",
|
||||||
"@volar/vue-typescript": "^1.6.5",
|
"@volar/vue-typescript": "^1.6.5",
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^2.4.4",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css.escape": "^1.5.1",
|
"css.escape": "^1.5.1",
|
||||||
"d3": "^7.4.0",
|
"d3": "^7.4.0",
|
||||||
"esbuild": "^0.19.3",
|
"esbuild": "^0.19.3",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-plugin-vue": "^9.16.1",
|
"eslint-plugin-vue": "^9.22.0",
|
||||||
"floating-vue": "^2.0.0-beta.24",
|
"floating-vue": "^2.0.0-beta.24",
|
||||||
"hash-wasm": "^4.10.0",
|
"hash-wasm": "^4.10.0",
|
||||||
"histoire": "^0.17.2",
|
"histoire": "^0.17.2",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"playwright": "^1.39.0",
|
"playwright": "^1.39.0",
|
||||||
"postcss-nesting": "^12.0.1",
|
"postcss-nesting": "^12.0.1",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.3",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"shuffle-seed": "^1.1.6",
|
"shuffle-seed": "^1.1.6",
|
||||||
"sql-formatter": "^13.0.0",
|
"sql-formatter": "^13.0.0",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"tar": "^6.2.0",
|
"tar": "^6.2.0",
|
||||||
"tsx": "^3.12.6",
|
"tsx": "^4.7.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"unbzip2-stream": "^1.4.3",
|
"unbzip2-stream": "^1.4.3",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.38",
|
||||||
"vitest": "^0.34.2",
|
"vitest": "^1.3.1",
|
||||||
"vue-react-wrapper": "^0.3.1",
|
"vue-react-wrapper": "^0.3.1",
|
||||||
"vue-tsc": "^1.8.8"
|
"vue-tsc": "^1.8.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,31 +49,31 @@ export default defineConfig({
|
|||||||
headless: !DEBUG,
|
headless: !DEBUG,
|
||||||
trace: 'retain-on-failure',
|
trace: 'retain-on-failure',
|
||||||
viewport: { width: 1920, height: 1600 },
|
viewport: { width: 1920, height: 1600 },
|
||||||
...(DEBUG
|
...(DEBUG ?
|
||||||
? {}
|
{}
|
||||||
: {
|
: {
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
ignoreDefaultArgs: ['--headless'],
|
ignoreDefaultArgs: ['--headless'],
|
||||||
args: [
|
args: [
|
||||||
// Much closer to headful Chromium than classic headless.
|
// Much closer to headful Chromium than classic headless.
|
||||||
'--headless=new',
|
'--headless=new',
|
||||||
// Required for `backdrop-filter: blur` to work.
|
// Required for `backdrop-filter: blur` to work.
|
||||||
'--use-angle=swiftshader',
|
'--use-angle=swiftshader',
|
||||||
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by
|
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by
|
||||||
// the software (CPU) compositor. This SHOULD be fixed eventually, but this flag
|
// the software (CPU) compositor. This SHOULD be fixed eventually, but this flag
|
||||||
// MUST stay as CI does not have a GPU.
|
// MUST stay as CI does not have a GPU.
|
||||||
'--disable-gpu',
|
'--disable-gpu',
|
||||||
// Fully disable GPU process.
|
// Fully disable GPU process.
|
||||||
'--disable-software-rasterizer',
|
'--disable-software-rasterizer',
|
||||||
// Disable text subpixel antialiasing.
|
// Disable text subpixel antialiasing.
|
||||||
'--font-render-hinting=none',
|
'--font-render-hinting=none',
|
||||||
'--disable-skia-runtime-opts',
|
'--disable-skia-runtime-opts',
|
||||||
'--disable-system-font-check',
|
'--disable-system-font-check',
|
||||||
'--disable-font-subpixel-positioning',
|
'--disable-font-subpixel-positioning',
|
||||||
'--disable-lcd-text',
|
'--disable-lcd-text',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
// projects: [
|
// projects: [
|
||||||
// {
|
// {
|
||||||
@ -112,9 +112,9 @@ export default defineConfig({
|
|||||||
E2E: 'true',
|
E2E: 'true',
|
||||||
},
|
},
|
||||||
command:
|
command:
|
||||||
process.env.CI || process.env.PROD
|
process.env.CI || process.env.PROD ?
|
||||||
? `npx vite build && npx vite preview --port ${PORT} --strictPort`
|
`npx vite build && npx vite preview --port ${PORT} --strictPort`
|
||||||
: `npx vite dev --port ${PORT}`,
|
: `npx vite dev --port ${PORT}`,
|
||||||
// Build from scratch apparently can take a while on CI machines.
|
// Build from scratch apparently can take a while on CI machines.
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
port: PORT,
|
port: PORT,
|
||||||
|
@ -40,9 +40,9 @@ function get(options, callback) {
|
|||||||
const location = response.headers.location
|
const location = response.headers.location
|
||||||
if (location) {
|
if (location) {
|
||||||
get(
|
get(
|
||||||
typeof options === 'string' || options instanceof URL
|
typeof options === 'string' || options instanceof URL ?
|
||||||
? location
|
location
|
||||||
: { ...options, ...new URL(location) },
|
: { ...options, ...new URL(location) },
|
||||||
callback,
|
callback,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -53,11 +53,13 @@ function get(options, callback) {
|
|||||||
|
|
||||||
/** @param {unknown} error */
|
/** @param {unknown} error */
|
||||||
function errorCode(error) {
|
function errorCode(error) {
|
||||||
return typeof error === 'object' &&
|
return (
|
||||||
error != null &&
|
typeof error === 'object' &&
|
||||||
'code' in error &&
|
error != null &&
|
||||||
typeof error.code === 'string'
|
'code' in error &&
|
||||||
? error.code
|
typeof error.code === 'string'
|
||||||
|
) ?
|
||||||
|
error.code
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { App, Ast, Group, MutableAst, OprApp, Wildcard } from './tree'
|
|||||||
|
|
||||||
export * from './mutableModule'
|
export * from './mutableModule'
|
||||||
export * from './parse'
|
export * from './parse'
|
||||||
|
export * from './text'
|
||||||
export * from './token'
|
export * from './token'
|
||||||
export * from './tree'
|
export * from './tree'
|
||||||
|
|
||||||
|
@ -204,8 +204,9 @@ class Abstractor {
|
|||||||
}
|
}
|
||||||
case RawAst.Tree.Type.OprApp: {
|
case RawAst.Tree.Type.OprApp: {
|
||||||
const lhs = tree.lhs ? this.abstractTree(tree.lhs) : undefined
|
const lhs = tree.lhs ? this.abstractTree(tree.lhs) : undefined
|
||||||
const opr = tree.opr.ok
|
const opr =
|
||||||
? [this.abstractToken(tree.opr.value)]
|
tree.opr.ok ?
|
||||||
|
[this.abstractToken(tree.opr.value)]
|
||||||
: Array.from(tree.opr.error.payload.operators, this.abstractToken.bind(this))
|
: Array.from(tree.opr.error.payload.operators, this.abstractToken.bind(this))
|
||||||
const rhs = tree.rhs ? this.abstractTree(tree.rhs) : undefined
|
const rhs = tree.rhs ? this.abstractTree(tree.rhs) : undefined
|
||||||
const soleOpr = tryGetSoleValue(opr)
|
const soleOpr = tryGetSoleValue(opr)
|
||||||
@ -831,9 +832,9 @@ function calculateCorrespondence(
|
|||||||
for (const partAfter of partsAfter) {
|
for (const partAfter of partsAfter) {
|
||||||
const astBefore = partAfterToAstBefore.get(sourceRangeKey(partAfter))!
|
const astBefore = partAfterToAstBefore.get(sourceRangeKey(partAfter))!
|
||||||
if (astBefore.typeName() === astAfter.typeName()) {
|
if (astBefore.typeName() === astAfter.typeName()) {
|
||||||
;(rangeLength(newSpans.get(astAfter.id)!) === rangeLength(partAfter)
|
;(rangeLength(newSpans.get(astAfter.id)!) === rangeLength(partAfter) ?
|
||||||
? toSync
|
toSync
|
||||||
: candidates
|
: candidates
|
||||||
).set(astBefore.id, astAfter)
|
).set(astBefore.id, astAfter)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -932,9 +933,9 @@ function syncTree(
|
|||||||
const editAst = edit.getVersion(ast)
|
const editAst = edit.getVersion(ast)
|
||||||
if (syncFieldsFrom) {
|
if (syncFieldsFrom) {
|
||||||
const originalAssignmentExpression =
|
const originalAssignmentExpression =
|
||||||
ast instanceof Assignment
|
ast instanceof Assignment ?
|
||||||
? metadataSource.get(ast.fields.get('expression').node)
|
metadataSource.get(ast.fields.get('expression').node)
|
||||||
: undefined
|
: undefined
|
||||||
syncFields(edit.getVersion(ast), syncFieldsFrom, childReplacerFor(ast.id))
|
syncFields(edit.getVersion(ast), syncFieldsFrom, childReplacerFor(ast.id))
|
||||||
if (editAst instanceof MutableAssignment && originalAssignmentExpression) {
|
if (editAst instanceof MutableAssignment && originalAssignmentExpression) {
|
||||||
if (editAst.expression.externalId !== originalAssignmentExpression.externalId)
|
if (editAst.expression.externalId !== originalAssignmentExpression.externalId)
|
||||||
|
@ -56,8 +56,9 @@ export class SourceDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (printed.code !== this.text_) {
|
if (printed.code !== this.text_) {
|
||||||
const textEdits = update.updateRoots.has(root.id)
|
const textEdits =
|
||||||
? [{ range: [0, this.text_.length] satisfies SourceRange, insert: printed.code }]
|
update.updateRoots.has(root.id) ?
|
||||||
|
[{ range: [0, this.text_.length] satisfies SourceRange, insert: printed.code }]
|
||||||
: subtreeTextEdits
|
: subtreeTextEdits
|
||||||
this.text_ = printed.code
|
this.text_ = printed.code
|
||||||
this.notifyObservers(textEdits, update.origin)
|
this.notifyObservers(textEdits, update.origin)
|
||||||
|
79
app/gui2/shared/ast/text.ts
Normal file
79
app/gui2/shared/ast/text.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/***
|
||||||
|
* String escaping and interpolation handling. Code in this module must be kept aligned with lexer's
|
||||||
|
* understanding of string literals. The relevant lexer code can be found in
|
||||||
|
* `lib/rust/parser/src/lexer.rs`, search for `fn text_escape`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assertUnreachable } from '../util/assert'
|
||||||
|
|
||||||
|
const escapeSequences = [
|
||||||
|
['0', '\0'],
|
||||||
|
['a', '\x07'],
|
||||||
|
['b', '\x08'],
|
||||||
|
['f', '\x0C'],
|
||||||
|
['n', '\x0A'],
|
||||||
|
['r', '\x0D'],
|
||||||
|
['t', '\x09'],
|
||||||
|
['v', '\x0B'],
|
||||||
|
['e', '\x1B'],
|
||||||
|
['\\', '\\'],
|
||||||
|
['"', '"'],
|
||||||
|
["'", "'"],
|
||||||
|
['`', '`'],
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function escapeAsCharCodes(str: string): string {
|
||||||
|
let out = ''
|
||||||
|
for (let i = 0; i < str.length; i += 1) out += `\\u{${str?.charCodeAt(i).toString(16)}}`
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapeRegex = new RegExp(
|
||||||
|
`${escapeSequences.map(([_, raw]) => escapeAsCharCodes(raw)).join('|')}`,
|
||||||
|
'gu',
|
||||||
|
)
|
||||||
|
|
||||||
|
const unescapeRegex = new RegExp(
|
||||||
|
'\\\\(?:' +
|
||||||
|
`${escapeSequences.map(([escape]) => escapeAsCharCodes(escape)).join('|')}` +
|
||||||
|
'|x[0-9a-fA-F]{0,2}' +
|
||||||
|
'|u\\{[0-9a-fA-F]{0,4}\\}?' + // Lexer allows trailing } to be missing.
|
||||||
|
'|u[0-9a-fA-F]{0,4}' +
|
||||||
|
'|U[0-9a-fA-F]{0,8}' +
|
||||||
|
')',
|
||||||
|
'gu',
|
||||||
|
)
|
||||||
|
|
||||||
|
const escapeMapping = Object.fromEntries(
|
||||||
|
escapeSequences.map(([escape, raw]) => [raw, `\\${escape}`]),
|
||||||
|
)
|
||||||
|
const unescapeMapping = Object.fromEntries(
|
||||||
|
escapeSequences.map(([escape, raw]) => [`\\${escape}`, raw]),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a string so it can be safely spliced into an interpolated (`''`) Enso string.
|
||||||
|
* Note: Escape sequences are NOT interpreted in raw (`""`) string literals.
|
||||||
|
* */
|
||||||
|
export function escapeTextLiteral(rawString: string) {
|
||||||
|
return rawString.replace(escapeRegex, (match) => escapeMapping[match] ?? assertUnreachable())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpret all escaped characters from an interpolated (`''`) Enso string.
|
||||||
|
* Note: Escape sequences are NOT interpreted in raw (`""`) string literals.
|
||||||
|
*/
|
||||||
|
export function unescapeTextLiteral(escapedString: string) {
|
||||||
|
return escapedString.replace(unescapeRegex, (match) => {
|
||||||
|
let cut = 2
|
||||||
|
switch (match[1]) {
|
||||||
|
case 'u':
|
||||||
|
if (match[2] === '{') cut = 3 // fallthrough
|
||||||
|
case 'U':
|
||||||
|
case 'x':
|
||||||
|
return String.fromCharCode(parseInt(match.substring(cut), 16))
|
||||||
|
default:
|
||||||
|
return unescapeMapping[match] ?? assertUnreachable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import type { DeepReadonly } from 'vue'
|
import type { DeepReadonly } from 'vue'
|
||||||
import type { AstId, Owned } from '.'
|
import type { AstId, NodeChild, Owned } from '.'
|
||||||
import { Ast, newExternalId } from '.'
|
import { Ast, newExternalId } from '.'
|
||||||
import { assert } from '../util/assert'
|
import { assert } from '../util/assert'
|
||||||
import type { ExternalId } from '../yjsModel'
|
import type { ExternalId } from '../yjsModel'
|
||||||
@ -11,6 +11,10 @@ export function isToken(t: unknown): t is Token {
|
|||||||
return t instanceof Token
|
return t instanceof Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTokenChild(child: NodeChild<unknown>): child is NodeChild<Token> {
|
||||||
|
return isToken(child.node)
|
||||||
|
}
|
||||||
|
|
||||||
declare const brandTokenId: unique symbol
|
declare const brandTokenId: unique symbol
|
||||||
export type TokenId = ExternalId & { [brandTokenId]: never }
|
export type TokenId = ExternalId & { [brandTokenId]: never }
|
||||||
|
|
||||||
|
@ -16,8 +16,10 @@ import {
|
|||||||
ROOT_ID,
|
ROOT_ID,
|
||||||
Token,
|
Token,
|
||||||
asOwned,
|
asOwned,
|
||||||
|
escapeTextLiteral,
|
||||||
isIdentifier,
|
isIdentifier,
|
||||||
isToken,
|
isToken,
|
||||||
|
isTokenChild,
|
||||||
isTokenId,
|
isTokenId,
|
||||||
newExternalId,
|
newExternalId,
|
||||||
parentId,
|
parentId,
|
||||||
@ -1194,24 +1196,6 @@ export interface MutableImport extends Import, MutableAst {
|
|||||||
}
|
}
|
||||||
applyMixins(MutableImport, [MutableAst])
|
applyMixins(MutableImport, [MutableAst])
|
||||||
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
'\b': '\\b',
|
|
||||||
'\f': '\\f',
|
|
||||||
'\n': '\\n',
|
|
||||||
'\r': '\\r',
|
|
||||||
'\t': '\\t',
|
|
||||||
'\v': '\\v',
|
|
||||||
'"': '\\"',
|
|
||||||
"'": "\\'",
|
|
||||||
'`': '``',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Escape a string so it can be safely spliced into an interpolated (`''`) Enso string.
|
|
||||||
* NOT USABLE to insert into raw strings. Does not include quotes. */
|
|
||||||
function escape(string: string) {
|
|
||||||
return string.replace(/[\0\b\f\n\r\t\v"'`]/g, (match) => mapping[match]!)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TreeRefs {
|
interface TreeRefs {
|
||||||
token: any
|
token: any
|
||||||
ast: any
|
ast: any
|
||||||
@ -1247,6 +1231,16 @@ function rawToConcrete(module: Module): RefMap<RawRefs, ConcreteRefs> {
|
|||||||
else return { ...child, node: module.get(child.node) }
|
else return { ...child, node: module.get(child.node) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function concreteToOwned(module: MutableModule): RefMap<ConcreteRefs, OwnedRefs> {
|
||||||
|
return (child: FieldData<ConcreteRefs>) => {
|
||||||
|
if (typeof child !== 'object') return
|
||||||
|
if (!('node' in child)) return
|
||||||
|
if (isTokenChild(child)) return child
|
||||||
|
else return { ...child, node: module.copy(child.node) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextToken<T extends TreeRefs = RawRefs> {
|
export interface TextToken<T extends TreeRefs = RawRefs> {
|
||||||
type: 'token'
|
type: 'token'
|
||||||
readonly token: T['token']
|
readonly token: T['token']
|
||||||
@ -1335,19 +1329,22 @@ export class TextLiteral extends Ast {
|
|||||||
return asOwned(new MutableTextLiteral(module, fields))
|
return asOwned(new MutableTextLiteral(module, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
static new(rawText: string, module: MutableModule): Owned<MutableTextLiteral> {
|
static new(rawText: string, module?: MutableModule): Owned<MutableTextLiteral> {
|
||||||
const escaped = escape(rawText)
|
const escaped = escapeTextLiteral(rawText)
|
||||||
const parsed = parse(`'${escaped}'`, module)
|
const parsed = parse(`'${escaped}'`, module)
|
||||||
if (!(parsed instanceof MutableTextLiteral)) {
|
if (!(parsed instanceof MutableTextLiteral)) {
|
||||||
console.error(`Failed to escape string for interpolated text`, rawText, escaped, parsed)
|
console.error(`Failed to escape string for interpolated text`, rawText, escaped, parsed)
|
||||||
const safeText = rawText.replaceAll(/[^-+A-Za-z0-9_. ]/, '')
|
const safeText = rawText.replaceAll(/[^-+A-Za-z0-9_. ]/g, '')
|
||||||
return this.new(safeText, module)
|
return this.new(safeText, module)
|
||||||
}
|
}
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the value of the string, interpreted except for any interpolated expressions. */
|
/**
|
||||||
contentUninterpolated(): string {
|
* Return the literal value of the string with all escape sequences applied, but without
|
||||||
|
* evaluating any interpolated expressions.
|
||||||
|
*/
|
||||||
|
get rawTextContent(): string {
|
||||||
return uninterpolatedText(this.fields.get('elements'), this.module)
|
return uninterpolatedText(this.fields.get('elements'), this.module)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1358,10 +1355,63 @@ export class TextLiteral extends Ast {
|
|||||||
for (const e of elements) yield* fieldConcreteChildren(e)
|
for (const e of elements) yield* fieldConcreteChildren(e)
|
||||||
if (close) yield close
|
if (close) yield close
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundaryTokenCode(): string | undefined {
|
||||||
|
return (this.open || this.close)?.code()
|
||||||
|
}
|
||||||
|
|
||||||
|
isInterpolated(): boolean {
|
||||||
|
const token = this.boundaryTokenCode()
|
||||||
|
return token === "'" || token === "'''"
|
||||||
|
}
|
||||||
|
|
||||||
|
get open(): Token | undefined {
|
||||||
|
return this.module.getToken(this.fields.get('open')?.node)
|
||||||
|
}
|
||||||
|
|
||||||
|
get close(): Token | undefined {
|
||||||
|
return this.module.getToken(this.fields.get('close')?.node)
|
||||||
|
}
|
||||||
|
|
||||||
|
get elements(): TextElement<ConcreteRefs>[] {
|
||||||
|
return this.fields.get('elements').map((e) => mapRefs(e, rawToConcrete(this.module)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export class MutableTextLiteral extends TextLiteral implements MutableAst {
|
export class MutableTextLiteral extends TextLiteral implements MutableAst {
|
||||||
declare readonly module: MutableModule
|
declare readonly module: MutableModule
|
||||||
declare readonly fields: FixedMap<AstFields & TextLiteralFields>
|
declare readonly fields: FixedMap<AstFields & TextLiteralFields>
|
||||||
|
|
||||||
|
setBoundaries(code: string) {
|
||||||
|
this.fields.set('open', unspaced(Token.new(code)))
|
||||||
|
this.fields.set('close', unspaced(Token.new(code)))
|
||||||
|
}
|
||||||
|
|
||||||
|
setElements(elements: TextElement<OwnedRefs>[]) {
|
||||||
|
this.fields.set(
|
||||||
|
'elements',
|
||||||
|
elements.map((e) => mapRefs(e, ownedToRaw(this.module, this.id))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set literal value of the string. The code representation of assigned text will be automatically
|
||||||
|
* transformed to use escape sequences when necessary.
|
||||||
|
*/
|
||||||
|
setRawTextContent(rawText: string) {
|
||||||
|
let boundary = this.boundaryTokenCode()
|
||||||
|
const isInterpolated = this.isInterpolated()
|
||||||
|
const mustBecomeInterpolated = !isInterpolated && (!boundary || rawText.includes(boundary))
|
||||||
|
if (mustBecomeInterpolated) {
|
||||||
|
boundary = "'"
|
||||||
|
this.setBoundaries(boundary)
|
||||||
|
}
|
||||||
|
const literalContents =
|
||||||
|
isInterpolated || mustBecomeInterpolated ? escapeTextLiteral(rawText) : rawText
|
||||||
|
const parsed = parse(`${boundary}${literalContents}${boundary}`)
|
||||||
|
assert(parsed instanceof TextLiteral)
|
||||||
|
const elements = parsed.elements.map((e) => mapRefs(e, concreteToOwned(this.module)))
|
||||||
|
this.setElements(elements)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export interface MutableTextLiteral extends TextLiteral, MutableAst {}
|
export interface MutableTextLiteral extends TextLiteral, MutableAst {}
|
||||||
applyMixins(MutableTextLiteral, [MutableAst])
|
applyMixins(MutableTextLiteral, [MutableAst])
|
||||||
@ -1953,8 +2003,9 @@ function lineFromRaw(raw: RawBlockLine, module: Module): BlockLine {
|
|||||||
const expression = raw.expression ? module.get(raw.expression.node) : undefined
|
const expression = raw.expression ? module.get(raw.expression.node) : undefined
|
||||||
return {
|
return {
|
||||||
newline: { ...raw.newline, node: module.getToken(raw.newline.node) },
|
newline: { ...raw.newline, node: module.getToken(raw.newline.node) },
|
||||||
expression: expression
|
expression:
|
||||||
? {
|
expression ?
|
||||||
|
{
|
||||||
whitespace: raw.expression?.whitespace,
|
whitespace: raw.expression?.whitespace,
|
||||||
node: expression,
|
node: expression,
|
||||||
}
|
}
|
||||||
@ -1966,8 +2017,9 @@ function ownedLineFromRaw(raw: RawBlockLine, module: MutableModule): OwnedBlockL
|
|||||||
const expression = raw.expression ? module.get(raw.expression.node).takeIfParented() : undefined
|
const expression = raw.expression ? module.get(raw.expression.node).takeIfParented() : undefined
|
||||||
return {
|
return {
|
||||||
newline: { ...raw.newline, node: module.getToken(raw.newline.node) },
|
newline: { ...raw.newline, node: module.getToken(raw.newline.node) },
|
||||||
expression: expression
|
expression:
|
||||||
? {
|
expression ?
|
||||||
|
{
|
||||||
whitespace: raw.expression?.whitespace,
|
whitespace: raw.expression?.whitespace,
|
||||||
node: expression,
|
node: expression,
|
||||||
}
|
}
|
||||||
@ -1978,8 +2030,9 @@ function ownedLineFromRaw(raw: RawBlockLine, module: MutableModule): OwnedBlockL
|
|||||||
function lineToRaw(line: OwnedBlockLine, module: MutableModule, block: AstId): RawBlockLine {
|
function lineToRaw(line: OwnedBlockLine, module: MutableModule, block: AstId): RawBlockLine {
|
||||||
return {
|
return {
|
||||||
newline: line.newline ?? unspaced(Token.new('\n', RawAst.Token.Type.Newline)),
|
newline: line.newline ?? unspaced(Token.new('\n', RawAst.Token.Type.Newline)),
|
||||||
expression: line.expression
|
expression:
|
||||||
? {
|
line.expression ?
|
||||||
|
{
|
||||||
whitespace: line.expression?.whitespace,
|
whitespace: line.expression?.whitespace,
|
||||||
node: claimChild(module, line.expression.node, block),
|
node: claimChild(module, line.expression.node, block),
|
||||||
}
|
}
|
||||||
@ -2084,40 +2137,24 @@ export class MutableWildcard extends Wildcard implements MutableAst {
|
|||||||
export interface MutableWildcard extends Wildcard, MutableAst {}
|
export interface MutableWildcard extends Wildcard, MutableAst {}
|
||||||
applyMixins(MutableWildcard, [MutableAst])
|
applyMixins(MutableWildcard, [MutableAst])
|
||||||
|
|
||||||
export type Mutable<T extends Ast = Ast> = T extends App
|
export type Mutable<T extends Ast = Ast> =
|
||||||
? MutableApp
|
T extends App ? MutableApp
|
||||||
: T extends Assignment
|
: T extends Assignment ? MutableAssignment
|
||||||
? MutableAssignment
|
: T extends BodyBlock ? MutableBodyBlock
|
||||||
: T extends BodyBlock
|
: T extends Documented ? MutableDocumented
|
||||||
? MutableBodyBlock
|
: T extends Function ? MutableFunction
|
||||||
: T extends Documented
|
: T extends Generic ? MutableGeneric
|
||||||
? MutableDocumented
|
: T extends Group ? MutableGroup
|
||||||
: T extends Function
|
: T extends Ident ? MutableIdent
|
||||||
? MutableFunction
|
: T extends Import ? MutableImport
|
||||||
: T extends Generic
|
: T extends Invalid ? MutableInvalid
|
||||||
? MutableGeneric
|
: T extends NegationApp ? MutableNegationApp
|
||||||
: T extends Group
|
: T extends NumericLiteral ? MutableNumericLiteral
|
||||||
? MutableGroup
|
: T extends OprApp ? MutableOprApp
|
||||||
: T extends Ident
|
: T extends PropertyAccess ? MutablePropertyAccess
|
||||||
? MutableIdent
|
: T extends TextLiteral ? MutableTextLiteral
|
||||||
: T extends Import
|
: T extends UnaryOprApp ? MutableUnaryOprApp
|
||||||
? MutableImport
|
: T extends Wildcard ? MutableWildcard
|
||||||
: T extends Invalid
|
|
||||||
? MutableInvalid
|
|
||||||
: T extends NegationApp
|
|
||||||
? MutableNegationApp
|
|
||||||
: T extends NumericLiteral
|
|
||||||
? MutableNumericLiteral
|
|
||||||
: T extends OprApp
|
|
||||||
? MutableOprApp
|
|
||||||
: T extends PropertyAccess
|
|
||||||
? MutablePropertyAccess
|
|
||||||
: T extends TextLiteral
|
|
||||||
? MutableTextLiteral
|
|
||||||
: T extends UnaryOprApp
|
|
||||||
? MutableUnaryOprApp
|
|
||||||
: T extends Wildcard
|
|
||||||
? MutableWildcard
|
|
||||||
: MutableAst
|
: MutableAst
|
||||||
|
|
||||||
export function materializeMutable(module: MutableModule, fields: FixedMap<AstFields>): MutableAst {
|
export function materializeMutable(module: MutableModule, fields: FixedMap<AstFields>): MutableAst {
|
||||||
@ -2296,15 +2333,27 @@ function concreteChild(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StrictIdentLike = Identifier | IdentifierToken
|
type StrictIdentLike = Identifier | IdentifierToken
|
||||||
function toIdentStrict(ident: StrictIdentLike): IdentifierToken {
|
function toIdentStrict(ident: StrictIdentLike): IdentifierToken
|
||||||
return isToken(ident) ? ident : (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierToken)
|
function toIdentStrict(ident: StrictIdentLike | undefined): IdentifierToken | undefined
|
||||||
|
function toIdentStrict(ident: StrictIdentLike | undefined): IdentifierToken | undefined {
|
||||||
|
return (
|
||||||
|
ident ?
|
||||||
|
isToken(ident) ? ident
|
||||||
|
: (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierToken)
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentLike = IdentifierOrOperatorIdentifier | IdentifierOrOperatorIdentifierToken
|
type IdentLike = IdentifierOrOperatorIdentifier | IdentifierOrOperatorIdentifierToken
|
||||||
function toIdent(ident: IdentLike): IdentifierOrOperatorIdentifierToken {
|
function toIdent(ident: IdentLike): IdentifierOrOperatorIdentifierToken
|
||||||
return isToken(ident)
|
function toIdent(ident: IdentLike | undefined): IdentifierOrOperatorIdentifierToken | undefined
|
||||||
? ident
|
function toIdent(ident: IdentLike | undefined): IdentifierOrOperatorIdentifierToken | undefined {
|
||||||
: (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierOrOperatorIdentifierToken)
|
return (
|
||||||
|
ident ?
|
||||||
|
isToken(ident) ? ident
|
||||||
|
: (Token.new(ident, RawAst.Token.Type.Ident) as IdentifierOrOperatorIdentifierToken)
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeEquals(): Token {
|
function makeEquals(): Token {
|
||||||
|
@ -668,10 +668,9 @@ export class ByteBuffer {
|
|||||||
offset<T extends OffsetConstraint>(bbPos: number, vtableOffset: number): Offset<T> {
|
offset<T extends OffsetConstraint>(bbPos: number, vtableOffset: number): Offset<T> {
|
||||||
const vtable = bbPos - this.view.getInt32(bbPos, true)
|
const vtable = bbPos - this.view.getInt32(bbPos, true)
|
||||||
return (
|
return (
|
||||||
vtableOffset < this.view.getInt16(vtable, true)
|
vtableOffset < this.view.getInt16(vtable, true) ?
|
||||||
? this.view.getInt16(vtable + vtableOffset, true)
|
this.view.getInt16(vtable + vtableOffset, true)
|
||||||
: 0
|
: 0) as Offset<T>
|
||||||
) as Offset<T>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
union(t: Table, offset: number): Table {
|
union(t: Table, offset: number): Table {
|
||||||
@ -1311,8 +1310,8 @@ export class VisualizationUpdate implements Table {
|
|||||||
|
|
||||||
visualizationContext(obj?: VisualizationContext): VisualizationContext | null {
|
visualizationContext(obj?: VisualizationContext): VisualizationContext | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new VisualizationContext()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new VisualizationContext()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1328,8 +1327,8 @@ export class VisualizationUpdate implements Table {
|
|||||||
|
|
||||||
dataArray(): Uint8Array | null {
|
dataArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 6)
|
const offset = this.bb.offset(this.bbPos, 6)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
@ -1414,8 +1413,8 @@ export class Path implements Table {
|
|||||||
|
|
||||||
rawSegments(index: number): ArrayBuffer {
|
rawSegments(index: number): ArrayBuffer {
|
||||||
const offset = this.bb.offset(this.bbPos, 6)
|
const offset = this.bb.offset(this.bbPos, 6)
|
||||||
return offset
|
return offset ?
|
||||||
? this.bb.rawMessage(this.bb.vector(this.bbPos + offset) + index * 4)
|
this.bb.rawMessage(this.bb.vector(this.bbPos + offset) + index * 4)
|
||||||
: new Uint8Array()
|
: new Uint8Array()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,8 +1516,8 @@ export class WriteFileCommand implements Table {
|
|||||||
|
|
||||||
contentsArray(): Uint8Array | null {
|
contentsArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 6)
|
const offset = this.bb.offset(this.bbPos, 6)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
@ -1662,8 +1661,8 @@ export class FileContentsReply implements Table {
|
|||||||
|
|
||||||
contentsArray(): Uint8Array | null {
|
contentsArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
@ -1761,8 +1760,8 @@ export class WriteBytesCommand implements Table {
|
|||||||
|
|
||||||
bytesArray(): Uint8Array | null {
|
bytesArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 10)
|
const offset = this.bb.offset(this.bbPos, 10)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
@ -1855,8 +1854,8 @@ export class WriteBytesReply implements Table {
|
|||||||
|
|
||||||
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1913,8 +1912,8 @@ export class ReadBytesCommand implements Table {
|
|||||||
|
|
||||||
segment(obj?: FileSegment): FileSegment | null {
|
segment(obj?: FileSegment): FileSegment | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new FileSegment()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new FileSegment()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1962,8 +1961,8 @@ export class ReadBytesReply implements Table {
|
|||||||
|
|
||||||
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1979,8 +1978,8 @@ export class ReadBytesReply implements Table {
|
|||||||
|
|
||||||
bytesArray(): Uint8Array | null {
|
bytesArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 6)
|
const offset = this.bb.offset(this.bbPos, 6)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
@ -2064,8 +2063,8 @@ export class ChecksumBytesCommand implements Table {
|
|||||||
|
|
||||||
segment(obj?: FileSegment): FileSegment | null {
|
segment(obj?: FileSegment): FileSegment | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new FileSegment()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new FileSegment()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2122,8 +2121,8 @@ export class ChecksumBytesReply implements Table {
|
|||||||
|
|
||||||
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
checksum(obj?: EnsoDigest): EnsoDigest | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? (obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
(obj ?? new EnsoDigest()).init(this.bb.indirect(this.bbPos + offset), this.bb!)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2181,8 +2180,8 @@ export class EnsoDigest implements Table {
|
|||||||
|
|
||||||
bytesArray(): Uint8Array | null {
|
bytesArray(): Uint8Array | null {
|
||||||
const offset = this.bb.offset(this.bbPos, 4)
|
const offset = this.bb.offset(this.bbPos, 4)
|
||||||
return offset
|
return offset ?
|
||||||
? new Uint8Array(
|
new Uint8Array(
|
||||||
this.bb.view.buffer,
|
this.bb.view.buffer,
|
||||||
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
this.bb.view.byteOffset + this.bb.vector(this.bbPos + offset),
|
||||||
this.bb.vectorLength(this.bbPos + offset),
|
this.bb.vectorLength(this.bbPos + offset),
|
||||||
|
@ -24,9 +24,9 @@ export function assertLength<T>(iterable: Iterable<T>, length: number, message?:
|
|||||||
const convertedArray = Array.from(iterable)
|
const convertedArray = Array.from(iterable)
|
||||||
const messagePrefix = message ? message + ' ' : ''
|
const messagePrefix = message ? message + ' ' : ''
|
||||||
const elementRepresentation =
|
const elementRepresentation =
|
||||||
convertedArray.length > 5
|
convertedArray.length > 5 ?
|
||||||
? `${convertedArray.slice(0, 5).join(', ')},...`
|
`${convertedArray.slice(0, 5).join(', ')},...`
|
||||||
: convertedArray.join(', ')
|
: convertedArray.join(', ')
|
||||||
assert(
|
assert(
|
||||||
convertedArray.length === length,
|
convertedArray.length === length,
|
||||||
`${messagePrefix}Expected iterable of length ${length}, got length ${convertedArray.length}. Elements: [${elementRepresentation}]`,
|
`${messagePrefix}Expected iterable of length ${length}, got length ${convertedArray.length}. Elements: [${elementRepresentation}]`,
|
||||||
|
@ -145,8 +145,9 @@ export class WebsocketClient extends ObservableV2<WebsocketEvents> {
|
|||||||
this.lastMessageReceived = 0
|
this.lastMessageReceived = 0
|
||||||
/** Whether to connect to other peers or not */
|
/** Whether to connect to other peers or not */
|
||||||
this.shouldConnect = false
|
this.shouldConnect = false
|
||||||
this._checkInterval = this.sendPings
|
this._checkInterval =
|
||||||
? setInterval(() => {
|
this.sendPings ?
|
||||||
|
setInterval(() => {
|
||||||
if (
|
if (
|
||||||
this.connected &&
|
this.connected &&
|
||||||
messageReconnectTimeout < time.getUnixTime() - this.lastMessageReceived
|
messageReconnectTimeout < time.getUnixTime() - this.lastMessageReceived
|
||||||
|
@ -8,6 +8,7 @@ import ProjectView from '@/views/ProjectView.vue'
|
|||||||
import { isDevMode } from 'shared/util/detect'
|
import { isDevMode } from 'shared/util/detect'
|
||||||
import { computed, onMounted, onUnmounted, toRaw } from 'vue'
|
import { computed, onMounted, onUnmounted, toRaw } from 'vue'
|
||||||
import { useProjectStore } from './stores/project'
|
import { useProjectStore } from './stores/project'
|
||||||
|
import { registerAutoBlurHandler } from './util/autoBlur'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
config: ApplicationConfig
|
config: ApplicationConfig
|
||||||
@ -20,6 +21,8 @@ const classSet = provideAppClassSet()
|
|||||||
|
|
||||||
provideGuiConfig(computed((): ApplicationConfigValue => configValue(props.config)))
|
provideGuiConfig(computed((): ApplicationConfigValue => configValue(props.config)))
|
||||||
|
|
||||||
|
registerAutoBlurHandler()
|
||||||
|
|
||||||
// Initialize suggestion db immediately, so it will be ready when user needs it.
|
// Initialize suggestion db immediately, so it will be ready when user needs it.
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const suggestionDb = useSuggestionDbStore()
|
const suggestionDb = useSuggestionDbStore()
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
--color-frame-bg: rgb(255 255 255 / 0.3);
|
--color-frame-bg: rgb(255 255 255 / 0.3);
|
||||||
--color-frame-selected-bg: rgb(255 255 255 / 0.7);
|
--color-frame-selected-bg: rgb(255 255 255 / 0.7);
|
||||||
--color-widget: rgb(255 255 255 / 0.12);
|
--color-widget: rgb(255 255 255 / 0.12);
|
||||||
|
--color-widget-focus: rgb(255 255 255 / 0.25);
|
||||||
--color-widget-selected: rgb(255 255 255 / 0.58);
|
--color-widget-selected: rgb(255 255 255 / 0.58);
|
||||||
--color-port-connected: rgb(255 255 255 / 0.15);
|
--color-port-connected: rgb(255 255 255 / 0.15);
|
||||||
|
|
||||||
|
@ -60,9 +60,9 @@ const emit = defineEmits<{
|
|||||||
class="icon-container button slot7"
|
class="icon-container button slot7"
|
||||||
:class="{ 'output-context-overridden': props.isOutputContextOverridden }"
|
:class="{ 'output-context-overridden': props.isOutputContextOverridden }"
|
||||||
:alt="`${
|
:alt="`${
|
||||||
props.isOutputContextEnabledGlobally != props.isOutputContextOverridden
|
props.isOutputContextEnabledGlobally != props.isOutputContextOverridden ?
|
||||||
? 'Disable'
|
'Disable'
|
||||||
: 'Enable'
|
: 'Enable'
|
||||||
} output context`"
|
} output context`"
|
||||||
:modelValue="props.isOutputContextOverridden"
|
:modelValue="props.isOutputContextOverridden"
|
||||||
@update:modelValue="emit('update:isOutputContextOverridden', $event)"
|
@update:modelValue="emit('update:isOutputContextOverridden', $event)"
|
||||||
@ -78,6 +78,11 @@ const emit = defineEmits<{
|
|||||||
top: -36px;
|
top: -36px;
|
||||||
width: 114px;
|
width: 114px;
|
||||||
height: 114px;
|
height: 114px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -86,6 +91,7 @@ const emit = defineEmits<{
|
|||||||
background: var(--color-app-bg);
|
background: var(--color-app-bg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.partial {
|
&.partial {
|
||||||
|
@ -227,9 +227,8 @@ function updateListener() {
|
|||||||
commitPendingChanges()
|
commitPendingChanges()
|
||||||
currentModule = newModule
|
currentModule = newModule
|
||||||
} else if (transaction.docChanged && currentModule) {
|
} else if (transaction.docChanged && currentModule) {
|
||||||
pendingChanges = pendingChanges
|
pendingChanges =
|
||||||
? pendingChanges.compose(transaction.changes)
|
pendingChanges ? pendingChanges.compose(transaction.changes) : transaction.changes
|
||||||
: transaction.changes
|
|
||||||
// Defer the update until after pending events have been processed, so that if changes are arriving faster than
|
// Defer the update until after pending events have been processed, so that if changes are arriving faster than
|
||||||
// we would be able to apply them individually we coalesce them to keep up.
|
// we would be able to apply them individually we coalesce them to keep up.
|
||||||
debouncer(commitPendingChanges)
|
debouncer(commitPendingChanges)
|
||||||
@ -282,8 +281,9 @@ function observeSourceChange(textEdits: SourceRangeEdit[], origin: Origin | unde
|
|||||||
// too quickly can result in incorrect ranges, but at idle it should correct itself when we receive new diagnostics.
|
// too quickly can result in incorrect ranges, but at idle it should correct itself when we receive new diagnostics.
|
||||||
watch([viewInitialized, () => projectStore.diagnostics], ([ready, diagnostics]) => {
|
watch([viewInitialized, () => projectStore.diagnostics], ([ready, diagnostics]) => {
|
||||||
if (!ready) return
|
if (!ready) return
|
||||||
executionContextDiagnostics.value = graphStore.moduleSource.text
|
executionContextDiagnostics.value =
|
||||||
? lsDiagnosticsToCMDiagnostics(graphStore.moduleSource.text, diagnostics)
|
graphStore.moduleSource.text ?
|
||||||
|
lsDiagnosticsToCMDiagnostics(graphStore.moduleSource.text, diagnostics)
|
||||||
: []
|
: []
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -403,7 +403,7 @@ const editorStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeEditor :is(.cm-editor) {
|
.CodeEditor :deep(.cm-editor) {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: white;
|
color: white;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -420,11 +420,11 @@ const editorStyle = computed(() => {
|
|||||||
transition: outline 0.1s ease-in-out;
|
transition: outline 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeEditor :is(.cm-focused) {
|
.CodeEditor :deep(.cm-focused) {
|
||||||
outline: 1px solid rgba(0, 0, 0, 0.5);
|
outline: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeEditor :is(.cm-tooltip-hover) {
|
.CodeEditor :deep(.cm-tooltip-hover) {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||||
@ -438,7 +438,7 @@ const editorStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeEditor :is(.cm-gutters) {
|
.CodeEditor :deep(.cm-gutters) {
|
||||||
border-radius: 3px 0 0 3px;
|
border-radius: 3px 0 0 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -69,7 +69,9 @@ export function lsDiagnosticsToCMDiagnostics(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const severity =
|
const severity =
|
||||||
diagnostic.kind === 'Error' ? 'error' : diagnostic.kind === 'Warning' ? 'warning' : 'info'
|
diagnostic.kind === 'Error' ? 'error'
|
||||||
|
: diagnostic.kind === 'Warning' ? 'warning'
|
||||||
|
: 'info'
|
||||||
results.push({ from, to, message: diagnostic.message, severity })
|
results.push({ from, to, message: diagnostic.message, severity })
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
|
@ -54,14 +54,9 @@ export function labelOfEntry(
|
|||||||
matchedAlias: match.matchedAlias,
|
matchedAlias: match.matchedAlias,
|
||||||
matchedRanges: [
|
matchedRanges: [
|
||||||
...(match.memberOfRanges ?? match.definedInRanges ?? []).flatMap((range) =>
|
...(match.memberOfRanges ?? match.definedInRanges ?? []).flatMap((range) =>
|
||||||
range.end <= lastSegmentStart
|
range.end <= lastSegmentStart ?
|
||||||
? []
|
[]
|
||||||
: [
|
: [new Range(Math.max(0, range.start - lastSegmentStart), range.end - lastSegmentStart)],
|
||||||
new Range(
|
|
||||||
Math.max(0, range.start - lastSegmentStart),
|
|
||||||
range.end - lastSegmentStart,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
...(match.nameRanges ?? []).map(
|
...(match.nameRanges ?? []).map(
|
||||||
(range) => new Range(range.start + nameOffset, range.end + nameOffset),
|
(range) => new Range(range.start + nameOffset, range.end + nameOffset),
|
||||||
@ -69,16 +64,15 @@ export function labelOfEntry(
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
return match.nameRanges
|
return match.nameRanges ?
|
||||||
? { label: entry.name, matchedAlias: match.matchedAlias, matchedRanges: match.nameRanges }
|
{ label: entry.name, matchedAlias: match.matchedAlias, matchedRanges: match.nameRanges }
|
||||||
: { label: entry.name, matchedAlias: match.matchedAlias }
|
: { label: entry.name, matchedAlias: match.matchedAlias }
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLabel(labelInfo: ComponentLabelInfo): ComponentLabel {
|
function formatLabel(labelInfo: ComponentLabelInfo): ComponentLabel {
|
||||||
return {
|
return {
|
||||||
label: labelInfo.matchedAlias
|
label:
|
||||||
? `${labelInfo.matchedAlias} (${labelInfo.label})`
|
labelInfo.matchedAlias ? `${labelInfo.matchedAlias} (${labelInfo.label})` : labelInfo.label,
|
||||||
: labelInfo.label,
|
|
||||||
matchedRanges: labelInfo.matchedRanges,
|
matchedRanges: labelInfo.matchedRanges,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ class FilteringWithPattern {
|
|||||||
// - The unmatched part up to the next matched letter
|
// - The unmatched part up to the next matched letter
|
||||||
const regex = pattern
|
const regex = pattern
|
||||||
.split('')
|
.split('')
|
||||||
.map((c) => `(${c})`)
|
.map((c) => `(${escapeStringRegexp(c)})`)
|
||||||
.join('([^_]*?[_ ])')
|
.join('([^_]*?[_ ])')
|
||||||
this.initialsMatchRegex = new RegExp('(^|.*?_)' + regex + '(.*)', 'i')
|
this.initialsMatchRegex = new RegExp('(^|.*?_)' + regex + '(.*)', 'i')
|
||||||
}
|
}
|
||||||
@ -307,9 +307,9 @@ export class Filtering {
|
|||||||
for (const [, text, separator] of this.fullPattern.matchAll(/(.+?)([._]|$)/g)) {
|
for (const [, text, separator] of this.fullPattern.matchAll(/(.+?)([._]|$)/g)) {
|
||||||
const escaped = escapeStringRegexp(text ?? '')
|
const escaped = escapeStringRegexp(text ?? '')
|
||||||
const segment =
|
const segment =
|
||||||
separator === '_'
|
separator === '_' ?
|
||||||
? `()(${escaped})([^_.]*)(_)`
|
`()(${escaped})([^_.]*)(_)`
|
||||||
: `([^.]*_)?(${escaped})([^.]*)(${separator === '.' ? '\\.' : ''})`
|
: `([^.]*_)?(${escaped})([^.]*)(${separator === '.' ? '\\.' : ''})`
|
||||||
prefix = '(?:' + prefix
|
prefix = '(?:' + prefix
|
||||||
suffix += segment + ')?'
|
suffix += segment + ')?'
|
||||||
}
|
}
|
||||||
|
@ -366,9 +366,9 @@ export function useComponentBrowserInput(
|
|||||||
const ctx = context.value
|
const ctx = context.value
|
||||||
const opr = ctx.type !== 'changeLiteral' && ctx.oprApp != null ? ctx.oprApp.lastOpr() : null
|
const opr = ctx.type !== 'changeLiteral' && ctx.oprApp != null ? ctx.oprApp.lastOpr() : null
|
||||||
const oprAppSpacing =
|
const oprAppSpacing =
|
||||||
ctx.type === 'insert' && opr != null && opr.inner.whitespaceLengthInCodeBuffer > 0
|
ctx.type === 'insert' && opr != null && opr.inner.whitespaceLengthInCodeBuffer > 0 ?
|
||||||
? ' '.repeat(opr.inner.whitespaceLengthInCodeBuffer)
|
' '.repeat(opr.inner.whitespaceLengthInCodeBuffer)
|
||||||
: ''
|
: ''
|
||||||
const extendingAccessOprChain = opr != null && opr.repr() === '.'
|
const extendingAccessOprChain = opr != null && opr.repr() === '.'
|
||||||
// Modules are special case, as we want to encourage user to continue writing path.
|
// Modules are special case, as we want to encourage user to continue writing path.
|
||||||
if (entry.kind === SuggestionKind.Module) {
|
if (entry.kind === SuggestionKind.Module) {
|
||||||
|
@ -55,46 +55,64 @@ const suggestionDb = useSuggestionDbStore()
|
|||||||
const interaction = provideInteractionHandler()
|
const interaction = provideInteractionHandler()
|
||||||
|
|
||||||
/// === UI Messages and Errors ===
|
/// === UI Messages and Errors ===
|
||||||
|
function toastOnce(id: string, ...[content, options]: Parameters<typeof toast>) {
|
||||||
|
if (toast.isActive(id)) toast.update(id, { ...options, render: content })
|
||||||
|
else toast(content, { ...options, toastId: id })
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ToastId {
|
||||||
|
startup = 'startup',
|
||||||
|
connectionLost = 'connectionLost',
|
||||||
|
connectionError = 'connectionError',
|
||||||
|
lspError = 'lspError',
|
||||||
|
executionFailed = 'executionFailed',
|
||||||
|
}
|
||||||
|
|
||||||
function initStartupToast() {
|
function initStartupToast() {
|
||||||
let startupToast = toast.info('Initializing the project. This can take up to one minute.', {
|
toastOnce(ToastId.startup, 'Initializing the project. This can take up to one minute.', {
|
||||||
|
type: 'info',
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeToast = () => toast.dismiss(startupToast)
|
const removeToast = () => toast.dismiss(ToastId.startup)
|
||||||
projectStore.firstExecution.then(removeToast)
|
projectStore.firstExecution.then(removeToast)
|
||||||
onScopeDispose(removeToast)
|
onScopeDispose(removeToast)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConnectionLostToast() {
|
function initConnectionLostToast() {
|
||||||
let connectionLostToast = 'connectionLostToast'
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
ProjectManagerEvents.loadingFailed,
|
ProjectManagerEvents.loadingFailed,
|
||||||
() => {
|
() => {
|
||||||
toast.error('Lost connection to Language Server.', {
|
toastOnce(ToastId.connectionLost, 'Lost connection to Language Server.', {
|
||||||
|
type: 'error',
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
toastId: connectionLostToast,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
)
|
)
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
toast.dismiss(connectionLostToast)
|
toast.dismiss(ToastId.connectionLost)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
projectStore.lsRpcConnection.then(
|
projectStore.lsRpcConnection.then(
|
||||||
(ls) => {
|
(ls) => {
|
||||||
ls.client.onError((err) => {
|
ls.client.onError((err) => {
|
||||||
toast.error(`Language server error: ${err}`)
|
toastOnce(ToastId.lspError, `Language server error: ${err}`, { type: 'error' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
toast.error(`Connection to language server failed: ${JSON.stringify(err)}`)
|
toastOnce(
|
||||||
|
ToastId.connectionError,
|
||||||
|
`Connection to language server failed: ${JSON.stringify(err)}`,
|
||||||
|
{ type: 'error' },
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
projectStore.executionContext.on('executionComplete', () => toast.dismiss(ToastId.executionFailed))
|
||||||
projectStore.executionContext.on('executionFailed', (err) => {
|
projectStore.executionContext.on('executionFailed', (err) => {
|
||||||
toast.error(`Execution Failed: ${JSON.stringify(err)}`, {})
|
toastOnce(ToastId.executionFailed, `Execution Failed: ${JSON.stringify(err)}`, { type: 'error' })
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -55,9 +55,8 @@ const targetPos = computed<Vec2 | undefined>(() => {
|
|||||||
if (expr != null && targetNode.value != null && targetNodeRect.value != null) {
|
if (expr != null && targetNode.value != null && targetNodeRect.value != null) {
|
||||||
const targetRectRelative = graph.getPortRelativeRect(expr)
|
const targetRectRelative = graph.getPortRelativeRect(expr)
|
||||||
if (targetRectRelative == null) return
|
if (targetRectRelative == null) return
|
||||||
const yAdjustment = targetIsSelfArgument.value
|
const yAdjustment =
|
||||||
? -(selfArgumentArrowHeight + selfArgumentArrowYOffset)
|
targetIsSelfArgument.value ? -(selfArgumentArrowHeight + selfArgumentArrowYOffset) : 0
|
||||||
: 0
|
|
||||||
return targetNodeRect.value.pos.add(new Vec2(targetRectRelative.center().x, yAdjustment))
|
return targetNodeRect.value.pos.add(new Vec2(targetRectRelative.center().x, yAdjustment))
|
||||||
} else if (navigator?.sceneMousePos != null) {
|
} else if (navigator?.sceneMousePos != null) {
|
||||||
return navigator.sceneMousePos
|
return navigator.sceneMousePos
|
||||||
@ -389,9 +388,9 @@ const activeStyle = computed(() => {
|
|||||||
const distances = mouseLocationOnEdge.value
|
const distances = mouseLocationOnEdge.value
|
||||||
if (distances == null) return {}
|
if (distances == null) return {}
|
||||||
const offset =
|
const offset =
|
||||||
distances.sourceToMouse < distances.mouseToTarget
|
distances.sourceToMouse < distances.mouseToTarget ?
|
||||||
? distances.mouseToTarget
|
distances.mouseToTarget
|
||||||
: -distances.sourceToMouse
|
: -distances.sourceToMouse
|
||||||
return {
|
return {
|
||||||
...baseStyle.value,
|
...baseStyle.value,
|
||||||
strokeDasharray: distances.sourceToTarget,
|
strokeDasharray: distances.sourceToTarget,
|
||||||
|
@ -71,14 +71,13 @@ const outputPortsSet = computed(() => {
|
|||||||
return bindings
|
return bindings
|
||||||
})
|
})
|
||||||
|
|
||||||
const widthOverridePx = ref<number>()
|
|
||||||
const nodeId = computed(() => asNodeId(props.node.rootSpan.id))
|
const nodeId = computed(() => asNodeId(props.node.rootSpan.id))
|
||||||
const externalId = computed(() => props.node.rootSpan.externalId)
|
const externalId = computed(() => props.node.rootSpan.externalId)
|
||||||
const potentialSelfArgumentId = computed(() => props.node.primarySubject)
|
const potentialSelfArgumentId = computed(() => props.node.primarySubject)
|
||||||
const connectedSelfArgumentId = computed(() =>
|
const connectedSelfArgumentId = computed(() =>
|
||||||
props.node.primarySubject && graph.isConnectedTarget(props.node.primarySubject)
|
props.node.primarySubject && graph.isConnectedTarget(props.node.primarySubject) ?
|
||||||
? props.node.primarySubject
|
props.node.primarySubject
|
||||||
: undefined,
|
: undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => graph.unregisterNodeRect(nodeId.value))
|
onUnmounted(() => graph.unregisterNodeRect(nodeId.value))
|
||||||
@ -86,7 +85,6 @@ onUnmounted(() => graph.unregisterNodeRect(nodeId.value))
|
|||||||
const rootNode = ref<HTMLElement>()
|
const rootNode = ref<HTMLElement>()
|
||||||
const contentNode = ref<HTMLElement>()
|
const contentNode = ref<HTMLElement>()
|
||||||
const nodeSize = useResizeObserver(rootNode)
|
const nodeSize = useResizeObserver(rootNode)
|
||||||
const baseNodeSize = computed(() => new Vec2(contentNode.value?.scrollWidth ?? 0, nodeSize.value.y))
|
|
||||||
|
|
||||||
const error = computed(() => {
|
const error = computed(() => {
|
||||||
const externalId = graph.db.idToExternal(nodeId.value)
|
const externalId = graph.db.idToExternal(nodeId.value)
|
||||||
@ -215,11 +213,11 @@ const isOutputContextOverridden = computed({
|
|||||||
const module = projectStore.module
|
const module = projectStore.module
|
||||||
if (!module) return
|
if (!module) return
|
||||||
const edit = props.node.rootSpan.module.edit()
|
const edit = props.node.rootSpan.module.edit()
|
||||||
const replacementText = shouldOverride
|
const replacementText =
|
||||||
? [Ast.TextLiteral.new(projectStore.executionMode, edit)]
|
shouldOverride ? [Ast.TextLiteral.new(projectStore.executionMode, edit)] : undefined
|
||||||
: undefined
|
const replacements =
|
||||||
const replacements = projectStore.isOutputContextEnabled
|
projectStore.isOutputContextEnabled ?
|
||||||
? {
|
{
|
||||||
enableOutputContext: undefined,
|
enableOutputContext: undefined,
|
||||||
disableOutputContext: replacementText,
|
disableOutputContext: replacementText,
|
||||||
}
|
}
|
||||||
@ -388,10 +386,7 @@ const documentation = computed<string | undefined>({
|
|||||||
class="GraphNode"
|
class="GraphNode"
|
||||||
:style="{
|
:style="{
|
||||||
transform,
|
transform,
|
||||||
width:
|
minWidth: isVisualizationVisible ? `${visualizationWidth}px` : undefined,
|
||||||
widthOverridePx != null && isVisualizationVisible
|
|
||||||
? `${Math.max(widthOverridePx, contentNode?.scrollWidth ?? 0)}px`
|
|
||||||
: undefined,
|
|
||||||
'--node-group-color': color,
|
'--node-group-color': color,
|
||||||
}"
|
}"
|
||||||
:class="{
|
:class="{
|
||||||
@ -424,7 +419,7 @@ const documentation = computed<string | undefined>({
|
|||||||
/>
|
/>
|
||||||
<GraphVisualization
|
<GraphVisualization
|
||||||
v-if="isVisualizationVisible"
|
v-if="isVisualizationVisible"
|
||||||
:nodeSize="baseNodeSize"
|
:nodeSize="nodeSize"
|
||||||
:scale="navigator?.scale ?? 1"
|
:scale="navigator?.scale ?? 1"
|
||||||
:nodePosition="props.node.position"
|
:nodePosition="props.node.position"
|
||||||
:isCircularMenuVisible="menuVisible"
|
:isCircularMenuVisible="menuVisible"
|
||||||
@ -434,10 +429,7 @@ const documentation = computed<string | undefined>({
|
|||||||
:typename="expressionInfo?.typename"
|
:typename="expressionInfo?.typename"
|
||||||
:width="visualizationWidth"
|
:width="visualizationWidth"
|
||||||
:isFocused="isOnlyOneSelected"
|
:isFocused="isOnlyOneSelected"
|
||||||
@update:rect="
|
@update:rect="emit('update:visualizationRect', $event)"
|
||||||
emit('update:visualizationRect', $event),
|
|
||||||
(widthOverridePx = $event && $event.size.x > baseNodeSize.x ? $event.size.x : undefined)
|
|
||||||
"
|
|
||||||
@update:id="emit('update:visualizationId', $event)"
|
@update:id="emit('update:visualizationId', $event)"
|
||||||
@update:visible="emit('update:visualizationVisible', $event)"
|
@update:visible="emit('update:visualizationVisible', $event)"
|
||||||
@update:fullscreen="emit('update:visualizationFullscreen', $event)"
|
@update:fullscreen="emit('update:visualizationFullscreen', $event)"
|
||||||
@ -453,7 +445,7 @@ const documentation = computed<string | undefined>({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<div
|
<div
|
||||||
ref="contentNode"
|
ref="contentNode"
|
||||||
class="node"
|
class="content"
|
||||||
v-on="dragPointer.events"
|
v-on="dragPointer.events"
|
||||||
@click.stop
|
@click.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@ -600,6 +592,7 @@ const documentation = computed<string | undefined>({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: var(--node-border-radius);
|
border-radius: var(--node-border-radius);
|
||||||
transition: box-shadow 0.2s ease-in-out;
|
transition: box-shadow 0.2s ease-in-out;
|
||||||
|
box-sizing: border-box;
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(255, 255, 255, 20%);
|
background-color: rgba(255, 255, 255, 20%);
|
||||||
}
|
}
|
||||||
@ -609,7 +602,7 @@ const documentation = computed<string | undefined>({
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node {
|
.content {
|
||||||
font-family: var(--font-code);
|
font-family: var(--font-code);
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -617,7 +610,7 @@ const documentation = computed<string | undefined>({
|
|||||||
caret-shape: bar;
|
caret-shape: bar;
|
||||||
height: var(--node-height);
|
height: var(--node-height);
|
||||||
border-radius: var(--node-border-radius);
|
border-radius: var(--node-border-radius);
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -29,7 +29,7 @@ const iconForType: Record<GraphNodeMessageType, Icon | undefined> = {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="GraphNodeMessage" :class="styleClassForType[props.type]">
|
<div class="GraphNodeMessage" :class="styleClassForType[props.type]">
|
||||||
<SvgIcon class="icon" :name="icon" />
|
<SvgIcon v-if="icon" class="icon" :name="icon" />
|
||||||
<div v-text="props.message"></div>
|
<div v-text="props.message"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -20,7 +20,6 @@ import type { Result } from '@/util/data/result'
|
|||||||
import type { URLString } from '@/util/data/urlString'
|
import type { URLString } from '@/util/data/urlString'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import type { Icon } from '@/util/iconName'
|
import type { Icon } from '@/util/iconName'
|
||||||
import { debouncedGetter } from '@/util/reactivity'
|
|
||||||
import { computedAsync } from '@vueuse/core'
|
import { computedAsync } from '@vueuse/core'
|
||||||
import { isIdentifier } from 'shared/ast'
|
import { isIdentifier } from 'shared/ast'
|
||||||
import { visIdentifierEquals, type VisualizationIdentifier } from 'shared/yjsModel'
|
import { visIdentifierEquals, type VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
@ -91,9 +90,9 @@ const defaultVisualizationForCurrentNodeSource = computed<VisualizationIdentifie
|
|||||||
return {
|
return {
|
||||||
name: raw.value.name,
|
name: raw.value.name,
|
||||||
module:
|
module:
|
||||||
raw.value.library == null
|
raw.value.library == null ?
|
||||||
? { kind: 'Builtin' }
|
{ kind: 'Builtin' }
|
||||||
: { kind: 'Library', name: raw.value.library.name },
|
: { kind: 'Library', name: raw.value.library.name },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -225,26 +224,23 @@ watchEffect(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isBelowToolbar = ref(false)
|
const isBelowToolbar = ref(false)
|
||||||
let width = ref<Opt<number>>(props.width)
|
let userSetHeight = ref(150)
|
||||||
let height = ref(150)
|
|
||||||
// We want to debounce width changes, because they are saved to the metadata.
|
|
||||||
const debouncedWidth = debouncedGetter(() => width.value, 300)
|
|
||||||
watch(debouncedWidth, (value) => value != null && emit('update:width', value))
|
|
||||||
|
|
||||||
watchEffect(() =>
|
const rect = computed(
|
||||||
emit(
|
() =>
|
||||||
'update:rect',
|
|
||||||
new Rect(
|
new Rect(
|
||||||
props.nodePosition,
|
props.nodePosition,
|
||||||
new Vec2(
|
new Vec2(
|
||||||
width.value ?? props.nodeSize.x,
|
Math.max(props.width ?? 0, props.nodeSize.x),
|
||||||
height.value + (isBelowToolbar.value ? TOP_WITH_TOOLBAR_PX : TOP_WITHOUT_TOOLBAR_PX),
|
userSetHeight.value + (isBelowToolbar.value ? TOP_WITH_TOOLBAR_PX : TOP_WITHOUT_TOOLBAR_PX),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => emit('update:rect', undefined))
|
watchEffect(() => emit('update:rect', rect.value))
|
||||||
|
onUnmounted(() => {
|
||||||
|
emit('update:rect', undefined)
|
||||||
|
})
|
||||||
|
|
||||||
const allTypes = computed(() => Array.from(visualizationStore.types(props.typename)))
|
const allTypes = computed(() => Array.from(visualizationStore.types(props.typename)))
|
||||||
|
|
||||||
@ -262,16 +258,16 @@ provideVisualizationConfig({
|
|||||||
return props.scale
|
return props.scale
|
||||||
},
|
},
|
||||||
get width() {
|
get width() {
|
||||||
return width.value ?? null
|
return rect.value.width
|
||||||
},
|
},
|
||||||
set width(value) {
|
set width(value) {
|
||||||
width.value = value
|
emit('update:width', value)
|
||||||
},
|
},
|
||||||
get height() {
|
get height() {
|
||||||
return height.value
|
return userSetHeight.value
|
||||||
},
|
},
|
||||||
set height(value) {
|
set height(value) {
|
||||||
height.value = value
|
userSetHeight.value = value
|
||||||
},
|
},
|
||||||
get isBelowToolbar() {
|
get isBelowToolbar() {
|
||||||
return isBelowToolbar.value
|
return isBelowToolbar.value
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import type { WidgetModule } from '@/providers/widgetRegistry'
|
||||||
injectWidgetRegistry,
|
import { injectWidgetRegistry, WidgetInput, type WidgetUpdate } from '@/providers/widgetRegistry'
|
||||||
type WidgetInput,
|
|
||||||
type WidgetUpdate,
|
|
||||||
} from '@/providers/widgetRegistry'
|
|
||||||
import { injectWidgetTree } from '@/providers/widgetTree'
|
import { injectWidgetTree } from '@/providers/widgetTree'
|
||||||
import {
|
import {
|
||||||
injectWidgetUsageInfo,
|
injectWidgetUsageInfo,
|
||||||
@ -12,7 +9,7 @@ import {
|
|||||||
} from '@/providers/widgetUsageInfo'
|
} from '@/providers/widgetUsageInfo'
|
||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import { computed, proxyRefs } from 'vue'
|
import { computed, getCurrentInstance, proxyRefs, shallowRef, watchEffect, withCtx } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
input: WidgetInput
|
input: WidgetInput
|
||||||
@ -43,16 +40,17 @@ const sameInputParentWidgets = computed(() =>
|
|||||||
)
|
)
|
||||||
const nesting = computed(() => (parentUsageInfo?.nesting ?? 0) + (props.nest === true ? 1 : 0))
|
const nesting = computed(() => (parentUsageInfo?.nesting ?? 0) + (props.nest === true ? 1 : 0))
|
||||||
|
|
||||||
const selectedWidget = computed(() => {
|
const selectedWidget = shallowRef<WidgetModule<WidgetInput> | undefined>()
|
||||||
return registry.select(
|
const updateSelection = withCtx(() => {
|
||||||
|
selectedWidget.value = registry.select(
|
||||||
{
|
{
|
||||||
input: props.input,
|
input: props.input,
|
||||||
nesting: nesting.value,
|
nesting: nesting.value,
|
||||||
},
|
},
|
||||||
sameInputParentWidgets.value,
|
sameInputParentWidgets.value,
|
||||||
)
|
)
|
||||||
})
|
}, getCurrentInstance())
|
||||||
|
watchEffect(() => updateSelection())
|
||||||
const updateHandler = computed(() => {
|
const updateHandler = computed(() => {
|
||||||
const nextHandler =
|
const nextHandler =
|
||||||
parentUsageInfo?.updateHandler ?? (() => console.log('Missing update handler'))
|
parentUsageInfo?.updateHandler ?? (() => console.log('Missing update handler'))
|
||||||
|
@ -48,7 +48,9 @@ function handleWidgetUpdates(update: WidgetUpdate) {
|
|||||||
const { value, origin } = update.portUpdate
|
const { value, origin } = update.portUpdate
|
||||||
if (Ast.isAstId(origin)) {
|
if (Ast.isAstId(origin)) {
|
||||||
const ast =
|
const ast =
|
||||||
value instanceof Ast.Ast ? value : value == null ? Ast.Wildcard.new(edit) : undefined
|
value instanceof Ast.Ast ? value
|
||||||
|
: value == null ? Ast.Wildcard.new(edit)
|
||||||
|
: undefined
|
||||||
if (ast) {
|
if (ast) {
|
||||||
edit.replaceValue(origin as Ast.AstId, ast)
|
edit.replaceValue(origin as Ast.AstId, ast)
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
@ -105,7 +107,7 @@ provideWidgetTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:has(.WidgetPort.newToConnect > .r-24:only-child) {
|
&:has(.WidgetPort.newToConnect > .r-24:only-child) {
|
||||||
margin-left: 4px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,17 +171,19 @@ export function useDragging() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNodesPosition() {
|
updateNodesPosition() {
|
||||||
for (const [id, dragged] of this.draggedNodes) {
|
graphStore.batchEdits(() => {
|
||||||
const node = graphStore.db.nodeIdToNode.get(id)
|
for (const [id, dragged] of this.draggedNodes) {
|
||||||
if (node == null) continue
|
const node = graphStore.db.nodeIdToNode.get(id)
|
||||||
// If node was moved in other way than current dragging, we want to stop dragging it.
|
if (node == null) continue
|
||||||
if (node.position.distanceSquared(dragged.currentPos) > 1.0) {
|
// If node was moved in other way than current dragging, we want to stop dragging it.
|
||||||
this.draggedNodes.delete(id)
|
if (node.position.distanceSquared(dragged.currentPos) > 1.0) {
|
||||||
} else {
|
this.draggedNodes.delete(id)
|
||||||
dragged.currentPos = dragged.initialPos.add(snappedOffset.value)
|
} else {
|
||||||
graphStore.setNodePosition(id, dragged.currentPos)
|
dragged.currentPos = dragged.initialPos.add(snappedOffset.value)
|
||||||
|
graphStore.setNodePosition(id, dragged.currentPos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Awareness } from '@/stores/awareness'
|
import { Awareness } from '@/stores/awareness'
|
||||||
import * as astText from '@/util/ast/text'
|
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { Keccak, sha3_224 as SHA3 } from '@noble/hashes/sha3'
|
import { Keccak, sha3_224 as SHA3 } from '@noble/hashes/sha3'
|
||||||
import type { Hash } from '@noble/hashes/utils'
|
import type { Hash } from '@noble/hashes/utils'
|
||||||
import { bytesToHex } from '@noble/hashes/utils'
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
import { escapeTextLiteral } from 'shared/ast'
|
||||||
import type { DataServer } from 'shared/dataServer'
|
import type { DataServer } from 'shared/dataServer'
|
||||||
import type { LanguageServer } from 'shared/languageServer'
|
import type { LanguageServer } from 'shared/languageServer'
|
||||||
import { ErrorCode, RemoteRpcError } from 'shared/languageServer'
|
import { ErrorCode, RemoteRpcError } from 'shared/languageServer'
|
||||||
@ -17,10 +17,10 @@ const DATA_DIR_NAME = 'data'
|
|||||||
export function uploadedExpression(result: UploadResult) {
|
export function uploadedExpression(result: UploadResult) {
|
||||||
switch (result.source) {
|
switch (result.source) {
|
||||||
case 'Project': {
|
case 'Project': {
|
||||||
return `enso_project.data/'${astText.escape(result.name)}' . read`
|
return `enso_project.data/'${escapeTextLiteral(result.name)}' . read`
|
||||||
}
|
}
|
||||||
case 'FileSystemRoot': {
|
case 'FileSystemRoot': {
|
||||||
return `Data.read '${astText.escape(result.name)}'`
|
return `Data.read '${escapeTextLiteral(result.name)}'`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,7 @@ import { requiredImportsByFQN } from '@/stores/graph/imports'
|
|||||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||||
import { assert } from '@/util/assert'
|
import { assert } from '@/util/assert'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import type { TokenId } from '@/util/ast/abstract'
|
|
||||||
import { ArgumentInfoKey } from '@/util/callTree'
|
import { ArgumentInfoKey } from '@/util/callTree'
|
||||||
import { asNot } from '@/util/data/types.ts'
|
|
||||||
import { type Identifier, type QualifiedName } from '@/util/qualifiedName'
|
import { type Identifier, type QualifiedName } from '@/util/qualifiedName'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
@ -51,7 +49,7 @@ const value = computed({
|
|||||||
edit,
|
edit,
|
||||||
portUpdate: {
|
portUpdate: {
|
||||||
value: value ? 'True' : 'False',
|
value: value ? 'True' : 'False',
|
||||||
origin: asNot<TokenId>(props.input.portId),
|
origin: props.input.portId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -68,11 +66,9 @@ const argumentName = computed(() => {
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
function isBoolNode(ast: Ast.Ast) {
|
function isBoolNode(ast: Ast.Ast) {
|
||||||
const candidate =
|
const candidate =
|
||||||
ast instanceof Ast.PropertyAccess && ast.lhs?.code() === 'Boolean'
|
ast instanceof Ast.PropertyAccess && ast.lhs?.code() === 'Boolean' ? ast.rhs
|
||||||
? ast.rhs
|
: ast instanceof Ast.Ident ? ast.token
|
||||||
: ast instanceof Ast.Ident
|
: undefined
|
||||||
? ast.token
|
|
||||||
: undefined
|
|
||||||
return candidate && ['True', 'False'].includes(candidate.code())
|
return candidate && ['True', 'False'].includes(candidate.code())
|
||||||
}
|
}
|
||||||
function setBoolNode(ast: Ast.Mutable, value: Identifier): { requiresImport: boolean } {
|
function setBoolNode(ast: Ast.Mutable, value: Identifier): { requiresImport: boolean } {
|
||||||
@ -90,15 +86,15 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
|||||||
priority: 500,
|
priority: 500,
|
||||||
score: (props) => {
|
score: (props) => {
|
||||||
if (props.input.value instanceof Ast.Ast && isBoolNode(props.input.value)) return Score.Perfect
|
if (props.input.value instanceof Ast.Ast && isBoolNode(props.input.value)) return Score.Perfect
|
||||||
return props.input.expectedType === 'Standard.Base.Data.Boolean.Boolean'
|
return props.input.expectedType === 'Standard.Base.Data.Boolean.Boolean' ?
|
||||||
? Score.Good
|
Score.Good
|
||||||
: Score.Mismatch
|
: Score.Mismatch
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="CheckboxContainer" :class="{ primary }">
|
<div class="CheckboxContainer r-24" :class="{ primary }">
|
||||||
<span v-if="argumentName" class="name" v-text="argumentName" />
|
<span v-if="argumentName" class="name" v-text="argumentName" />
|
||||||
<!-- See comment in GraphNode next to dragPointer definition about stopping pointerdown and pointerup -->
|
<!-- See comment in GraphNode next to dragPointer definition about stopping pointerdown and pointerup -->
|
||||||
<CheckboxWidget
|
<CheckboxWidget
|
||||||
|
@ -78,10 +78,12 @@ const application = computed(() => {
|
|||||||
widgetCfg: widgetConfiguration.value,
|
widgetCfg: widgetConfiguration.value,
|
||||||
subjectAsSelf: selfArgumentPreapplied.value,
|
subjectAsSelf: selfArgumentPreapplied.value,
|
||||||
notAppliedArguments:
|
notAppliedArguments:
|
||||||
noArgsCall != null &&
|
(
|
||||||
(!subjectTypeMatchesMethod.value || noArgsCall.notAppliedArguments.length > 0)
|
noArgsCall != null &&
|
||||||
? noArgsCall.notAppliedArguments
|
(!subjectTypeMatchesMethod.value || noArgsCall.notAppliedArguments.length > 0)
|
||||||
: undefined,
|
) ?
|
||||||
|
noArgsCall.notAppliedArguments
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -107,9 +109,9 @@ const selfArgumentExternalId = computed<Opt<ExternalId>>(() => {
|
|||||||
const knownArguments = methodCallInfo.value?.suggestion?.arguments
|
const knownArguments = methodCallInfo.value?.suggestion?.arguments
|
||||||
const hasSelfArgument = knownArguments?.[0]?.name === 'self'
|
const hasSelfArgument = knownArguments?.[0]?.name === 'self'
|
||||||
const selfArgument =
|
const selfArgument =
|
||||||
hasSelfArgument && !selfArgumentPreapplied.value
|
hasSelfArgument && !selfArgumentPreapplied.value ?
|
||||||
? analyzed.args.find((a) => a.argName === 'self' || a.argName == null)?.argument
|
analyzed.args.find((a) => a.argName === 'self' || a.argName == null)?.argument
|
||||||
: getAccessOprSubject(analyzed.func) ?? analyzed.args[0]?.argument
|
: getAccessOprSubject(analyzed.func) ?? analyzed.args[0]?.argument
|
||||||
|
|
||||||
return selfArgument?.externalId
|
return selfArgument?.externalId
|
||||||
}
|
}
|
||||||
@ -191,9 +193,9 @@ function handleArgUpdate(update: WidgetUpdate): boolean {
|
|||||||
newArg = Ast.parse(value, edit)
|
newArg = Ast.parse(value, edit)
|
||||||
}
|
}
|
||||||
const name =
|
const name =
|
||||||
argApp.argument.insertAsNamed && isIdentifier(argApp.argument.argInfo.name)
|
argApp.argument.insertAsNamed && isIdentifier(argApp.argument.argInfo.name) ?
|
||||||
? argApp.argument.argInfo.name
|
argApp.argument.argInfo.name
|
||||||
: undefined
|
: undefined
|
||||||
edit
|
edit
|
||||||
.getVersion(argApp.appTree)
|
.getVersion(argApp.appTree)
|
||||||
.updateValue((oldAppTree) => Ast.App.new(edit, oldAppTree, name, newArg))
|
.updateValue((oldAppTree) => Ast.App.new(edit, oldAppTree, name, newArg))
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
import NumericInputWidget from '@/components/widgets/NumericInputWidget.vue'
|
import NumericInputWidget from '@/components/widgets/NumericInputWidget.vue'
|
||||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import type { TokenId } from '@/util/ast/abstract.ts'
|
|
||||||
import { asNot } from '@/util/data/types.ts'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps(widgetProps(widgetDefinition))
|
const props = defineProps(widgetProps(widgetDefinition))
|
||||||
@ -14,7 +12,7 @@ const value = computed({
|
|||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
props.onUpdate({
|
props.onUpdate({
|
||||||
portUpdate: { value: value.toString(), origin: asNot<TokenId>(props.input.portId) },
|
portUpdate: { value: value.toString(), origin: props.input.portId },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -10,16 +10,13 @@ import { injectWidgetTree } from '@/providers/widgetTree'
|
|||||||
import { PortViewInstance, useGraphStore } from '@/stores/graph'
|
import { PortViewInstance, useGraphStore } from '@/stores/graph'
|
||||||
import { assert } from '@/util/assert'
|
import { assert } from '@/util/assert'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import type { TokenId } from '@/util/ast/abstract'
|
|
||||||
import { ArgumentInfoKey } from '@/util/callTree'
|
import { ArgumentInfoKey } from '@/util/callTree'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { asNot } from '@/util/data/types.ts'
|
|
||||||
import { cachedGetter } from '@/util/reactivity'
|
import { cachedGetter } from '@/util/reactivity'
|
||||||
import { uuidv4 } from 'lib0/random'
|
import { uuidv4 } from 'lib0/random'
|
||||||
import { isUuid } from 'shared/yjsModel'
|
import { isUuid } from 'shared/yjsModel'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
markRaw,
|
|
||||||
nextTick,
|
nextTick,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
proxyRefs,
|
proxyRefs,
|
||||||
@ -66,7 +63,7 @@ const isTarget = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const rootNode = shallowRef<HTMLElement>()
|
const rootNode = shallowRef<HTMLElement>()
|
||||||
const nodeSize = useResizeObserver(rootNode, false)
|
const nodeSize = useResizeObserver(rootNode)
|
||||||
|
|
||||||
// Compute the scene-space bounding rectangle of the expression's widget. Those bounds are later
|
// Compute the scene-space bounding rectangle of the expression's widget. Those bounds are later
|
||||||
// used for edge positioning. Querying and updating those bounds is relatively expensive, so we only
|
// used for edge positioning. Querying and updating those bounds is relatively expensive, so we only
|
||||||
@ -82,7 +79,7 @@ const randomUuid = uuidv4() as PortId
|
|||||||
// effects depending on the port ID value will not be re-triggered unnecessarily.
|
// effects depending on the port ID value will not be re-triggered unnecessarily.
|
||||||
const portId = cachedGetter<PortId>(() => {
|
const portId = cachedGetter<PortId>(() => {
|
||||||
assert(!isUuid(props.input.portId))
|
assert(!isUuid(props.input.portId))
|
||||||
return asNot<TokenId>(props.input.portId)
|
return props.input.portId
|
||||||
})
|
})
|
||||||
|
|
||||||
const innerWidget = computed(() => {
|
const innerWidget = computed(() => {
|
||||||
@ -100,7 +97,7 @@ const randSlice = randomUuid.slice(0, 4)
|
|||||||
watchEffect(
|
watchEffect(
|
||||||
(onCleanup) => {
|
(onCleanup) => {
|
||||||
const id = portId.value
|
const id = portId.value
|
||||||
const instance = markRaw(new PortViewInstance(portRect, tree.nodeId, props.onUpdate))
|
const instance = new PortViewInstance(portRect, tree.nodeId, props.onUpdate)
|
||||||
graph.addPortInstance(id, instance)
|
graph.addPortInstance(id, instance)
|
||||||
onCleanup(() => graph.removePortInstance(id, instance))
|
onCleanup(() => graph.removePortInstance(id, instance))
|
||||||
},
|
},
|
||||||
@ -109,7 +106,7 @@ watchEffect(
|
|||||||
|
|
||||||
function updateRect() {
|
function updateRect() {
|
||||||
let domNode = rootNode.value
|
let domNode = rootNode.value
|
||||||
const rootDomNode = domNode?.closest('.node')
|
const rootDomNode = domNode?.closest('.GraphNode')
|
||||||
if (domNode == null || rootDomNode == null) return
|
if (domNode == null || rootDomNode == null) return
|
||||||
|
|
||||||
const exprClientRect = Rect.FromDomRect(domNode.getBoundingClientRect())
|
const exprClientRect = Rect.FromDomRect(domNode.getBoundingClientRect())
|
||||||
|
@ -16,11 +16,9 @@ import {
|
|||||||
type SuggestionEntryArgument,
|
type SuggestionEntryArgument,
|
||||||
} from '@/stores/suggestionDatabase/entry.ts'
|
} from '@/stores/suggestionDatabase/entry.ts'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import type { TokenId } from '@/util/ast/abstract.ts'
|
|
||||||
import { targetIsOutside } from '@/util/autoBlur'
|
import { targetIsOutside } from '@/util/autoBlur'
|
||||||
import { ArgumentInfoKey } from '@/util/callTree'
|
import { ArgumentInfoKey } from '@/util/callTree'
|
||||||
import { arrayEquals } from '@/util/data/array'
|
import { arrayEquals } from '@/util/data/array'
|
||||||
import { asNot } from '@/util/data/types.ts'
|
|
||||||
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
@ -53,11 +51,9 @@ function tagFromEntry(entry: SuggestionEntry): Tag {
|
|||||||
return {
|
return {
|
||||||
label: entry.name,
|
label: entry.name,
|
||||||
expression:
|
expression:
|
||||||
entry.selfType != null
|
entry.selfType != null ? `_.${entry.name}`
|
||||||
? `_.${entry.name}`
|
: entry.memberOf ? `${qnLastSegment(entry.memberOf)}.${entry.name}`
|
||||||
: entry.memberOf
|
: entry.name,
|
||||||
? `${qnLastSegment(entry.memberOf)}.${entry.name}`
|
|
||||||
: entry.name,
|
|
||||||
requiredImports: requiredImports(suggestions.entries, entry),
|
requiredImports: requiredImports(suggestions.entries, entry),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +94,11 @@ const selectedTag = computed(() => {
|
|||||||
// To prevent partial prefix matches, we arrange tags in reverse lexicographical order.
|
// To prevent partial prefix matches, we arrange tags in reverse lexicographical order.
|
||||||
const sortedTags = tags.value
|
const sortedTags = tags.value
|
||||||
.map((tag, index) => [removeSurroundingParens(tag.expression), index] as [string, number])
|
.map((tag, index) => [removeSurroundingParens(tag.expression), index] as [string, number])
|
||||||
.sort(([a], [b]) => (a < b ? 1 : a > b ? -1 : 0))
|
.sort(([a], [b]) =>
|
||||||
|
a < b ? 1
|
||||||
|
: a > b ? -1
|
||||||
|
: 0,
|
||||||
|
)
|
||||||
const [_, index] = sortedTags.find(([expr]) => currentExpression.startsWith(expr)) ?? []
|
const [_, index] = sortedTags.find(([expr]) => currentExpression.startsWith(expr)) ?? []
|
||||||
return index != null ? tags.value[index] : undefined
|
return index != null ? tags.value[index] : undefined
|
||||||
}
|
}
|
||||||
@ -146,13 +146,7 @@ watch(selectedIndex, (_index) => {
|
|||||||
value = conflicts[0]?.fullyQualified
|
value = conflicts[0]?.fullyQualified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
props.onUpdate({
|
props.onUpdate({ edit, portUpdate: { value, origin: props.input.portId } })
|
||||||
edit,
|
|
||||||
portUpdate: {
|
|
||||||
value,
|
|
||||||
origin: asNot<TokenId>(props.input.portId),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isHovered = ref(false)
|
const isHovered = ref(false)
|
||||||
|
@ -14,9 +14,9 @@ const icon = computed(() => tree.icon)
|
|||||||
export const widgetDefinition = defineWidget(WidgetInput.isAst, {
|
export const widgetDefinition = defineWidget(WidgetInput.isAst, {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
score: (props, _db) =>
|
score: (props, _db) =>
|
||||||
props.input.value.id === injectWidgetTree().connectedSelfArgumentId
|
props.input.value.id === injectWidgetTree().connectedSelfArgumentId ?
|
||||||
? Score.Perfect
|
Score.Perfect
|
||||||
: Score.Mismatch,
|
: Score.Mismatch,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,23 +1,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import EnsoTextInputWidget from '@/components/widgets/EnsoTextInputWidget.vue'
|
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||||
|
import AutoSizedInput from '@/components/widgets/AutoSizedInput.vue'
|
||||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import type { TokenId } from '@/util/ast/abstract'
|
import { MutableModule } from '@/util/ast/abstract'
|
||||||
import { asNot } from '@/util/data/types'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps(widgetProps(widgetDefinition))
|
const props = defineProps(widgetProps(widgetDefinition))
|
||||||
const value = computed({
|
const graph = useGraphStore()
|
||||||
|
|
||||||
|
const inputTextLiteral = computed((): Ast.TextLiteral | undefined => {
|
||||||
|
if (props.input.value instanceof Ast.TextLiteral) return props.input.value
|
||||||
|
const valueStr = WidgetInput.valueRepr(props.input)
|
||||||
|
const parsed = valueStr != null ? Ast.parse(valueStr) : undefined
|
||||||
|
if (parsed instanceof Ast.TextLiteral) return parsed
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
function makeNewLiteral(value: string) {
|
||||||
|
return Ast.TextLiteral.new(value, MutableModule.Transient())
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyTextLiteral = makeNewLiteral('')
|
||||||
|
const shownLiteral = computed(() => inputTextLiteral.value ?? emptyTextLiteral)
|
||||||
|
const closeToken = computed(() => shownLiteral.value.close ?? shownLiteral.value.open)
|
||||||
|
|
||||||
|
const textContents = computed({
|
||||||
get() {
|
get() {
|
||||||
const valueStr = WidgetInput.valueRepr(props.input)
|
return shownLiteral.value.rawTextContent
|
||||||
return typeof valueStr === 'string' && Ast.parse(valueStr) instanceof Ast.TextLiteral
|
|
||||||
? valueStr
|
|
||||||
: ''
|
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
props.onUpdate({
|
if (props.input.value instanceof Ast.TextLiteral) {
|
||||||
portUpdate: { value: value.toString(), origin: asNot<TokenId>(props.input.portId) },
|
const edit = graph.startEdit()
|
||||||
})
|
edit.getVersion(props.input.value).setRawTextContent(value)
|
||||||
|
props.onUpdate({ edit })
|
||||||
|
} else {
|
||||||
|
props.onUpdate({
|
||||||
|
portUpdate: {
|
||||||
|
value: makeNewLiteral(value).code(),
|
||||||
|
origin: props.input.portId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -36,13 +61,29 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- See comment in GraphNode next to dragPointer definition about stopping pointerdown -->
|
<label class="WidgetText r-24" @pointerdown.stop>
|
||||||
<EnsoTextInputWidget v-model="value" class="WidgetText r-24" @pointerdown.stop />
|
<NodeWidget v-if="shownLiteral.open" :input="WidgetInput.FromAst(shownLiteral.open)" />
|
||||||
|
<AutoSizedInput v-model.lazy="textContents" />
|
||||||
|
<NodeWidget v-if="closeToken" :input="WidgetInput.FromAst(closeToken)" />
|
||||||
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.WidgetText {
|
.WidgetText {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
background: var(--color-widget);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 0px 4px;
|
||||||
|
min-width: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:has(> .AutoSizedInput:focus) {
|
||||||
|
outline: none;
|
||||||
|
background: var(--color-widget-focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -10,9 +10,9 @@ const props = defineProps(widgetProps(widgetDefinition))
|
|||||||
export const widgetDefinition = defineWidget(ArgumentInfoKey, {
|
export const widgetDefinition = defineWidget(ArgumentInfoKey, {
|
||||||
priority: -1,
|
priority: -1,
|
||||||
score: (props) =>
|
score: (props) =>
|
||||||
props.nesting < 2 && props.input[ArgumentInfoKey].appKind === ApplicationKind.Prefix
|
props.nesting < 2 && props.input[ArgumentInfoKey].appKind === ApplicationKind.Prefix ?
|
||||||
? Score.Perfect
|
Score.Perfect
|
||||||
: Score.Mismatch,
|
: Score.Mismatch,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -4,16 +4,15 @@ import ListWidget from '@/components/widgets/ListWidget.vue'
|
|||||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import { MutableModule, type TokenId } from '@/util/ast/abstract.ts'
|
import { MutableModule } from '@/util/ast/abstract.ts'
|
||||||
import { asNot } from '@/util/data/types.ts'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps(widgetProps(widgetDefinition))
|
const props = defineProps(widgetProps(widgetDefinition))
|
||||||
|
|
||||||
const itemConfig = computed(() =>
|
const itemConfig = computed(() =>
|
||||||
props.input.dynamicConfig?.kind === 'Vector_Editor'
|
props.input.dynamicConfig?.kind === 'Vector_Editor' ?
|
||||||
? props.input.dynamicConfig.item_editor
|
props.input.dynamicConfig.item_editor
|
||||||
: undefined,
|
: undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultItem = computed(() => {
|
const defaultItem = computed(() => {
|
||||||
@ -35,7 +34,7 @@ const value = computed({
|
|||||||
// TODO[ao]: here we re-create AST. It would be better to reuse existing AST nodes.
|
// TODO[ao]: here we re-create AST. It would be better to reuse existing AST nodes.
|
||||||
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
||||||
props.onUpdate({
|
props.onUpdate({
|
||||||
portUpdate: { value: newCode, origin: asNot<TokenId>(props.input.portId) },
|
portUpdate: { value: newCode, origin: props.input.portId },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -51,8 +50,8 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
|||||||
else if (props.input.expectedType?.startsWith('Standard.Base.Data.Vector.Vector'))
|
else if (props.input.expectedType?.startsWith('Standard.Base.Data.Vector.Vector'))
|
||||||
return Score.Good
|
return Score.Good
|
||||||
else if (props.input.value instanceof Ast.Ast) {
|
else if (props.input.value instanceof Ast.Ast) {
|
||||||
return props.input.value.children().next().value.code() === '['
|
return props.input.value.children().next().value.code() === '[' ?
|
||||||
? Score.Perfect
|
Score.Perfect
|
||||||
: Score.Mismatch
|
: Score.Mismatch
|
||||||
} else return Score.Mismatch
|
} else return Score.Mismatch
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useApproach } from '@/composables/animation'
|
import { useApproach } from '@/composables/animation'
|
||||||
import type { Vec2 } from '@/util/data/vec2'
|
import type { Vec2 } from '@/util/data/vec2'
|
||||||
import { computed, watch, type Ref } from 'vue'
|
import { computed, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
position: Vec2
|
position: Vec2
|
||||||
@ -9,7 +9,15 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const hidden = computed(() => props.anchor == null)
|
const hidden = computed(() => props.anchor == null)
|
||||||
const lastSetAnchor: Ref<Vec2 | undefined> = computed(() => props.anchor ?? lastSetAnchor.value)
|
const lastSetAnchor = shallowRef<Vec2>()
|
||||||
|
watch(
|
||||||
|
() => props.anchor,
|
||||||
|
(anchor) => {
|
||||||
|
if (anchor !== null && lastSetAnchor.value !== anchor) {
|
||||||
|
lastSetAnchor.value = anchor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const anchorAnimFactor = useApproach(() => (props.anchor != null ? 1 : 0), 60)
|
const anchorAnimFactor = useApproach(() => (props.anchor != null ? 1 : 0), 60)
|
||||||
watch(
|
watch(
|
||||||
|
@ -49,7 +49,7 @@ function blur(event: Event) {
|
|||||||
const rootNode = ref<HTMLElement>()
|
const rootNode = ref<HTMLElement>()
|
||||||
const contentNode = ref<HTMLElement>()
|
const contentNode = ref<HTMLElement>()
|
||||||
|
|
||||||
onMounted(() => (config.width = Math.max(config.width ?? config.nodeSize.x, MIN_WIDTH_PX)))
|
onMounted(() => (config.width = MIN_WIDTH_PX))
|
||||||
|
|
||||||
function hideSelector() {
|
function hideSelector() {
|
||||||
requestAnimationFrame(() => (isSelectorVisible.value = false))
|
requestAnimationFrame(() => (isSelectorVisible.value = false))
|
||||||
@ -61,7 +61,7 @@ const resizeRight = usePointer((pos, _, type) => {
|
|||||||
}
|
}
|
||||||
const width =
|
const width =
|
||||||
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
||||||
config.width = Math.max(config.nodeSize.x, width, MIN_WIDTH_PX)
|
config.width = Math.max(width, MIN_WIDTH_PX)
|
||||||
}, PointerButtonMask.Main)
|
}, PointerButtonMask.Main)
|
||||||
|
|
||||||
const resizeBottom = usePointer((pos, _, type) => {
|
const resizeBottom = usePointer((pos, _, type) => {
|
||||||
@ -80,7 +80,7 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
if (pos.delta.x !== 0) {
|
if (pos.delta.x !== 0) {
|
||||||
const width =
|
const width =
|
||||||
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
||||||
config.width = Math.max(config.nodeSize.x, width)
|
config.width = Math.max(0, width)
|
||||||
}
|
}
|
||||||
if (pos.delta.y !== 0) {
|
if (pos.delta.y !== 0) {
|
||||||
const height =
|
const height =
|
||||||
@ -117,12 +117,10 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
class="content scrollable"
|
class="content scrollable"
|
||||||
:class="{ overflow: props.overflow }"
|
:class="{ overflow: props.overflow }"
|
||||||
:style="{
|
:style="{
|
||||||
width: config.fullscreen
|
width:
|
||||||
? undefined
|
config.fullscreen ? undefined : `${Math.max(config.width ?? 0, config.nodeSize.x)}px`,
|
||||||
: `${Math.max(config.width ?? 0, config.nodeSize.x)}px`,
|
height:
|
||||||
height: config.fullscreen
|
config.fullscreen ? undefined : `${Math.max(config.height ?? 0, config.nodeSize.y)}px`,
|
||||||
? undefined
|
|
||||||
: `${Math.max(config.height ?? 0, config.nodeSize.y)}px`,
|
|
||||||
}"
|
}"
|
||||||
@wheel.passive="onWheel"
|
@wheel.passive="onWheel"
|
||||||
>
|
>
|
||||||
|
@ -380,9 +380,9 @@ function pushPoints(newPoints: Location[]) {
|
|||||||
) {
|
) {
|
||||||
let position: [number, number] = [point.longitude, point.latitude]
|
let position: [number, number] = [point.longitude, point.latitude]
|
||||||
let radius =
|
let radius =
|
||||||
typeof point.radius === 'number' && !Number.isNaN(point.radius)
|
typeof point.radius === 'number' && !Number.isNaN(point.radius) ?
|
||||||
? point.radius
|
point.radius
|
||||||
: DEFAULT_POINT_RADIUS
|
: DEFAULT_POINT_RADIUS
|
||||||
let color = point.color ?? ACCENT_COLOR
|
let color = point.color ?? ACCENT_COLOR
|
||||||
let label = point.label ?? ''
|
let label = point.label ?? ''
|
||||||
points.push({ position, color, radius, label })
|
points.push({ position, color, radius, label })
|
||||||
|
@ -89,14 +89,16 @@ const fill = computed(() =>
|
|||||||
|
|
||||||
const width = ref(Math.max(config.width ?? 0, config.nodeSize.x))
|
const width = ref(Math.max(config.width ?? 0, config.nodeSize.x))
|
||||||
watchPostEffect(() => {
|
watchPostEffect(() => {
|
||||||
width.value = config.fullscreen
|
width.value =
|
||||||
? containerNode.value?.parentElement?.clientWidth ?? 0
|
config.fullscreen ?
|
||||||
|
containerNode.value?.parentElement?.clientWidth ?? 0
|
||||||
: Math.max(config.width ?? 0, config.nodeSize.x)
|
: Math.max(config.width ?? 0, config.nodeSize.x)
|
||||||
})
|
})
|
||||||
const height = ref(config.height ?? (config.nodeSize.x * 3) / 4)
|
const height = ref(config.height ?? (config.nodeSize.x * 3) / 4)
|
||||||
watchPostEffect(() => {
|
watchPostEffect(() => {
|
||||||
height.value = config.fullscreen
|
height.value =
|
||||||
? containerNode.value?.parentElement?.clientHeight ?? 0
|
config.fullscreen ?
|
||||||
|
containerNode.value?.parentElement?.clientHeight ?? 0
|
||||||
: config.height ?? (config.nodeSize.x * 3) / 4
|
: config.height ?? (config.nodeSize.x * 3) / 4
|
||||||
})
|
})
|
||||||
const boxWidth = computed(() => Math.max(0, width.value - MARGIN.left - MARGIN.right))
|
const boxWidth = computed(() => Math.max(0, width.value - MARGIN.left - MARGIN.right))
|
||||||
|
@ -248,14 +248,16 @@ const margin = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
const width = ref(Math.max(config.width ?? 0, config.nodeSize.x))
|
const width = ref(Math.max(config.width ?? 0, config.nodeSize.x))
|
||||||
watchPostEffect(() => {
|
watchPostEffect(() => {
|
||||||
width.value = config.fullscreen
|
width.value =
|
||||||
? containerNode.value?.parentElement?.clientWidth ?? 0
|
config.fullscreen ?
|
||||||
|
containerNode.value?.parentElement?.clientWidth ?? 0
|
||||||
: Math.max(config.width ?? 0, config.nodeSize.x)
|
: Math.max(config.width ?? 0, config.nodeSize.x)
|
||||||
})
|
})
|
||||||
const height = ref(config.height ?? (config.nodeSize.x * 3) / 4)
|
const height = ref(config.height ?? (config.nodeSize.x * 3) / 4)
|
||||||
watchPostEffect(() => {
|
watchPostEffect(() => {
|
||||||
height.value = config.fullscreen
|
height.value =
|
||||||
? containerNode.value?.parentElement?.clientHeight ?? 0
|
config.fullscreen ?
|
||||||
|
containerNode.value?.parentElement?.clientHeight ?? 0
|
||||||
: config.height ?? (config.nodeSize.x * 3) / 4
|
: config.height ?? (config.nodeSize.x * 3) / 4
|
||||||
})
|
})
|
||||||
const boxWidth = computed(() => Math.max(0, width.value - margin.value.left - margin.value.right))
|
const boxWidth = computed(() => Math.max(0, width.value - margin.value.left - margin.value.right))
|
||||||
@ -309,7 +311,9 @@ const zoom = computed(() =>
|
|||||||
const medDelta = 0.05
|
const medDelta = 0.05
|
||||||
const maxDelta = 1
|
const maxDelta = 1
|
||||||
const wheelSpeedMultiplier =
|
const wheelSpeedMultiplier =
|
||||||
event.deltaMode === 1 ? medDelta : event.deltaMode ? maxDelta : minDelta
|
event.deltaMode === 1 ? medDelta
|
||||||
|
: event.deltaMode ? maxDelta
|
||||||
|
: minDelta
|
||||||
return -event.deltaY * wheelSpeedMultiplier
|
return -event.deltaY * wheelSpeedMultiplier
|
||||||
})
|
})
|
||||||
.scaleExtent(ZOOM_EXTENT)
|
.scaleExtent(ZOOM_EXTENT)
|
||||||
|
@ -46,9 +46,9 @@ const props = defineProps<{ data: Data }>()
|
|||||||
const theme: Theme = DEFAULT_THEME
|
const theme: Theme = DEFAULT_THEME
|
||||||
|
|
||||||
const language = computed(() =>
|
const language = computed(() =>
|
||||||
props.data.dialect != null && sqlFormatter.supportedDialects.includes(props.data.dialect)
|
props.data.dialect != null && sqlFormatter.supportedDialects.includes(props.data.dialect) ?
|
||||||
? props.data.dialect
|
props.data.dialect
|
||||||
: 'sql',
|
: 'sql',
|
||||||
)
|
)
|
||||||
const formatted = computed(() => {
|
const formatted = computed(() => {
|
||||||
if (props.data.error != null || props.data.code == null) {
|
if (props.data.error != null || props.data.code == null) {
|
||||||
|
@ -135,9 +135,8 @@ const SCALE_TO_D3_SCALE: Record<ScaleType, () => d3.ScaleContinuousNumeric<numbe
|
|||||||
|
|
||||||
const data = computed<Data>(() => {
|
const data = computed<Data>(() => {
|
||||||
let rawData = props.data
|
let rawData = props.data
|
||||||
const unfilteredData = Array.isArray(rawData)
|
const unfilteredData =
|
||||||
? rawData.map((y, index) => ({ x: index, y }))
|
Array.isArray(rawData) ? rawData.map((y, index) => ({ x: index, y })) : rawData.data ?? []
|
||||||
: rawData.data ?? []
|
|
||||||
const data: Point[] = unfilteredData.filter(
|
const data: Point[] = unfilteredData.filter(
|
||||||
(point) =>
|
(point) =>
|
||||||
typeof point.x === 'number' &&
|
typeof point.x === 'number' &&
|
||||||
@ -203,15 +202,15 @@ const margin = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
const width = computed(() =>
|
const width = computed(() =>
|
||||||
config.fullscreen
|
config.fullscreen ?
|
||||||
? containerNode.value?.parentElement?.clientWidth ?? 0
|
containerNode.value?.parentElement?.clientWidth ?? 0
|
||||||
: Math.max(config.width ?? 0, config.nodeSize.x),
|
: Math.max(config.width ?? 0, config.nodeSize.x),
|
||||||
)
|
)
|
||||||
|
|
||||||
const height = computed(() =>
|
const height = computed(() =>
|
||||||
config.fullscreen
|
config.fullscreen ?
|
||||||
? containerNode.value?.parentElement?.clientHeight ?? 0
|
containerNode.value?.parentElement?.clientHeight ?? 0
|
||||||
: config.height ?? (config.nodeSize.x * 3) / 4,
|
: config.height ?? (config.nodeSize.x * 3) / 4,
|
||||||
)
|
)
|
||||||
|
|
||||||
const boxWidth = computed(() => Math.max(0, width.value - margin.value.left - margin.value.right))
|
const boxWidth = computed(() => Math.max(0, width.value - margin.value.left - margin.value.right))
|
||||||
@ -291,7 +290,9 @@ const zoom = computed(() =>
|
|||||||
const medDelta = 0.05
|
const medDelta = 0.05
|
||||||
const maxDelta = 1
|
const maxDelta = 1
|
||||||
const wheelSpeedMultiplier =
|
const wheelSpeedMultiplier =
|
||||||
event.deltaMode === 1 ? medDelta : event.deltaMode ? maxDelta : minDelta
|
event.deltaMode === 1 ? medDelta
|
||||||
|
: event.deltaMode ? maxDelta
|
||||||
|
: minDelta
|
||||||
return -event.deltaY * wheelSpeedMultiplier
|
return -event.deltaY * wheelSpeedMultiplier
|
||||||
})
|
})
|
||||||
.scaleExtent(ZOOM_EXTENT)
|
.scaleExtent(ZOOM_EXTENT)
|
||||||
|
@ -296,11 +296,9 @@ watchEffect(() => {
|
|||||||
const dataHeader = ('header' in data_ ? data_.header : [])?.map(toField) ?? []
|
const dataHeader = ('header' in data_ ? data_.header : [])?.map(toField) ?? []
|
||||||
columnDefs = [...indicesHeader, ...dataHeader]
|
columnDefs = [...indicesHeader, ...dataHeader]
|
||||||
const rows =
|
const rows =
|
||||||
data_.data && data_.data.length > 0
|
data_.data && data_.data.length > 0 ? data_.data[0]?.length ?? 0
|
||||||
? data_.data[0]?.length ?? 0
|
: data_.indices && data_.indices.length > 0 ? data_.indices[0]?.length ?? 0
|
||||||
: data_.indices && data_.indices.length > 0
|
: 0
|
||||||
? data_.indices[0]?.length ?? 0
|
|
||||||
: 0
|
|
||||||
rowData = Array.from({ length: rows }, (_, i) => {
|
rowData = Array.from({ length: rows }, (_, i) => {
|
||||||
const shift = data_.indices ? data_.indices.length : 0
|
const shift = data_.indices ? data_.indices.length : 0
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
82
app/gui2/src/components/widgets/AutoSizedInput.vue
Normal file
82
app/gui2/src/components/widgets/AutoSizedInput.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAutoBlur } from '@/util/autoBlur'
|
||||||
|
import { getTextWidthByFont } from '@/util/measurement'
|
||||||
|
import { computed, ref, watch, type StyleValue } from 'vue'
|
||||||
|
|
||||||
|
const [model, modifiers] = defineModel<string>()
|
||||||
|
const props = defineProps<{ autoSelect?: boolean }>()
|
||||||
|
|
||||||
|
const innerModel = modifiers.lazy ? ref(model.value) : model
|
||||||
|
if (modifiers.lazy) watch(model, (newVal) => (innerModel.value = newVal))
|
||||||
|
const onChange = modifiers.lazy ? () => (model.value = innerModel.value) : undefined
|
||||||
|
|
||||||
|
const inputNode = ref<HTMLInputElement>()
|
||||||
|
useAutoBlur(inputNode)
|
||||||
|
function onFocus() {
|
||||||
|
if (props.autoSelect) {
|
||||||
|
inputNode.value?.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssFont = computed(() => {
|
||||||
|
if (inputNode.value == null) return ''
|
||||||
|
let style = window.getComputedStyle(inputNode.value)
|
||||||
|
return style.font
|
||||||
|
})
|
||||||
|
|
||||||
|
const getTextWidth = (text: string) => getTextWidthByFont(text, cssFont.value)
|
||||||
|
const inputWidth = computed(() => getTextWidth(`${innerModel.value}`))
|
||||||
|
const inputStyle = computed<StyleValue>(() => ({ width: `${inputWidth.value}px` }))
|
||||||
|
|
||||||
|
function onEnterDown() {
|
||||||
|
inputNode.value?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
inputWidth,
|
||||||
|
getTextWidth,
|
||||||
|
select: () => inputNode.value?.select(),
|
||||||
|
focus: () => inputNode.value?.focus(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
ref="inputNode"
|
||||||
|
v-model="innerModel"
|
||||||
|
class="AutoSizedInput"
|
||||||
|
:style="inputStyle"
|
||||||
|
@keydown.backspace.stop
|
||||||
|
@keydown.delete.stop
|
||||||
|
@keydown.enter.stop="onEnterDown"
|
||||||
|
@change="onChange"
|
||||||
|
@focus="onFocus"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.AutoSizedInput {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 171.5%;
|
||||||
|
height: 24px;
|
||||||
|
appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
cursor: default;
|
||||||
|
user-select: all;
|
||||||
|
box-sizing: content-box;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -21,10 +21,18 @@ const sortedValuesAndIndices = computed(() => {
|
|||||||
])
|
])
|
||||||
switch (sortDirection.value) {
|
switch (sortDirection.value) {
|
||||||
case SortDirection.ascending: {
|
case SortDirection.ascending: {
|
||||||
return valuesAndIndices.sort((a, b) => (a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0))
|
return valuesAndIndices.sort((a, b) =>
|
||||||
|
a[0] > b[0] ? 1
|
||||||
|
: a[0] < b[0] ? -1
|
||||||
|
: 0,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case SortDirection.descending: {
|
case SortDirection.descending: {
|
||||||
return valuesAndIndices.sort((a, b) => (a[0] > b[0] ? -1 : a[0] < b[0] ? 1 : 0))
|
return valuesAndIndices.sort((a, b) =>
|
||||||
|
a[0] > b[0] ? -1
|
||||||
|
: a[0] < b[0] ? 1
|
||||||
|
: 0,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
case SortDirection.none:
|
case SortDirection.none:
|
||||||
default: {
|
default: {
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useResizeObserver } from '@/composables/events'
|
|
||||||
import { escape, unescape } from '@/util/ast/text'
|
|
||||||
import { blurIfNecessary } from '@/util/autoBlur'
|
|
||||||
import { getTextWidthByFont } from '@/util/measurement'
|
|
||||||
import { computed, ref, watch, type StyleValue } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: string
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits<{ 'update:modelValue': [modelValue: string] }>()
|
|
||||||
|
|
||||||
// Edited value reflects the `modelValue`, but does not update it until the user defocuses the field.
|
|
||||||
const editedValue = ref(props.modelValue)
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
editedValue.value = newValue
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const inputNode = ref<HTMLInputElement>()
|
|
||||||
const inputSize = useResizeObserver(inputNode)
|
|
||||||
const inputMeasurements = computed(() => {
|
|
||||||
if (inputNode.value == null) return { availableWidth: 0, font: '' }
|
|
||||||
let style = window.getComputedStyle(inputNode.value)
|
|
||||||
let availableWidth =
|
|
||||||
inputSize.value.x - (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight))
|
|
||||||
return { availableWidth, font: style.font }
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputStyle = computed<StyleValue>(() => {
|
|
||||||
if (inputNode.value == null) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const value = `${editedValue.value}`
|
|
||||||
const measurements = inputMeasurements.value
|
|
||||||
const width = getTextWidthByFont(value, measurements.font)
|
|
||||||
return {
|
|
||||||
width: `${width}px`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** To prevent other elements from stealing mouse events (which breaks blur),
|
|
||||||
* we instead setup our own `pointerdown` handler while the input is focused.
|
|
||||||
* Any click outside of the input field causes `blur`.
|
|
||||||
* We don’t want to `useAutoBlur` here, because it would require a separate `pointerdown` handler per input widget.
|
|
||||||
* Instead we setup a single handler for the currently focused widget only, and thus safe performance. */
|
|
||||||
function setupAutoBlur() {
|
|
||||||
const options = { capture: true }
|
|
||||||
function callback(event: MouseEvent) {
|
|
||||||
if (blurIfNecessary(inputNode, event)) {
|
|
||||||
window.removeEventListener('pointerdown', callback, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('pointerdown', callback, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
const separators = /(^('''|"""|['"]))|(('''|"""|['"])$)/g
|
|
||||||
/** Display the value in a more human-readable form for easier editing. */
|
|
||||||
function prepareForEditing() {
|
|
||||||
editedValue.value = unescape(editedValue.value.replace(separators, ''))
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
setupAutoBlur()
|
|
||||||
prepareForEditing()
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapedValue = computed(() => `'${escape(editedValue.value)}'`)
|
|
||||||
|
|
||||||
function blur() {
|
|
||||||
emit('update:modelValue', escapedValue.value)
|
|
||||||
editedValue.value = props.modelValue
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="EnsoTextInputWidget"
|
|
||||||
@pointerdown.stop="() => inputNode?.focus()"
|
|
||||||
@keydown.backspace.stop
|
|
||||||
@keydown.delete.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref="inputNode"
|
|
||||||
v-model="editedValue"
|
|
||||||
class="value"
|
|
||||||
:style="inputStyle"
|
|
||||||
@keydown.enter.stop="($event.target as HTMLInputElement).blur()"
|
|
||||||
@focus="focus"
|
|
||||||
@blur="blur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.EnsoTextInputWidget {
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
background: var(--color-widget);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
min-width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 171.5%;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0px 4px;
|
|
||||||
appearance: textfield;
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
background-color: rgba(255, 255, 255, 15%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -46,9 +46,9 @@ const dragMetaMimePrefix = 'application/x-enso-list-item;item='
|
|||||||
|
|
||||||
function stringToHex(str: string) {
|
function stringToHex(str: string) {
|
||||||
return Array.from(str, (c) =>
|
return Array.from(str, (c) =>
|
||||||
c.charCodeAt(0) < 128
|
c.charCodeAt(0) < 128 ?
|
||||||
? c.charCodeAt(0).toString(16)
|
c.charCodeAt(0).toString(16)
|
||||||
: encodeURIComponent(c).replace(/%/g, '').toLowerCase(),
|
: encodeURIComponent(c).replace(/%/g, '').toLowerCase(),
|
||||||
).join('')
|
).join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PointerButtonMask, usePointer, useResizeObserver } from '@/composables/events'
|
import { PointerButtonMask, usePointer } from '@/composables/events'
|
||||||
import { blurIfNecessary } from '@/util/autoBlur'
|
import { computed, ref, watch, type ComponentInstance, type StyleValue } from 'vue'
|
||||||
import { getTextWidthByFont } from '@/util/measurement'
|
import AutoSizedInput from './AutoSizedInput.vue'
|
||||||
import { computed, ref, watch, type StyleValue } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: number | string
|
modelValue: number | string
|
||||||
@ -12,11 +11,11 @@ const emit = defineEmits<{ 'update:modelValue': [modelValue: number | string] }>
|
|||||||
|
|
||||||
const inputFieldActive = ref(false)
|
const inputFieldActive = ref(false)
|
||||||
// Edited value reflects the `modelValue`, but does not update it until the user defocuses the field.
|
// Edited value reflects the `modelValue`, but does not update it until the user defocuses the field.
|
||||||
const editedValue = ref(props.modelValue)
|
const editedValue = ref(`${props.modelValue}`)
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
editedValue.value = newValue
|
editedValue.value = `${newValue}`
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const SLIDER_INPUT_THRESHOLD = 4.0
|
const SLIDER_INPUT_THRESHOLD = 4.0
|
||||||
@ -24,31 +23,27 @@ const SLIDER_INPUT_THRESHOLD = 4.0
|
|||||||
const dragPointer = usePointer(
|
const dragPointer = usePointer(
|
||||||
(position, event, eventType) => {
|
(position, event, eventType) => {
|
||||||
const slider = event.target
|
const slider = event.target
|
||||||
if (!(slider instanceof HTMLElement)) {
|
if (!(slider instanceof HTMLElement)) return false
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'stop' && Math.abs(position.relative.x) < SLIDER_INPUT_THRESHOLD) {
|
if (eventType === 'stop' && Math.abs(position.relative.x) < SLIDER_INPUT_THRESHOLD) {
|
||||||
inputNode.value?.focus()
|
event.stopImmediatePropagation()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'start') {
|
if (eventType === 'start') {
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputFieldActive.value || props.limits == null) return
|
if (inputFieldActive.value || props.limits == null) return false
|
||||||
|
|
||||||
const { min, max } = props.limits
|
const { min, max } = props.limits
|
||||||
const rect = slider.getBoundingClientRect()
|
const rect = slider.getBoundingClientRect()
|
||||||
const fractionRaw = (position.absolute.x - rect.left) / (rect.right - rect.left)
|
const fractionRaw = (position.absolute.x - rect.left) / (rect.right - rect.left)
|
||||||
const fraction = Math.max(0, Math.min(1, fractionRaw))
|
const fraction = Math.max(0, Math.min(1, fractionRaw))
|
||||||
const newValue = min + Math.round(fraction * (max - min))
|
const newValue = min + Math.round(fraction * (max - min))
|
||||||
editedValue.value = newValue
|
editedValue.value = `${newValue}`
|
||||||
if (eventType === 'stop') {
|
if (eventType === 'stop') emitUpdate()
|
||||||
emit('update:modelValue', editedValue.value)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
PointerButtonMask.Main,
|
PointerButtonMask.Main,
|
||||||
(event) => !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey,
|
(event) => !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey,
|
||||||
@ -62,132 +57,80 @@ const sliderWidth = computed(() => {
|
|||||||
}%`
|
}%`
|
||||||
})
|
})
|
||||||
|
|
||||||
const inputNode = ref<HTMLInputElement>()
|
const inputComponent = ref<ComponentInstance<typeof AutoSizedInput>>()
|
||||||
const inputSize = useResizeObserver(inputNode)
|
const MIN_CONTENT_WIDTH = 56
|
||||||
const inputMeasurements = computed(() => {
|
|
||||||
if (inputNode.value == null) return { availableWidth: 0, font: '' }
|
|
||||||
let style = window.getComputedStyle(inputNode.value)
|
|
||||||
let availableWidth =
|
|
||||||
inputSize.value.x - (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight))
|
|
||||||
return { availableWidth, font: style.font }
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputStyle = computed<StyleValue>(() => {
|
const inputStyle = computed<StyleValue>(() => {
|
||||||
if (inputNode.value == null) {
|
const value = `${editedValue.value}`
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const value = `${props.modelValue}`
|
|
||||||
const dotIdx = value.indexOf('.')
|
const dotIdx = value.indexOf('.')
|
||||||
let indent = 0
|
let indent = 0
|
||||||
if (dotIdx >= 0) {
|
if (dotIdx >= 0 && inputComponent.value != null) {
|
||||||
|
const { inputWidth, getTextWidth } = inputComponent.value
|
||||||
const textBefore = value.slice(0, dotIdx)
|
const textBefore = value.slice(0, dotIdx)
|
||||||
const textAfter = value.slice(dotIdx + 1)
|
const textAfter = value.slice(dotIdx + 1)
|
||||||
|
const availableWidth = Math.max(inputWidth, MIN_CONTENT_WIDTH)
|
||||||
const measurements = inputMeasurements.value
|
const beforeDot = getTextWidth(textBefore)
|
||||||
const total = getTextWidthByFont(value, measurements.font)
|
const afterDot = getTextWidth(textAfter)
|
||||||
const beforeDot = getTextWidthByFont(textBefore, measurements.font)
|
const blankSpace = Math.max(availableWidth - inputWidth, 0)
|
||||||
const afterDot = getTextWidthByFont(textAfter, measurements.font)
|
|
||||||
const blankSpace = Math.max(measurements.availableWidth - total, 0)
|
|
||||||
indent = Math.min(Math.max(-blankSpace, afterDot - beforeDot), blankSpace)
|
indent = Math.min(Math.max(-blankSpace, afterDot - beforeDot), blankSpace)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
textIndent: `${indent}px`,
|
textIndent: `${indent}px`,
|
||||||
|
// Note: The input element here uses `box-sizing: content-box;`.
|
||||||
|
minWidth: `${MIN_CONTENT_WIDTH}px`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function blur() {
|
function emitUpdate() {
|
||||||
inputFieldActive.value = false
|
if (`${props.modelValue}` !== editedValue.value) {
|
||||||
emit('update:modelValue', editedValue.value)
|
emit('update:modelValue', editedValue.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** To prevent other elements from stealing mouse events (which breaks blur),
|
function blur() {
|
||||||
* we instead setup our own `pointerdown` handler while the input is focused.
|
inputFieldActive.value = false
|
||||||
* Any click outside of the input field causes `blur`.
|
emitUpdate()
|
||||||
* We don’t want to `useAutoBlur` here, because it would require a separate `pointerdown` handler per input widget.
|
|
||||||
* Instead we setup a single handler for the currently focused widget only, and thus safe performance. */
|
|
||||||
function setupAutoBlur() {
|
|
||||||
const options = { capture: true }
|
|
||||||
function callback(event: MouseEvent) {
|
|
||||||
if (blurIfNecessary(inputNode, event)) {
|
|
||||||
window.removeEventListener('pointerdown', callback, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('pointerdown', callback, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
inputNode.value?.select()
|
|
||||||
inputFieldActive.value = true
|
inputFieldActive.value = true
|
||||||
setupAutoBlur()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<label class="NumericInputWidget">
|
||||||
class="NumericInputWidget"
|
<div v-if="props.limits != null" class="slider" :style="{ width: sliderWidth }"></div>
|
||||||
v-on="dragPointer.events"
|
<AutoSizedInput
|
||||||
@keydown.backspace.stop
|
ref="inputComponent"
|
||||||
@keydown.delete.stop
|
|
||||||
>
|
|
||||||
<div v-if="props.limits != null" class="fraction" :style="{ width: sliderWidth }"></div>
|
|
||||||
<input
|
|
||||||
ref="inputNode"
|
|
||||||
v-model="editedValue"
|
v-model="editedValue"
|
||||||
class="value"
|
autoSelect
|
||||||
:style="inputStyle"
|
:style="inputStyle"
|
||||||
@keydown.enter.stop="($event.target as HTMLInputElement).blur()"
|
v-on="dragPointer.events"
|
||||||
@blur="blur"
|
@blur="blur"
|
||||||
@focus="focus"
|
@focus="focus"
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.NumericInputWidget {
|
.NumericInputWidget {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
.AutoSizedInput {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
justify-content: space-around;
|
|
||||||
background: var(--color-widget);
|
background: var(--color-widget);
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
width: 56px;
|
padding: 0px 4px;
|
||||||
|
&:focus {
|
||||||
|
background: var(--color-widget-focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fraction {
|
.slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: var(--color-widget);
|
background: var(--color-widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 0;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 171.5%;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0px 4px;
|
|
||||||
appearance: textfield;
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
background-color: rgba(255, 255, 255, 15%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { Opt } from '@/util/data/opt'
|
import type { Opt } from '@/util/data/opt'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import type { VueInstance } from '@vueuse/core'
|
import { type VueInstance } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
onScopeDispose,
|
onScopeDispose,
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
watchEffect,
|
watchEffect,
|
||||||
type Ref,
|
type Ref,
|
||||||
|
type ShallowRef,
|
||||||
type WatchSource,
|
type WatchSource,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
@ -139,6 +140,45 @@ export function unrefElement(
|
|||||||
return (plain as VueInstance)?.$el ?? plain
|
return (plain as VueInstance)?.$el ?? plain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResizeObserverData {
|
||||||
|
refCount: number
|
||||||
|
boundRectUsers: number
|
||||||
|
contentRect: ShallowRef<Vec2>
|
||||||
|
boundRect: ShallowRef<Vec2>
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserverData = new WeakMap<Element, ResizeObserverData>()
|
||||||
|
function getOrCreateObserverData(element: Element): ResizeObserverData {
|
||||||
|
const existingData = resizeObserverData.get(element)
|
||||||
|
if (existingData) return existingData
|
||||||
|
const data: ResizeObserverData = {
|
||||||
|
refCount: 0,
|
||||||
|
boundRectUsers: 0,
|
||||||
|
contentRect: shallowRef<Vec2>(Vec2.Zero),
|
||||||
|
boundRect: shallowRef<Vec2>(Vec2.Zero),
|
||||||
|
}
|
||||||
|
resizeObserverData.set(element, data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedResizeObserver: ResizeObserver | undefined =
|
||||||
|
typeof ResizeObserver === 'undefined' ? undefined : (
|
||||||
|
new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const data = resizeObserverData.get(entry.target)
|
||||||
|
if (data != null) {
|
||||||
|
if (entry.contentRect != null) {
|
||||||
|
data.contentRect.value = new Vec2(entry.contentRect.width, entry.contentRect.height)
|
||||||
|
}
|
||||||
|
if (data.boundRectUsers > 0) {
|
||||||
|
const rect = entry.target.getBoundingClientRect()
|
||||||
|
data.boundRect.value = new Vec2(rect.width, rect.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get DOM node size and keep it up to date.
|
* Get DOM node size and keep it up to date.
|
||||||
*
|
*
|
||||||
@ -153,8 +193,8 @@ export function useResizeObserver(
|
|||||||
elementRef: Ref<Element | undefined | null | VueInstance>,
|
elementRef: Ref<Element | undefined | null | VueInstance>,
|
||||||
useContentRect = true,
|
useContentRect = true,
|
||||||
): Ref<Vec2> {
|
): Ref<Vec2> {
|
||||||
const sizeRef = shallowRef<Vec2>(Vec2.Zero)
|
if (!sharedResizeObserver) {
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
const sizeRef = shallowRef<Vec2>(Vec2.Zero)
|
||||||
// Fallback implementation for browsers/test environment that do not support ResizeObserver:
|
// Fallback implementation for browsers/test environment that do not support ResizeObserver:
|
||||||
// Grab the size of the element every time the ref is assigned, or when the page is resized.
|
// Grab the size of the element every time the ref is assigned, or when the page is resized.
|
||||||
function refreshSize() {
|
function refreshSize() {
|
||||||
@ -168,36 +208,36 @@ export function useResizeObserver(
|
|||||||
useEvent(window, 'resize', refreshSize)
|
useEvent(window, 'resize', refreshSize)
|
||||||
return sizeRef
|
return sizeRef
|
||||||
}
|
}
|
||||||
const observer = new ResizeObserver((entries) => {
|
const observer = sharedResizeObserver
|
||||||
let rect: { width: number; height: number } | null = null
|
|
||||||
const target = unrefElement(elementRef)
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.target === target) {
|
|
||||||
if (useContentRect) {
|
|
||||||
rect = entry.contentRect
|
|
||||||
} else {
|
|
||||||
rect = entry.target.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rect != null) {
|
|
||||||
sizeRef.value = new Vec2(rect.width, rect.height)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watchEffect((onCleanup) => {
|
watchEffect((onCleanup) => {
|
||||||
const element = unrefElement(elementRef)
|
const element = unrefElement(elementRef)
|
||||||
if (element != null) {
|
if (element != null) {
|
||||||
observer.observe(element)
|
const data = getOrCreateObserverData(element)
|
||||||
|
if (data.refCount === 0) observer.observe(element)
|
||||||
|
data.refCount += 1
|
||||||
|
if (!useContentRect) {
|
||||||
|
if (data.boundRectUsers === 0) {
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
data.boundRect.value = new Vec2(rect.width, rect.height)
|
||||||
|
}
|
||||||
|
data.boundRectUsers += 1
|
||||||
|
}
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (elementRef.value != null) {
|
if (elementRef.value != null) {
|
||||||
observer.unobserve(element)
|
data.refCount -= 1
|
||||||
|
if (!useContentRect) data.boundRectUsers -= 1
|
||||||
|
if (data.refCount === 0) observer.unobserve(element)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sizeRef
|
return computed(() => {
|
||||||
|
const element = unrefElement(elementRef)
|
||||||
|
if (element == null) return Vec2.Zero
|
||||||
|
const data = getOrCreateObserverData(element)
|
||||||
|
return useContentRect ? data.contentRect.value : data.boundRect.value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventPosition {
|
export interface EventPosition {
|
||||||
@ -240,7 +280,7 @@ export const enum PointerButtonMask {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function usePointer(
|
export function usePointer(
|
||||||
handler: (pos: EventPosition, event: PointerEvent, eventType: PointerEventType) => void,
|
handler: (pos: EventPosition, event: PointerEvent, eventType: PointerEventType) => void | boolean,
|
||||||
requiredButtonMask: number = PointerButtonMask.Main,
|
requiredButtonMask: number = PointerButtonMask.Main,
|
||||||
predicate?: (e: PointerEvent) => boolean,
|
predicate?: (e: PointerEvent) => boolean,
|
||||||
) {
|
) {
|
||||||
@ -256,18 +296,22 @@ export function usePointer(
|
|||||||
trackedElement?.releasePointerCapture(trackedPointer.value)
|
trackedElement?.releasePointerCapture(trackedPointer.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
trackedPointer.value = null
|
|
||||||
|
|
||||||
if (trackedElement != null && initialGrabPos != null && lastPos != null) {
|
if (trackedElement != null && initialGrabPos != null && lastPos != null) {
|
||||||
handler(computePosition(e, initialGrabPos, lastPos), e, 'stop')
|
if (handler(computePosition(e, initialGrabPos, lastPos), e, 'stop') !== false) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
lastPos = null
|
lastPos = null
|
||||||
trackedElement = null
|
trackedElement = null
|
||||||
}
|
}
|
||||||
|
trackedPointer.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function doMove(e: PointerEvent) {
|
function doMove(e: PointerEvent) {
|
||||||
if (trackedElement != null && initialGrabPos != null && lastPos != null) {
|
if (trackedElement != null && initialGrabPos != null && lastPos != null) {
|
||||||
handler(computePosition(e, initialGrabPos, lastPos), e, 'move')
|
if (handler(computePosition(e, initialGrabPos, lastPos), e, 'move') !== false) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
lastPos = new Vec2(e.clientX, e.clientY)
|
lastPos = new Vec2(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,7 +324,6 @@ export function usePointer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trackedPointer.value == null && e.currentTarget instanceof Element) {
|
if (trackedPointer.value == null && e.currentTarget instanceof Element) {
|
||||||
e.preventDefault()
|
|
||||||
trackedPointer.value = e.pointerId
|
trackedPointer.value = e.pointerId
|
||||||
// This is mostly SAFE, as virtually all `Element`s also extend `GlobalEventHandlers`.
|
// This is mostly SAFE, as virtually all `Element`s also extend `GlobalEventHandlers`.
|
||||||
trackedElement = e.currentTarget as Element & GlobalEventHandlers
|
trackedElement = e.currentTarget as Element & GlobalEventHandlers
|
||||||
@ -288,21 +331,21 @@ export function usePointer(
|
|||||||
trackedElement.setPointerCapture?.(e.pointerId)
|
trackedElement.setPointerCapture?.(e.pointerId)
|
||||||
initialGrabPos = new Vec2(e.clientX, e.clientY)
|
initialGrabPos = new Vec2(e.clientX, e.clientY)
|
||||||
lastPos = initialGrabPos
|
lastPos = initialGrabPos
|
||||||
handler(computePosition(e, initialGrabPos, lastPos), e, 'start')
|
if (handler(computePosition(e, initialGrabPos, lastPos), e, 'start') !== false) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pointerup(e: PointerEvent) {
|
pointerup(e: PointerEvent) {
|
||||||
if (trackedPointer.value !== e.pointerId) {
|
if (trackedPointer.value !== e.pointerId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
|
||||||
doStop(e)
|
doStop(e)
|
||||||
},
|
},
|
||||||
pointermove(e: PointerEvent) {
|
pointermove(e: PointerEvent) {
|
||||||
if (trackedPointer.value !== e.pointerId) {
|
if (trackedPointer.value !== e.pointerId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
|
||||||
// handle release of all masked buttons as stop
|
// handle release of all masked buttons as stop
|
||||||
if ((e.buttons & requiredButtonMask) !== 0) {
|
if ((e.buttons & requiredButtonMask) !== 0) {
|
||||||
doMove(e)
|
doMove(e)
|
||||||
|
@ -4,7 +4,7 @@ import { useApproach } from '@/composables/animation'
|
|||||||
import { PointerButtonMask, useEvent, usePointer, useResizeObserver } from '@/composables/events'
|
import { PointerButtonMask, useEvent, usePointer, useResizeObserver } from '@/composables/events'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { computed, proxyRefs, ref, type Ref } from 'vue'
|
import { computed, proxyRefs, shallowRef, type Ref } from 'vue'
|
||||||
|
|
||||||
function elemRect(target: Element | undefined): Rect {
|
function elemRect(target: Element | undefined): Rect {
|
||||||
if (target != null && target instanceof Element) {
|
if (target != null && target instanceof Element) {
|
||||||
@ -17,7 +17,7 @@ function elemRect(target: Element | undefined): Rect {
|
|||||||
export type NavigatorComposable = ReturnType<typeof useNavigator>
|
export type NavigatorComposable = ReturnType<typeof useNavigator>
|
||||||
export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
||||||
const size = useResizeObserver(viewportNode)
|
const size = useResizeObserver(viewportNode)
|
||||||
const targetCenter = ref<Vec2>(Vec2.Zero)
|
const targetCenter = shallowRef<Vec2>(Vec2.Zero)
|
||||||
const targetX = computed(() => targetCenter.value.x)
|
const targetX = computed(() => targetCenter.value.x)
|
||||||
const targetY = computed(() => targetCenter.value.y)
|
const targetY = computed(() => targetCenter.value.y)
|
||||||
const centerX = useApproach(targetX)
|
const centerX = useApproach(targetX)
|
||||||
@ -32,7 +32,7 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
centerY.value = value.y
|
centerY.value = value.y
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const targetScale = ref(1)
|
const targetScale = shallowRef(1)
|
||||||
const animatedScale = useApproach(targetScale)
|
const animatedScale = useApproach(targetScale)
|
||||||
const scale = computed({
|
const scale = computed({
|
||||||
get() {
|
get() {
|
||||||
@ -140,7 +140,7 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
|
|
||||||
let isPointerDown = false
|
let isPointerDown = false
|
||||||
let scrolledThisFrame = false
|
let scrolledThisFrame = false
|
||||||
const eventMousePos = ref<Vec2 | null>(null)
|
const eventMousePos = shallowRef<Vec2 | null>(null)
|
||||||
let eventTargetScrollPos: Vec2 | null = null
|
let eventTargetScrollPos: Vec2 | null = null
|
||||||
const sceneMousePos = computed(() =>
|
const sceneMousePos = computed(() =>
|
||||||
eventMousePos.value ? clientToScenePos(eventMousePos.value) : null,
|
eventMousePos.value ? clientToScenePos(eventMousePos.value) : null,
|
||||||
|
@ -6,7 +6,7 @@ import { type NodeId } from '@/stores/graph'
|
|||||||
import type { Rect } from '@/util/data/rect'
|
import type { Rect } from '@/util/data/rect'
|
||||||
import { intersectionSize } from '@/util/data/set'
|
import { intersectionSize } from '@/util/data/set'
|
||||||
import type { Vec2 } from '@/util/data/vec2'
|
import type { Vec2 } from '@/util/data/vec2'
|
||||||
import { computed, proxyRefs, reactive, ref, shallowRef } from 'vue'
|
import { computed, proxyRefs, ref, shallowReactive, shallowRef } from 'vue'
|
||||||
|
|
||||||
export type SelectionComposable<T> = ReturnType<typeof useSelection<T>>
|
export type SelectionComposable<T> = ReturnType<typeof useSelection<T>>
|
||||||
export function useSelection<T>(
|
export function useSelection<T>(
|
||||||
@ -20,7 +20,7 @@ export function useSelection<T>(
|
|||||||
) {
|
) {
|
||||||
const anchor = shallowRef<Vec2>()
|
const anchor = shallowRef<Vec2>()
|
||||||
const initiallySelected = new Set<T>()
|
const initiallySelected = new Set<T>()
|
||||||
const selected = reactive(new Set<T>())
|
const selected = shallowReactive(new Set<T>())
|
||||||
const hoveredNode = ref<NodeId>()
|
const hoveredNode = ref<NodeId>()
|
||||||
const hoveredPort = ref<PortId>()
|
const hoveredPort = ref<PortId>()
|
||||||
|
|
||||||
@ -28,11 +28,13 @@ export function useSelection<T>(
|
|||||||
if (event.target instanceof Element) {
|
if (event.target instanceof Element) {
|
||||||
const widgetPort = event.target.closest('.WidgetPort')
|
const widgetPort = event.target.closest('.WidgetPort')
|
||||||
hoveredPort.value =
|
hoveredPort.value =
|
||||||
widgetPort instanceof HTMLElement &&
|
(
|
||||||
'port' in widgetPort.dataset &&
|
widgetPort instanceof HTMLElement &&
|
||||||
typeof widgetPort.dataset.port === 'string'
|
'port' in widgetPort.dataset &&
|
||||||
? (widgetPort.dataset.port as PortId)
|
typeof widgetPort.dataset.port === 'string'
|
||||||
: undefined
|
) ?
|
||||||
|
(widgetPort.dataset.port as PortId)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -125,18 +127,19 @@ export function useSelection<T>(
|
|||||||
const pointer = usePointer((_pos, event, eventType) => {
|
const pointer = usePointer((_pos, event, eventType) => {
|
||||||
if (eventType === 'start') {
|
if (eventType === 'start') {
|
||||||
readInitiallySelected()
|
readInitiallySelected()
|
||||||
|
} else if (eventType === 'stop') {
|
||||||
|
if (anchor.value == null) {
|
||||||
|
// If there was no drag, we want to handle "clicking-off" selected nodes.
|
||||||
|
selectionEventHandler(event)
|
||||||
|
} else {
|
||||||
|
anchor.value = undefined
|
||||||
|
}
|
||||||
|
initiallySelected.clear()
|
||||||
} else if (pointer.dragging) {
|
} else if (pointer.dragging) {
|
||||||
if (anchor.value == null) {
|
if (anchor.value == null) {
|
||||||
anchor.value = navigator.sceneMousePos?.copy()
|
anchor.value = navigator.sceneMousePos?.copy()
|
||||||
}
|
}
|
||||||
selectionEventHandler(event)
|
selectionEventHandler(event)
|
||||||
} else if (eventType === 'stop') {
|
|
||||||
if (anchor.value == null) {
|
|
||||||
// If there was no drag, we want to handle "clicking-off" selected nodes.
|
|
||||||
selectionEventHandler(event)
|
|
||||||
}
|
|
||||||
anchor.value = undefined
|
|
||||||
initiallySelected.clear()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,9 +56,8 @@ export function createContextStore<F extends (...args: any[]) => any>(name: stri
|
|||||||
): ReturnType<F> {
|
): ReturnType<F> {
|
||||||
// Right now this function assumes that an array always represents the arguments to the factory.
|
// Right now this function assumes that an array always represents the arguments to the factory.
|
||||||
// If we ever need to mock an array as the context value, we'll worry about it then.
|
// If we ever need to mock an array as the context value, we'll worry about it then.
|
||||||
const constructed: ReturnType<F> = Array.isArray(valueOrArgs)
|
const constructed: ReturnType<F> =
|
||||||
? factory(...valueOrArgs)
|
Array.isArray(valueOrArgs) ? factory(...valueOrArgs) : valueOrArgs
|
||||||
: valueOrArgs
|
|
||||||
if (app != null) app.provide(provideKey, constructed)
|
if (app != null) app.provide(provideKey, constructed)
|
||||||
else provide(provideKey, constructed)
|
else provide(provideKey, constructed)
|
||||||
return constructed
|
return constructed
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createContextStore } from '@/providers'
|
import { createContextStore } from '@/providers'
|
||||||
import type { AstId } from '@/util/ast/abstract'
|
import type { AstId, TokenId } from '@/util/ast/abstract'
|
||||||
import { identity } from '@vueuse/core'
|
import { identity } from '@vueuse/core'
|
||||||
|
|
||||||
declare const portIdBrand: unique symbol
|
declare const portIdBrand: unique symbol
|
||||||
@ -7,7 +7,7 @@ declare const portIdBrand: unique symbol
|
|||||||
* Port identification. A port represents a fragment of code displayed/modified by the widget;
|
* Port identification. A port represents a fragment of code displayed/modified by the widget;
|
||||||
* usually Ast nodes, but other ids are also possible (like argument placeholders).
|
* usually Ast nodes, but other ids are also possible (like argument placeholders).
|
||||||
*/
|
*/
|
||||||
export type PortId = AstId | (string & { [portIdBrand]: never })
|
export type PortId = AstId | TokenId | (string & { [portIdBrand]: never })
|
||||||
|
|
||||||
interface PortInfo {
|
interface PortInfo {
|
||||||
portId: PortId
|
portId: PortId
|
||||||
|
@ -4,7 +4,7 @@ import type { WidgetConfiguration } from '@/providers/widgetRegistry/configurati
|
|||||||
import type { GraphDb } from '@/stores/graph/graphDatabase'
|
import type { GraphDb } from '@/stores/graph/graphDatabase'
|
||||||
import type { Typename } from '@/stores/suggestionDatabase/entry'
|
import type { Typename } from '@/stores/suggestionDatabase/entry'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
import { MutableModule, type TokenId } from '@/util/ast/abstract.ts'
|
import { MutableModule } from '@/util/ast/abstract.ts'
|
||||||
import { computed, shallowReactive, type Component, type PropType } from 'vue'
|
import { computed, shallowReactive, type Component, type PropType } from 'vue'
|
||||||
|
|
||||||
export type WidgetComponent<T extends WidgetInput> = Component<WidgetProps<T>>
|
export type WidgetComponent<T extends WidgetInput> = Component<WidgetProps<T>>
|
||||||
@ -92,7 +92,7 @@ export interface WidgetInput {
|
|||||||
*
|
*
|
||||||
* Also, used as usage key (see {@link usageKeyForInput})
|
* Also, used as usage key (see {@link usageKeyForInput})
|
||||||
*/
|
*/
|
||||||
portId: PortId | TokenId
|
portId: PortId
|
||||||
/**
|
/**
|
||||||
* An expected widget value. If Ast.Ast or Ast.Token, the widget represents an existing part of
|
* An expected widget value. If Ast.Ast or Ast.Token, the widget represents an existing part of
|
||||||
* code. If string, it may be e.g. a default value of an argument.
|
* code. If string, it may be e.g. a default value of an argument.
|
||||||
@ -171,12 +171,10 @@ export function widgetProps<T extends WidgetInput>(_def: WidgetDefinition<T>) {
|
|||||||
type InputMatcherFn<T extends WidgetInput> = (input: WidgetInput) => input is T
|
type InputMatcherFn<T extends WidgetInput> = (input: WidgetInput) => input is T
|
||||||
type InputMatcher<T extends WidgetInput> = keyof WidgetInput | InputMatcherFn<T>
|
type InputMatcher<T extends WidgetInput> = keyof WidgetInput | InputMatcherFn<T>
|
||||||
|
|
||||||
type InputTy<M> = M extends (infer T)[]
|
type InputTy<M> =
|
||||||
? InputTy<T>
|
M extends (infer T)[] ? InputTy<T>
|
||||||
: M extends InputMatcherFn<infer T>
|
: M extends InputMatcherFn<infer T> ? T
|
||||||
? T
|
: M extends keyof WidgetInput ? WidgetInput & Required<Pick<WidgetInput, M>>
|
||||||
: M extends keyof WidgetInput
|
|
||||||
? WidgetInput & Required<Pick<WidgetInput, M>>
|
|
||||||
: never
|
: never
|
||||||
|
|
||||||
export interface WidgetOptions<T extends WidgetInput> {
|
export interface WidgetOptions<T extends WidgetInput> {
|
||||||
|
@ -169,8 +169,8 @@ export function requiredImports(
|
|||||||
]
|
]
|
||||||
switch (entry.kind) {
|
switch (entry.kind) {
|
||||||
case SuggestionKind.Module:
|
case SuggestionKind.Module:
|
||||||
return entry.reexportedIn
|
return entry.reexportedIn ?
|
||||||
? unqualifiedImport(entry.reexportedIn)
|
unqualifiedImport(entry.reexportedIn)
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
kind: 'Qualified',
|
kind: 'Qualified',
|
||||||
@ -181,11 +181,11 @@ export function requiredImports(
|
|||||||
return unqualifiedImport(entry.reexportedIn ? entry.reexportedIn : entry.definedIn)
|
return unqualifiedImport(entry.reexportedIn ? entry.reexportedIn : entry.definedIn)
|
||||||
case SuggestionKind.Constructor:
|
case SuggestionKind.Constructor:
|
||||||
if (directConImport) {
|
if (directConImport) {
|
||||||
return entry.reexportedIn
|
return (
|
||||||
? unqualifiedImport(entry.reexportedIn)
|
entry.reexportedIn ? unqualifiedImport(entry.reexportedIn)
|
||||||
: entry.memberOf
|
: entry.memberOf ? unqualifiedImport(entry.memberOf)
|
||||||
? unqualifiedImport(entry.memberOf)
|
|
||||||
: []
|
: []
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const selfType = selfTypeEntry(db, entry)
|
const selfType = selfTypeEntry(db, entry)
|
||||||
return selfType ? requiredImports(db, selfType) : []
|
return selfType ? requiredImports(db, selfType) : []
|
||||||
@ -254,11 +254,9 @@ export function requiredImportEquals(left: RequiredImport, right: RequiredImport
|
|||||||
/** Check if `existing` import statement covers `required`. */
|
/** Check if `existing` import statement covers `required`. */
|
||||||
export function covers(existing: Import, required: RequiredImport): boolean {
|
export function covers(existing: Import, required: RequiredImport): boolean {
|
||||||
const [parent, name] =
|
const [parent, name] =
|
||||||
required.kind === 'Qualified'
|
required.kind === 'Qualified' ? qnSplit(required.module)
|
||||||
? qnSplit(required.module)
|
: required.kind === 'Unqualified' ? [required.from, required.import]
|
||||||
: required.kind === 'Unqualified'
|
: [undefined, '']
|
||||||
? [required.from, required.import]
|
|
||||||
: [undefined, '']
|
|
||||||
const directlyImported =
|
const directlyImported =
|
||||||
required.kind === 'Qualified' &&
|
required.kind === 'Qualified' &&
|
||||||
existing.imported.kind === 'Module' &&
|
existing.imported.kind === 'Module' &&
|
||||||
|
@ -34,7 +34,16 @@ import { SourceDocument } from 'shared/ast/sourceDocument'
|
|||||||
import type { ExpressionUpdate, StackItem } from 'shared/languageServerTypes'
|
import type { ExpressionUpdate, StackItem } from 'shared/languageServerTypes'
|
||||||
import type { LocalOrigin, SourceRangeKey, VisualizationMetadata } from 'shared/yjsModel'
|
import type { LocalOrigin, SourceRangeKey, VisualizationMetadata } from 'shared/yjsModel'
|
||||||
import { defaultLocalOrigin, sourceRangeKey, visMetadataEquals } from 'shared/yjsModel'
|
import { defaultLocalOrigin, sourceRangeKey, visMetadataEquals } from 'shared/yjsModel'
|
||||||
import { computed, markRaw, reactive, ref, toRef, watch, type ShallowRef } from 'vue'
|
import {
|
||||||
|
computed,
|
||||||
|
markRaw,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
shallowReactive,
|
||||||
|
toRef,
|
||||||
|
watch,
|
||||||
|
type ShallowRef,
|
||||||
|
} from 'vue'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Node,
|
Node,
|
||||||
@ -53,7 +62,9 @@ export class PortViewInstance {
|
|||||||
public rect: ShallowRef<Rect | undefined>,
|
public rect: ShallowRef<Rect | undefined>,
|
||||||
public nodeId: NodeId,
|
public nodeId: NodeId,
|
||||||
public onUpdate: (update: WidgetUpdate) => void,
|
public onUpdate: (update: WidgetUpdate) => void,
|
||||||
) {}
|
) {
|
||||||
|
markRaw(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGraphStore = defineStore('graph', () => {
|
export const useGraphStore = defineStore('graph', () => {
|
||||||
@ -77,7 +88,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
toRef(suggestionDb, 'groups'),
|
toRef(suggestionDb, 'groups'),
|
||||||
proj.computedValueRegistry,
|
proj.computedValueRegistry,
|
||||||
)
|
)
|
||||||
const portInstances = reactive(new Map<PortId, Set<PortViewInstance>>())
|
const portInstances = shallowReactive(new Map<PortId, Set<PortViewInstance>>())
|
||||||
const editedNodeInfo = ref<NodeEditInfo>()
|
const editedNodeInfo = ref<NodeEditInfo>()
|
||||||
const methodAst = ref<Ast.Function>()
|
const methodAst = ref<Ast.Function>()
|
||||||
|
|
||||||
@ -107,8 +118,12 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
function handleModuleUpdate(module: Module, moduleChanged: boolean, update: ModuleUpdate) {
|
function handleModuleUpdate(module: Module, moduleChanged: boolean, update: ModuleUpdate) {
|
||||||
const root = module.root()
|
const root = module.root()
|
||||||
if (!root) return
|
if (!root) return
|
||||||
moduleRoot.value = root
|
if (moduleRoot.value != root) {
|
||||||
if (root instanceof Ast.BodyBlock) topLevel.value = root
|
moduleRoot.value = root
|
||||||
|
}
|
||||||
|
if (root instanceof Ast.BodyBlock && topLevel.value != root) {
|
||||||
|
topLevel.value = root
|
||||||
|
}
|
||||||
// We can cast maps of unknown metadata fields to `NodeMetadata` because all `NodeMetadata` fields are optional.
|
// We can cast maps of unknown metadata fields to `NodeMetadata` because all `NodeMetadata` fields are optional.
|
||||||
const nodeMetadataUpdates = update.metadataUpdated as any as {
|
const nodeMetadataUpdates = update.metadataUpdated as any as {
|
||||||
id: AstId
|
id: AstId
|
||||||
@ -375,25 +390,38 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateNodeRect(nodeId: NodeId, rect: Rect) {
|
function updateNodeRect(nodeId: NodeId, rect: Rect) {
|
||||||
const nodeAst = syncModule.value?.tryGet(nodeId)
|
nodeRects.set(nodeId, rect)
|
||||||
if (!nodeAst) return
|
if (rect.pos.equals(Vec2.Zero)) {
|
||||||
if (rect.pos.equals(Vec2.Zero) && !nodeAst.nodeMetadata.get('position')) {
|
nodesToPlace.push(nodeId)
|
||||||
const { position } = nonDictatedPlacement(rect.size, {
|
|
||||||
nodeRects: visibleNodeAreas.value,
|
|
||||||
// The rest of the properties should not matter.
|
|
||||||
selectedNodeRects: [],
|
|
||||||
screenBounds: Rect.Zero,
|
|
||||||
mousePosition: Vec2.Zero,
|
|
||||||
})
|
|
||||||
editNodeMetadata(nodeAst, (metadata) =>
|
|
||||||
metadata.set('position', { x: position.x, y: position.y }),
|
|
||||||
)
|
|
||||||
nodeRects.set(nodeId, new Rect(position, rect.size))
|
|
||||||
} else {
|
|
||||||
nodeRects.set(nodeId, rect)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodesToPlace = reactive<NodeId[]>([])
|
||||||
|
|
||||||
|
watch(nodesToPlace, (nodeIds) => {
|
||||||
|
if (nodeIds.length === 0) return
|
||||||
|
const nodesToProcess = [...nodeIds]
|
||||||
|
nodesToPlace.length = 0
|
||||||
|
batchEdits(() => {
|
||||||
|
for (const nodeId of nodesToProcess) {
|
||||||
|
const nodeAst = syncModule.value?.get(nodeId)
|
||||||
|
const rect = nodeRects.get(nodeId)
|
||||||
|
if (!rect || !nodeAst || nodeAst.nodeMetadata.get('position') != null) continue
|
||||||
|
const { position } = nonDictatedPlacement(rect.size, {
|
||||||
|
nodeRects: visibleNodeAreas.value,
|
||||||
|
// The rest of the properties should not matter.
|
||||||
|
selectedNodeRects: [],
|
||||||
|
screenBounds: Rect.Zero,
|
||||||
|
mousePosition: Vec2.Zero,
|
||||||
|
})
|
||||||
|
editNodeMetadata(nodeAst, (metadata) =>
|
||||||
|
metadata.set('position', { x: position.x, y: position.y }),
|
||||||
|
)
|
||||||
|
nodeRects.set(nodeId, new Rect(position, rect.size))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function updateVizRect(id: NodeId, rect: Rect | undefined) {
|
function updateVizRect(id: NodeId, rect: Rect | undefined) {
|
||||||
if (rect) vizRects.set(id, rect)
|
if (rect) vizRects.set(id, rect)
|
||||||
else vizRects.delete(id)
|
else vizRects.delete(id)
|
||||||
@ -506,6 +534,11 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
return result!
|
return result!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function batchEdits(f: () => void) {
|
||||||
|
assert(syncModule.value != null)
|
||||||
|
syncModule.value.transact(f, 'local')
|
||||||
|
}
|
||||||
|
|
||||||
function editNodeMetadata(ast: Ast.Ast, f: (metadata: Ast.MutableNodeMetadata) => void) {
|
function editNodeMetadata(ast: Ast.Ast, f: (metadata: Ast.MutableNodeMetadata) => void) {
|
||||||
edit((edit) => f(edit.getVersion(ast).mutableNodeMetadata()), true, true)
|
edit((edit) => f(edit.getVersion(ast).mutableNodeMetadata()), true, true)
|
||||||
}
|
}
|
||||||
@ -626,6 +659,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
createNode,
|
createNode,
|
||||||
deleteNodes,
|
deleteNodes,
|
||||||
ensureCorrectNodeOrder,
|
ensureCorrectNodeOrder,
|
||||||
|
batchEdits,
|
||||||
setNodeContent,
|
setNodeContent,
|
||||||
setNodePosition,
|
setNodePosition,
|
||||||
setNodeVisualization,
|
setNodeVisualization,
|
||||||
|
@ -45,7 +45,6 @@ import {
|
|||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch,
|
||||||
watchEffect,
|
watchEffect,
|
||||||
type ShallowRef,
|
|
||||||
type WatchSource,
|
type WatchSource,
|
||||||
type WritableComputedRef,
|
type WritableComputedRef,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
@ -220,9 +219,9 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
executionContextId: this.id,
|
executionContextId: this.id,
|
||||||
expression: config.expression,
|
expression: config.expression,
|
||||||
visualizationModule: config.visualizationModule,
|
visualizationModule: config.visualizationModule,
|
||||||
...(config.positionalArgumentsExpressions
|
...(config.positionalArgumentsExpressions ?
|
||||||
? { positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||||
: {}),
|
: {}),
|
||||||
}),
|
}),
|
||||||
'Failed to attach visualization',
|
'Failed to attach visualization',
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -237,9 +236,9 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
|||||||
executionContextId: this.id,
|
executionContextId: this.id,
|
||||||
expression: config.expression,
|
expression: config.expression,
|
||||||
visualizationModule: config.visualizationModule,
|
visualizationModule: config.visualizationModule,
|
||||||
...(config.positionalArgumentsExpressions
|
...(config.positionalArgumentsExpressions ?
|
||||||
? { positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||||
: {}),
|
: {}),
|
||||||
}),
|
}),
|
||||||
'Failed to modify visualization',
|
'Failed to modify visualization',
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -582,9 +581,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
diagnostics.value = newDiagnostics
|
diagnostics.value = newDiagnostics
|
||||||
})
|
})
|
||||||
|
|
||||||
function useVisualizationData(
|
function useVisualizationData(configuration: WatchSource<Opt<NodeVisualizationConfiguration>>) {
|
||||||
configuration: WatchSource<Opt<NodeVisualizationConfiguration>>,
|
|
||||||
): ShallowRef<Result<{}> | undefined> {
|
|
||||||
const id = random.uuidv4() as Uuid
|
const id = random.uuidv4() as Uuid
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -598,28 +595,28 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
{ immediate: true, flush: 'post' },
|
{ immediate: true, flush: 'post' },
|
||||||
)
|
)
|
||||||
|
|
||||||
return shallowRef(
|
return computed(() => {
|
||||||
computed(() => {
|
const json = visualizationDataRegistry.getRawData(id)
|
||||||
const json = visualizationDataRegistry.getRawData(id)
|
if (!json?.ok) return json ?? undefined
|
||||||
if (!json?.ok) return json ?? undefined
|
const parsed = Ok(JSON.parse(json.value))
|
||||||
else return Ok(JSON.parse(json.value))
|
markRaw(parsed)
|
||||||
}),
|
return parsed
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataflowErrors = new ReactiveMapping(computedValueRegistry.db, (id, info) => {
|
const dataflowErrors = new ReactiveMapping(computedValueRegistry.db, (id, info) => {
|
||||||
const config = computed(() =>
|
const config = computed(() =>
|
||||||
info.payload.type === 'DataflowError'
|
info.payload.type === 'DataflowError' ?
|
||||||
? {
|
{
|
||||||
expressionId: id,
|
expressionId: id,
|
||||||
visualizationModule: 'Standard.Visualization.Preprocessor',
|
visualizationModule: 'Standard.Visualization.Preprocessor',
|
||||||
expression: {
|
expression: {
|
||||||
module: 'Standard.Visualization.Preprocessor',
|
module: 'Standard.Visualization.Preprocessor',
|
||||||
definedOnType: 'Standard.Visualization.Preprocessor',
|
definedOnType: 'Standard.Visualization.Preprocessor',
|
||||||
name: 'error_preprocessor',
|
name: 'error_preprocessor',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
)
|
)
|
||||||
const data = useVisualizationData(config)
|
const data = useVisualizationData(config)
|
||||||
return computed<{ kind: 'Dataflow'; message: string } | undefined>(() => {
|
return computed<{ kind: 'Dataflow'; message: string } | undefined>(() => {
|
||||||
|
@ -416,10 +416,10 @@ async function rewriteImports(code: string, dir: string, id: string | undefined)
|
|||||||
const pathJSON = JSON.stringify(path)
|
const pathJSON = JSON.stringify(path)
|
||||||
const destructureExpression = `{ ${specifiers.join(', ')} }`
|
const destructureExpression = `{ ${specifiers.join(', ')} }`
|
||||||
const rewritten =
|
const rewritten =
|
||||||
namespace != null
|
namespace != null ?
|
||||||
? `const ${namespace} = await window.__visualizationModules[${pathJSON}];` +
|
`const ${namespace} = await window.__visualizationModules[${pathJSON}];` +
|
||||||
(specifiers.length > 0 ? `\nconst ${destructureExpression} = ${namespace};` : '')
|
(specifiers.length > 0 ? `\nconst ${destructureExpression} = ${namespace};` : '')
|
||||||
: `const ${destructureExpression} = await window.__visualizationModules[${pathJSON}];`
|
: `const ${destructureExpression} = await window.__visualizationModules[${pathJSON}];`
|
||||||
s.overwrite(stmt.start!, stmt.end!, rewritten)
|
s.overwrite(stmt.start!, stmt.end!, rewritten)
|
||||||
if (isBuiltin) {
|
if (isBuiltin) {
|
||||||
// No further action is needed.
|
// No further action is needed.
|
||||||
@ -437,6 +437,7 @@ async function rewriteImports(code: string, dir: string, id: string | undefined)
|
|||||||
if (mimetype != null) {
|
if (mimetype != null) {
|
||||||
return importAsset(path, mimetype)
|
return importAsset(path, mimetype)
|
||||||
}
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -484,9 +485,9 @@ onmessage = async (
|
|||||||
case 'compile-request': {
|
case 'compile-request': {
|
||||||
try {
|
try {
|
||||||
const path = event.data.path
|
const path = event.data.path
|
||||||
await (event.data.recompile
|
await (event.data.recompile ?
|
||||||
? importVue(path)
|
importVue(path)
|
||||||
: map.setIfUndefined(alreadyCompiledModules, path, () => importVue(path)))
|
: map.setIfUndefined(alreadyCompiledModules, path, () => importVue(path)))
|
||||||
postMessage<CompilationResultResponse>({
|
postMessage<CompilationResultResponse>({
|
||||||
type: 'compilation-result-response',
|
type: 'compilation-result-response',
|
||||||
id: event.data.id,
|
id: event.data.id,
|
||||||
|
@ -235,12 +235,12 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
|||||||
|
|
||||||
function* types(type: Opt<string>) {
|
function* types(type: Opt<string>) {
|
||||||
const types =
|
const types =
|
||||||
type == null
|
type == null ?
|
||||||
? metadata.keys()
|
metadata.keys()
|
||||||
: new Set([
|
: new Set([
|
||||||
...(metadata.visualizationIdToType.reverseLookup(type) ?? []),
|
...(metadata.visualizationIdToType.reverseLookup(type) ?? []),
|
||||||
...(metadata.visualizationIdToType.reverseLookup('Any') ?? []),
|
...(metadata.visualizationIdToType.reverseLookup('Any') ?? []),
|
||||||
])
|
])
|
||||||
for (const type of types) yield fromVisualizationId(type)
|
for (const type of types) yield fromVisualizationId(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,13 @@ import {
|
|||||||
readTokenSpan,
|
readTokenSpan,
|
||||||
walkRecursive,
|
walkRecursive,
|
||||||
} from '@/util/ast'
|
} from '@/util/ast'
|
||||||
|
import { fc, test } from '@fast-check/vitest'
|
||||||
import { initializeFFI } from 'shared/ast/ffi'
|
import { initializeFFI } from 'shared/ast/ffi'
|
||||||
import { Token, Tree } from 'shared/ast/generated/ast'
|
import { Token, Tree } from 'shared/ast/generated/ast'
|
||||||
import type { LazyObject } from 'shared/ast/parserSupport'
|
import type { LazyObject } from 'shared/ast/parserSupport'
|
||||||
import { assert, expect, test } from 'vitest'
|
import { escapeTextLiteral, unescapeTextLiteral } from 'shared/ast/text'
|
||||||
|
import { assert, expect } from 'vitest'
|
||||||
|
import { TextLiteral } from '../abstract'
|
||||||
|
|
||||||
await initializeFFI()
|
await initializeFFI()
|
||||||
|
|
||||||
@ -224,3 +227,46 @@ test.each([
|
|||||||
expect(readAstSpan(ast, code)).toBe(expected?.repr)
|
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)
|
||||||
|
})
|
||||||
|
@ -107,11 +107,9 @@ function printArgPattern(application: ArgumentApplication | Ast.Ast) {
|
|||||||
|
|
||||||
while (current instanceof ArgumentApplication) {
|
while (current instanceof ArgumentApplication) {
|
||||||
const sigil =
|
const sigil =
|
||||||
current.argument instanceof ArgumentPlaceholder
|
current.argument instanceof ArgumentPlaceholder ? '?'
|
||||||
? '?'
|
: current.appTree instanceof Ast.App && current.appTree.argumentName ? '='
|
||||||
: current.appTree instanceof Ast.App && current.appTree.argumentName
|
: '@'
|
||||||
? '='
|
|
||||||
: '@'
|
|
||||||
parts.push(sigil + (current.argument.argInfo?.name ?? '_'))
|
parts.push(sigil + (current.argument.argInfo?.name ?? '_'))
|
||||||
current = current.target
|
current = current.target
|
||||||
}
|
}
|
||||||
|
@ -92,11 +92,11 @@ test.each([
|
|||||||
).toBe(extracted != null)
|
).toBe(extracted != null)
|
||||||
expect(
|
expect(
|
||||||
patternAst.match(targetAst)?.map((match) => module.tryGet(match)?.code()),
|
patternAst.match(targetAst)?.map((match) => module.tryGet(match)?.code()),
|
||||||
extracted != null
|
extracted != null ?
|
||||||
? `'${target}' matches '${pattern}' with '__'s corresponding to ${JSON.stringify(extracted)
|
`'${target}' matches '${pattern}' with '__'s corresponding to ${JSON.stringify(extracted)
|
||||||
.slice(1, -1)
|
.slice(1, -1)
|
||||||
.replace(/"/g, "'")}`
|
.replace(/"/g, "'")}`
|
||||||
: `'${target}' does not match '${pattern}'`,
|
: `'${target}' does not match '${pattern}'`,
|
||||||
).toStrictEqual(extracted)
|
).toStrictEqual(extracted)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import * as astText from '@/util/ast/text'
|
|
||||||
import { unescape } from 'querystring'
|
|
||||||
import { expect, test } from 'vitest'
|
|
||||||
|
|
||||||
test.each([
|
|
||||||
{ string: 'abcdef_123', escaped: 'abcdef_123' },
|
|
||||||
{ string: '\t\r\n\v"\'`', escaped: '\\t\\r\\n\\v\\"\\\'``' },
|
|
||||||
{ string: '`foo` `bar` `baz`', escaped: '``foo`` ``bar`` ``baz``' },
|
|
||||||
])('`escape`', ({ string, escaped }) => {
|
|
||||||
const result = astText.escape(string)
|
|
||||||
expect(result).toBe(escaped)
|
|
||||||
expect(unescape(escaped)).toBe(string)
|
|
||||||
})
|
|
@ -302,10 +302,9 @@ export class AliasAnalyzer {
|
|||||||
const expression = caseLine.case?.expression
|
const expression = caseLine.case?.expression
|
||||||
if (pattern) {
|
if (pattern) {
|
||||||
const armStart = parsedTreeOrTokenRange(pattern)[0]
|
const armStart = parsedTreeOrTokenRange(pattern)[0]
|
||||||
const armEnd = expression
|
const armEnd =
|
||||||
? parsedTreeOrTokenRange(expression)[1]
|
expression ? parsedTreeOrTokenRange(expression)[1]
|
||||||
: arrow
|
: arrow ? parsedTreeOrTokenRange(arrow)[1]
|
||||||
? parsedTreeOrTokenRange(arrow)[1]
|
|
||||||
: parsedTreeOrTokenRange(pattern)[1]
|
: parsedTreeOrTokenRange(pattern)[1]
|
||||||
|
|
||||||
const armRange: SourceRange = [armStart, armEnd]
|
const armRange: SourceRange = [armStart, armEnd]
|
||||||
|
@ -18,9 +18,8 @@ import { tryGetSoleValue } from 'shared/util/data/iterable'
|
|||||||
import type { ExternalId, IdMap, SourceRange } from 'shared/yjsModel'
|
import type { ExternalId, IdMap, SourceRange } from 'shared/yjsModel'
|
||||||
import { markRaw } from 'vue'
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
type ExtractType<V, T> = T extends ReadonlyArray<infer Ts>
|
type ExtractType<V, T> =
|
||||||
? Extract<V, { type: Ts }>
|
T extends ReadonlyArray<infer Ts> ? Extract<V, { type: Ts }> : Extract<V, { type: T }>
|
||||||
: Extract<V, { type: T }>
|
|
||||||
|
|
||||||
type OneOrArray<T> = T | readonly T[]
|
type OneOrArray<T> = T | readonly T[]
|
||||||
|
|
||||||
@ -149,8 +148,8 @@ export class AstExtended<T extends Tree | Token = Tree | Token, HasIdMap extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
whitespaceLength() {
|
whitespaceLength() {
|
||||||
return 'whitespaceLengthInCodeBuffer' in this.inner
|
return 'whitespaceLengthInCodeBuffer' in this.inner ?
|
||||||
? this.inner.whitespaceLengthInCodeBuffer
|
this.inner.whitespaceLengthInCodeBuffer
|
||||||
: this.inner.whitespaceLengthInCodeParsed
|
: this.inner.whitespaceLengthInCodeParsed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,10 +178,9 @@ export class AstExtended<T extends Tree | Token = Tree | Token, HasIdMap extends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CondType<T, Cond extends boolean> = Cond extends true
|
type CondType<T, Cond extends boolean> =
|
||||||
? T
|
Cond extends true ? T
|
||||||
: Cond extends false
|
: Cond extends false ? undefined
|
||||||
? undefined
|
|
||||||
: T | undefined
|
: T | undefined
|
||||||
|
|
||||||
class AstExtendedCtx<HasIdMap extends boolean> {
|
class AstExtendedCtx<HasIdMap extends boolean> {
|
||||||
|
@ -3,9 +3,9 @@ import { Ast } from '@/util/ast'
|
|||||||
|
|
||||||
export function nodeFromAst(ast: Ast.Ast): NodeDataFromAst | undefined {
|
export function nodeFromAst(ast: Ast.Ast): NodeDataFromAst | undefined {
|
||||||
const { nodeCode, documentation } =
|
const { nodeCode, documentation } =
|
||||||
ast instanceof Ast.Documented
|
ast instanceof Ast.Documented ?
|
||||||
? { nodeCode: ast.expression, documentation: ast.documentation() }
|
{ nodeCode: ast.expression, documentation: ast.documentation() }
|
||||||
: { nodeCode: ast, documentation: undefined }
|
: { nodeCode: ast, documentation: undefined }
|
||||||
if (!nodeCode) return
|
if (!nodeCode) return
|
||||||
const pattern = nodeCode instanceof Ast.Assignment ? nodeCode.pattern : undefined
|
const pattern = nodeCode instanceof Ast.Assignment ? nodeCode.pattern : undefined
|
||||||
const rootSpan = nodeCode instanceof Ast.Assignment ? nodeCode.expression : nodeCode
|
const rootSpan = nodeCode instanceof Ast.Assignment ? nodeCode.expression : nodeCode
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { swapKeysAndValues } from '@/util/record'
|
|
||||||
|
|
||||||
const mapping: Record<string, string> = {
|
|
||||||
'\b': '\\b',
|
|
||||||
'\f': '\\f',
|
|
||||||
'\n': '\\n',
|
|
||||||
'\r': '\\r',
|
|
||||||
'\t': '\\t',
|
|
||||||
'\v': '\\v',
|
|
||||||
'\\': '\\\\',
|
|
||||||
'"': '\\"',
|
|
||||||
"'": "\\'",
|
|
||||||
'`': '``',
|
|
||||||
}
|
|
||||||
|
|
||||||
const reverseMapping = swapKeysAndValues(mapping)
|
|
||||||
|
|
||||||
/** Escape a string so it can be safely spliced into an interpolated (`''`) Enso string.
|
|
||||||
* NOT USABLE to insert into raw strings. Does not include quotes. */
|
|
||||||
export function escape(string: string) {
|
|
||||||
return string.replace(/[\0\b\f\n\r\t\v\\"'`]/g, (match) => mapping[match]!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The reverse of `escape`: transform the string into human-readable form, not suitable for interpolation. */
|
|
||||||
export function unescape(string: string) {
|
|
||||||
return string.replace(/\\[0bfnrtv\\"']|``/g, (match) => reverseMapping[match]!)
|
|
||||||
}
|
|
@ -1,10 +1,43 @@
|
|||||||
import { useEvent } from '@/composables/events'
|
import { useEvent } from '@/composables/events'
|
||||||
import type { Ref } from 'vue'
|
import { watchEffect, type Ref } from 'vue'
|
||||||
|
|
||||||
/** Automatically `blur` the currently active element on any mouse click outside of `root`.
|
/** Automatically `blur` the currently active element on any mouse click outside of `root`.
|
||||||
* It is useful when other elements may capture pointer events, preventing default browser behavior for focus change. */
|
* It is useful when other elements may capture pointer events, preventing default browser behavior for focus change. */
|
||||||
export function useAutoBlur(root: Ref<HTMLElement | SVGElement | MathMLElement | undefined>) {
|
export function useAutoBlur(root: Ref<HTMLElement | SVGElement | undefined>) {
|
||||||
useEvent(window, 'pointerdown', (event) => blurIfNecessary(root, event), { capture: true })
|
watchEffect((onCleanup) => {
|
||||||
|
const element = root.value
|
||||||
|
if (element) {
|
||||||
|
autoBlurRoots.add(element)
|
||||||
|
onCleanup(() => autoBlurRoots.delete(element))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoBlurRoots = new Set<HTMLElement | SVGElement | MathMLElement>()
|
||||||
|
|
||||||
|
export function registerAutoBlurHandler() {
|
||||||
|
useEvent(
|
||||||
|
window,
|
||||||
|
'pointerdown',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
!(event.target instanceof Element) ||
|
||||||
|
(!(document.activeElement instanceof HTMLElement) &&
|
||||||
|
!(document.activeElement instanceof SVGElement) &&
|
||||||
|
!(document.activeElement instanceof MathMLElement))
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
for (const root of autoBlurRoots) {
|
||||||
|
if (root.contains(document.activeElement) && !root.contains(event.target)) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
{ capture: true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if the target of the event is in the DOM subtree of the given `area` element. */
|
/** Returns true if the target of the event is in the DOM subtree of the given `area` element. */
|
||||||
@ -14,21 +47,3 @@ export function targetIsOutside(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return !!area.value && e.target instanceof Element && !area.value.contains(e.target)
|
return !!area.value && e.target instanceof Element && !area.value.contains(e.target)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal logic of `useAutoBlur`, useful for direct usage in some cases.
|
|
||||||
* Returns `true` if `event` does not target `root` and blurs currently active element.
|
|
||||||
* Otherwise returns `false` and does nothing. */
|
|
||||||
export function blurIfNecessary(
|
|
||||||
root: Ref<HTMLElement | SVGElement | MathMLElement | undefined>,
|
|
||||||
event: MouseEvent,
|
|
||||||
): boolean {
|
|
||||||
if (!root.value?.contains(document.activeElement) || !targetIsOutside(event, root)) return false
|
|
||||||
if (
|
|
||||||
!(document.activeElement instanceof HTMLElement) &&
|
|
||||||
!(document.activeElement instanceof SVGElement) &&
|
|
||||||
!(document.activeElement instanceof MathMLElement)
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
document.activeElement.blur()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
@ -178,8 +178,8 @@ export class ArgumentApplication {
|
|||||||
const argFor = (key: 'lhs' | 'rhs', index: number) => {
|
const argFor = (key: 'lhs' | 'rhs', index: number) => {
|
||||||
const tree = interpreted[key]
|
const tree = interpreted[key]
|
||||||
const info = tryGetIndex(suggestion?.arguments, index) ?? unknownArgInfoNamed(key)
|
const info = tryGetIndex(suggestion?.arguments, index) ?? unknownArgInfoNamed(key)
|
||||||
return tree != null
|
return tree != null ?
|
||||||
? ArgumentAst.WithRetrievedConfig(tree, index, info, kind, widgetCfg)
|
ArgumentAst.WithRetrievedConfig(tree, index, info, kind, widgetCfg)
|
||||||
: ArgumentPlaceholder.WithRetrievedConfig(callId, index, info, kind, false, widgetCfg)
|
: ArgumentPlaceholder.WithRetrievedConfig(callId, index, info, kind, false, widgetCfg)
|
||||||
}
|
}
|
||||||
return new ArgumentApplication(
|
return new ArgumentApplication(
|
||||||
@ -308,9 +308,9 @@ export class ArgumentApplication {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const argumentFromDefinition =
|
const argumentFromDefinition =
|
||||||
argumentInCode.argName == null
|
argumentInCode.argName == null ?
|
||||||
? takeNextArgumentFromDefinition()
|
takeNextArgumentFromDefinition()
|
||||||
: takeNamedArgumentFromDefinition(argumentInCode.argName)
|
: takeNamedArgumentFromDefinition(argumentInCode.argName)
|
||||||
const { index, info } = argumentFromDefinition ?? {}
|
const { index, info } = argumentFromDefinition ?? {}
|
||||||
resolvedArgs.push({
|
resolvedArgs.push({
|
||||||
appTree: argumentInCode.appTree,
|
appTree: argumentInCode.appTree,
|
||||||
@ -318,9 +318,9 @@ export class ArgumentApplication {
|
|||||||
argumentInCode.argument,
|
argumentInCode.argument,
|
||||||
index,
|
index,
|
||||||
info ??
|
info ??
|
||||||
(argumentInCode.argName != null
|
(argumentInCode.argName != null ?
|
||||||
? unknownArgInfoNamed(argumentInCode.argName)
|
unknownArgInfoNamed(argumentInCode.argName)
|
||||||
: undefined),
|
: undefined),
|
||||||
ApplicationKind.Prefix,
|
ApplicationKind.Prefix,
|
||||||
widgetCfg,
|
widgetCfg,
|
||||||
),
|
),
|
||||||
@ -375,9 +375,9 @@ export class ArgumentApplication {
|
|||||||
toWidgetInput(): WidgetInput {
|
toWidgetInput(): WidgetInput {
|
||||||
return {
|
return {
|
||||||
portId:
|
portId:
|
||||||
this.argument instanceof ArgumentAst
|
this.argument instanceof ArgumentAst ?
|
||||||
? this.appTree.id
|
this.appTree.id
|
||||||
: (`app:${this.argument.portId}` as PortId),
|
: (`app:${this.argument.portId}` as PortId),
|
||||||
value: this.appTree,
|
value: this.appTree,
|
||||||
[ArgumentApplicationKey]: this,
|
[ArgumentApplicationKey]: this,
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ const STRING_TO_BOOLEAN: Record<string, boolean> = {
|
|||||||
* a string 'true', 'false', '1', or '0', it is converted to a boolean value. Otherwise, null is
|
* a string 'true', 'false', '1', or '0', it is converted to a boolean value. Otherwise, null is
|
||||||
* returned. */
|
* returned. */
|
||||||
function parseBoolean(value: unknown): boolean | null {
|
function parseBoolean(value: unknown): boolean | null {
|
||||||
return typeof value === 'boolean'
|
return (
|
||||||
? value
|
typeof value === 'boolean' ? value
|
||||||
: typeof value === 'string'
|
: typeof value === 'string' ? STRING_TO_BOOLEAN[value] ?? null
|
||||||
? STRING_TO_BOOLEAN[value] ?? null
|
|
||||||
: null
|
: null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StringConfig {
|
export interface StringConfig {
|
||||||
@ -69,12 +69,12 @@ export interface Group<T = Required<RawGroup>> extends Config<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Config<T = Required<RawConfig>> {
|
export interface Config<T = Required<RawConfig>> {
|
||||||
options: T extends { options: infer Options extends object }
|
options: T extends { options: infer Options extends object } ?
|
||||||
? { [K in keyof Options]: Option<Options[K]> }
|
{ [K in keyof Options]: Option<Options[K]> }
|
||||||
: {}
|
: {}
|
||||||
groups: T extends { groups: infer Groups extends object }
|
groups: T extends { groups: infer Groups extends object } ?
|
||||||
? { [K in keyof Groups]: Group<Groups[K]> }
|
{ [K in keyof Groups]: Group<Groups[K]> }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOption<T>(option: T): Option<T> {
|
function loadOption<T>(option: T): Option<T> {
|
||||||
@ -87,12 +87,14 @@ function loadOption<T>(option: T): Option<T> {
|
|||||||
description: String(obj.description ?? ''),
|
description: String(obj.description ?? ''),
|
||||||
defaultDescription: obj.defaultDescription != null ? String(obj.defaultDescription) : undefined,
|
defaultDescription: obj.defaultDescription != null ? String(obj.defaultDescription) : undefined,
|
||||||
value:
|
value:
|
||||||
typeof value === 'string' ||
|
(
|
||||||
typeof value === 'number' ||
|
typeof value === 'string' ||
|
||||||
typeof value === 'boolean' ||
|
typeof value === 'number' ||
|
||||||
(Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
typeof value === 'boolean' ||
|
||||||
? value
|
(Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
||||||
: '',
|
) ?
|
||||||
|
value
|
||||||
|
: '',
|
||||||
primary: Boolean(obj.primary ?? true),
|
primary: Boolean(obj.primary ?? true),
|
||||||
} satisfies Option<RawOption> as any
|
} satisfies Option<RawOption> as any
|
||||||
}
|
}
|
||||||
@ -111,13 +113,13 @@ export function loadConfig<T>(config: T): Config<T> {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
options:
|
options:
|
||||||
'options' in config && typeof config.options === 'object' && config.options != null
|
'options' in config && typeof config.options === 'object' && config.options != null ?
|
||||||
? Object.fromEntries(Object.entries(config.options).map(([k, v]) => [k, loadOption(v)]))
|
Object.fromEntries(Object.entries(config.options).map(([k, v]) => [k, loadOption(v)]))
|
||||||
: {},
|
: {},
|
||||||
groups:
|
groups:
|
||||||
'groups' in config && typeof config.groups === 'object' && config.groups != null
|
'groups' in config && typeof config.groups === 'object' && config.groups != null ?
|
||||||
? Object.fromEntries(Object.entries(config.groups).map(([k, v]) => [k, loadGroup(v)]))
|
Object.fromEntries(Object.entries(config.groups).map(([k, v]) => [k, loadGroup(v)]))
|
||||||
: {},
|
: {},
|
||||||
} satisfies Config as any
|
} satisfies Config as any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export function nextEvent<O extends ObservableV2<any>, NAME extends string>(
|
|||||||
|
|
||||||
declare const EVENTS_BRAND: unique symbol
|
declare const EVENTS_BRAND: unique symbol
|
||||||
declare module 'lib0/observable' {
|
declare module 'lib0/observable' {
|
||||||
interface ObservableV2<EVENTS extends { [key: string]: (...arg0: any[]) => void }> {
|
interface ObservableV2<EVENTS extends { [key in keyof EVENTS]: (...arg0: any[]) => void }> {
|
||||||
[EVENTS_BRAND]: EVENTS
|
[EVENTS_BRAND]: EVENTS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export function asNot<Not>(value: any): Excluding<Not, typeof value> {
|
|
||||||
return value as any
|
|
||||||
}
|
|
||||||
|
|
||||||
type Excluding<Not, Value> = Value extends Not ? never : Value
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import { shallowReactive } from 'vue'
|
||||||
|
|
||||||
let _measureContext: CanvasRenderingContext2D | undefined
|
let _measureContext: CanvasRenderingContext2D | undefined
|
||||||
function getMeasureContext() {
|
function getMeasureContext() {
|
||||||
return (_measureContext ??= document.createElement('canvas').getContext('2d')!)
|
return (_measureContext ??= document.createElement('canvas').getContext('2d')!)
|
||||||
@ -14,11 +16,47 @@ export function getTextWidthBySizeAndFamily(
|
|||||||
|
|
||||||
/** Helper function to get text width. `font` is a CSS font specification as per https://developer.mozilla.org/en-US/docs/Web/CSS/font. */
|
/** Helper function to get text width. `font` is a CSS font specification as per https://developer.mozilla.org/en-US/docs/Web/CSS/font. */
|
||||||
export function getTextWidthByFont(text: string | null | undefined, font: string) {
|
export function getTextWidthByFont(text: string | null | undefined, font: string) {
|
||||||
if (text == null) {
|
if (text == null || font == '' || !fontReady(font)) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
const context = getMeasureContext()
|
const context = getMeasureContext()
|
||||||
context.font = font
|
context.font = font
|
||||||
const metrics = context.measureText(' ' + text)
|
const metrics = context.measureText(text)
|
||||||
return metrics.width
|
return metrics.width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores loading status of queried fonts, so we can make the check synchronous and reactive.
|
||||||
|
* This is supposed to be global, since the font loading state is scoped to the document and cannot
|
||||||
|
* revert back to loading (assuming we don't dynamically change existing @font-face definitions to
|
||||||
|
* point to different URLs, which would be incredibly cursed).
|
||||||
|
*/
|
||||||
|
const fontsReady = shallowReactive(new Map())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if given font is ready to use. In case if it is not, the check will automatically register
|
||||||
|
* a reactive dependency, which will be notified once loading is complete.
|
||||||
|
* @param font
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function fontReady(font: string): boolean {
|
||||||
|
const readyState = fontsReady.get(font)
|
||||||
|
if (readyState === undefined) {
|
||||||
|
let syncReady
|
||||||
|
try {
|
||||||
|
// This operation can fail if the provided font string is not a valid CSS font specifier.
|
||||||
|
syncReady = document.fonts.check(font)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
// In case of exception, treat the font as if it was loaded. That way we don't attempt loading
|
||||||
|
// it again, and the browser font fallback logic should still make things behave more or less
|
||||||
|
// correct.
|
||||||
|
syncReady = true
|
||||||
|
}
|
||||||
|
fontsReady.set(font, syncReady)
|
||||||
|
if (syncReady) return true
|
||||||
|
else document.fonts.load(font).then(() => fontsReady.set(font, true))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return readyState
|
||||||
|
}
|
||||||
|
@ -209,24 +209,21 @@ type NormalizeKeybindSegment = {
|
|||||||
[K in KeybindSegment as Lowercase<K>]: K
|
[K in KeybindSegment as Lowercase<K>]: K
|
||||||
}
|
}
|
||||||
type SuggestedKeybindSegment = ModifierPlus | Pointer | Key
|
type SuggestedKeybindSegment = ModifierPlus | Pointer | Key
|
||||||
type AutocompleteKeybind<T extends string, Key extends string = never> = T extends '+'
|
type AutocompleteKeybind<T extends string, Key extends string = never> =
|
||||||
? T
|
T extends '+' ? T
|
||||||
: T extends `${infer First}+${infer Rest}`
|
: T extends `${infer First}+${infer Rest}` ?
|
||||||
? Lowercase<First> extends LowercaseModifier
|
Lowercase<First> extends LowercaseModifier ?
|
||||||
? `${NormalizeKeybindSegment[Lowercase<First>] & string}+${AutocompleteKeybind<Rest>}`
|
`${NormalizeKeybindSegment[Lowercase<First>] & string}+${AutocompleteKeybind<Rest>}`
|
||||||
: Lowercase<First> extends LowercasePointer | LowercaseKey
|
: Lowercase<First> extends LowercasePointer | LowercaseKey ?
|
||||||
? AutocompleteKeybind<Rest, NormalizeKeybindSegment[Lowercase<First>] & string>
|
AutocompleteKeybind<Rest, NormalizeKeybindSegment[Lowercase<First>] & string>
|
||||||
: `${Modifier}+${AutocompleteKeybind<Rest>}`
|
: `${Modifier}+${AutocompleteKeybind<Rest>}`
|
||||||
: T extends ''
|
: T extends '' ? SuggestedKeybindSegment
|
||||||
? SuggestedKeybindSegment
|
: Lowercase<T> extends LowercasePointer | LowercaseKey ? NormalizeKeybindSegment[Lowercase<T>]
|
||||||
: Lowercase<T> extends LowercasePointer | LowercaseKey
|
: Lowercase<T> extends LowercaseModifier ?
|
||||||
? NormalizeKeybindSegment[Lowercase<T>]
|
[Key] extends [never] ?
|
||||||
: Lowercase<T> extends LowercaseModifier
|
`${NormalizeKeybindSegment[Lowercase<T>] & string}+${SuggestedKeybindSegment}`
|
||||||
? [Key] extends [never]
|
|
||||||
? `${NormalizeKeybindSegment[Lowercase<T>] & string}+${SuggestedKeybindSegment}`
|
|
||||||
: `${NormalizeKeybindSegment[Lowercase<T>] & string}+${Key}`
|
: `${NormalizeKeybindSegment[Lowercase<T>] & string}+${Key}`
|
||||||
: [Key] extends [never]
|
: [Key] extends [never] ? SuggestedKeybindSegment
|
||||||
? SuggestedKeybindSegment
|
|
||||||
: Key
|
: Key
|
||||||
|
|
||||||
type AutocompleteKeybinds<T extends string[]> = {
|
type AutocompleteKeybinds<T extends string[]> = {
|
||||||
@ -235,8 +232,9 @@ type AutocompleteKeybinds<T extends string[]> = {
|
|||||||
|
|
||||||
// `never extends T ? Result : InferenceSource` is a trick to unify `T` with the actual type of the
|
// `never extends T ? Result : InferenceSource` is a trick to unify `T` with the actual type of the
|
||||||
// argument.
|
// argument.
|
||||||
type Keybinds<T extends Record<K, string[]>, K extends keyof T = keyof T> = never extends T
|
type Keybinds<T extends Record<K, string[]>, K extends keyof T = keyof T> =
|
||||||
? {
|
never extends T ?
|
||||||
|
{
|
||||||
[K in keyof T]: AutocompleteKeybinds<T[K]>
|
[K in keyof T]: AutocompleteKeybinds<T[K]>
|
||||||
}
|
}
|
||||||
: T
|
: T
|
||||||
@ -353,9 +351,9 @@ export function defineKeybinds<
|
|||||||
return (event, stopAndPrevent = true) => {
|
return (event, stopAndPrevent = true) => {
|
||||||
const eventModifierFlags = modifierFlagsForEvent(event)
|
const eventModifierFlags = modifierFlagsForEvent(event)
|
||||||
const keybinds =
|
const keybinds =
|
||||||
event instanceof KeyboardEvent
|
event instanceof KeyboardEvent ?
|
||||||
? keyboardShortcuts[event.key.toLowerCase() as Key_]?.[eventModifierFlags]
|
keyboardShortcuts[event.key.toLowerCase() as Key_]?.[eventModifierFlags]
|
||||||
: mouseShortcuts[buttonFlagsForEvent(event)]?.[eventModifierFlags]
|
: mouseShortcuts[buttonFlagsForEvent(event)]?.[eventModifierFlags]
|
||||||
let handle = handlers[DefaultHandler]
|
let handle = handlers[DefaultHandler]
|
||||||
if (keybinds != null) {
|
if (keybinds != null) {
|
||||||
for (const bindingName in handlers) {
|
for (const bindingName in handlers) {
|
||||||
|
@ -42,8 +42,9 @@ watchEffect(async (onCleanup) => {
|
|||||||
const prefixLength = props.prefix?.length ?? 0
|
const prefixLength = props.prefix?.length ?? 0
|
||||||
const directory = maybeDirectory
|
const directory = maybeDirectory
|
||||||
const ls = await projectStore.lsRpcConnection
|
const ls = await projectStore.lsRpcConnection
|
||||||
const maybeProjectRoot = (await projectStore.contentRoots).find((root) => root.type === 'Project')
|
const maybeProjectRoot = (await projectStore.contentRoots).find(
|
||||||
?.id
|
(root) => root.type === 'Project',
|
||||||
|
)?.id
|
||||||
if (!maybeProjectRoot) return
|
if (!maybeProjectRoot) return
|
||||||
const projectRoot = maybeProjectRoot
|
const projectRoot = maybeProjectRoot
|
||||||
async function walkFiles(
|
async function walkFiles(
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"outDir": "../../node_modules/.cache/tsc",
|
"outDir": "../../node_modules/.cache/tsc",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"outDir": "../../node_modules/.cache/tsc",
|
"outDir": "../../node_modules/.cache/tsc",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
@ -30,17 +30,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
...(process.env.E2E === 'true'
|
...(process.env.E2E === 'true' ?
|
||||||
? { '/src/main.ts': fileURLToPath(new URL('./e2e/main.ts', import.meta.url)) }
|
{ '/src/main.ts': fileURLToPath(new URL('./e2e/main.ts', import.meta.url)) }
|
||||||
: {}),
|
: {}),
|
||||||
shared: fileURLToPath(new URL('./shared', import.meta.url)),
|
shared: fileURLToPath(new URL('./shared', import.meta.url)),
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
REDIRECT_OVERRIDE: IS_CLOUD_BUILD
|
REDIRECT_OVERRIDE:
|
||||||
? 'undefined'
|
IS_CLOUD_BUILD ? 'undefined' : JSON.stringify(`http://localhost:${localServerPort}`),
|
||||||
: JSON.stringify(`http://localhost:${localServerPort}`),
|
|
||||||
IS_CLOUD_BUILD: JSON.stringify(IS_CLOUD_BUILD),
|
IS_CLOUD_BUILD: JSON.stringify(IS_CLOUD_BUILD),
|
||||||
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
|
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
|
||||||
IS_DEV_MODE: JSON.stringify(process.env.NODE_ENV === 'development'),
|
IS_DEV_MODE: JSON.stringify(process.env.NODE_ENV === 'development'),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user