diff --git a/.env.template b/.env.template index acbf44f96..d7b9fd6ad 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,7 @@ PUBLIC_CONTEXT = local PUBLIC_FIREBASE_API_KEY = myfakeapikeymyfakeapikeymyfakeapikeymyf PUBLIC_FIREBASE_AUTH_DOMAIN = wordplay-dev.firebaseapp.com -PUBLIC_FIREBASE_PROJECT_ID = demo +PUBLIC_FIREBASE_PROJECT_ID = demo-wordplay PUBLIC_FIREBASE_MESSAGING_SENDER_ID = 123456789123 PUBLIC_FIREBASE_APP_ID = 1:123456789123:web:1234567890123456789012 PUBLIC_FIREBASE_MEASUREMENT_ID = G-FAKEFAKEFA \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 188aaa0a4..a45905907 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -23,7 +23,19 @@ const db = getFirestore(); type UserMatch = { uid: string; email: string | null; name: string | null }; +// Permit local testing and calls from our two domains. +const cors = { + cors: [ + '/firebase\.com$/', + '/127.0.0.1*/', + 'http://localhost:5173', + 'https://test.wordplay.dev', + 'https://wordplay.dev', + ], +}; + export const getCreators = onCall( + cors, async (request): Promise => { const users = await admin.auth().getUsers(request.data); const matches: UserMatch[] = []; @@ -42,7 +54,7 @@ export const getCreators = onCall( export const emailExists = onCall< EmailExistsInputs, Promise ->(async (request) => { +>(cors, async (request) => { const emails = request.data; return admin .auth() @@ -70,100 +82,85 @@ export const getTranslations = onCall<{ from: string; to: string; text: string[]; -}>( - // Permit local testing and calls from our two domains. - { - cors: [ - '/firebase\.com$/', - '/127.0.0.1*/', - '/localhost*/', - 'https://test.wordplay.dev', - 'https://wordplay.dev', - ], - }, - async (request): Promise => { - const from = request.data.from; - const to = request.data.to; - const text = request.data.text; +}>(cors, async (request): Promise => { + const from = request.data.from; + const to = request.data.to; + const text = request.data.text; - try { - // Creates a Google Translate client - const translator = new Translate.v2.Translate(); + try { + // Creates a Google Translate client + const translator = new Translate.v2.Translate(); - const [translations] = await translator.translate(text, { - from, - to, - }); + const [translations] = await translator.translate(text, { + from, + to, + }); - return translations; - } catch (e) { - console.error(e); - return null; - } - }, -); + return translations; + } catch (e) { + console.error(e); + return null; + } +}); /** Given a URL that should refer to an HTML document, sends a GET request to the URL to try to get the document's text. */ -export const getWebpage = onRequest( - { cors: true }, - async (request, response) => { - const url: string | undefined = - 'url' in request.query && typeof request.query.url === 'string' - ? decodeURI(request.query['url']) - : undefined; - - const lib = - url === undefined - ? undefined - : url.startsWith('https://') - ? https - : http; - - // Cache the response for 10 minutes to minimize requests. - response.set('Cache-Control', 'public, max-age=600, s-maxage=600'); - - if (lib === undefined || url === undefined) { - console.log('Invalid URL ' + url); - response.json('invalid-url'); - } else { - const result: string = await new Promise((resolve) => { - lib.get(url, (resp) => { - const contentType = resp.headers['content-type']; - if (resp.statusCode !== 200) { - console.error(`GET status: Code: ${resp.statusCode}`); - resolve('not-available'); - } else if (!contentType?.startsWith('text/html')) { - console.error(`GET received ${contentType}`); - resolve('not-html'); - } - - // Get the data - let data = ''; - - // A chunk of data has been recieved. - resp.on('data', (chunk) => { - data += chunk; - }); - - // The whole response has been received. Print out the result. - resp.on('end', () => { - console.log('GET success'); - resolve(data); - }); - }).on('error', (err) => { - console.error('GET error: ' + err.message); +export const getWebpage = onRequest(cors, async (request, response) => { + const url: string | undefined = + 'url' in request.query && typeof request.query.url === 'string' + ? decodeURI(request.query['url']) + : undefined; + + const lib = + url === undefined + ? undefined + : url.startsWith('https://') + ? https + : http; + + // Cache the response for 10 minutes to minimize requests. + response.set('Cache-Control', 'public, max-age=600, s-maxage=600'); + + if (lib === undefined || url === undefined) { + console.log('Invalid URL ' + url); + response.json('invalid-url'); + } else { + const result: string = await new Promise((resolve) => { + lib.get(url, (resp) => { + const contentType = resp.headers['content-type']; + if (resp.statusCode !== 200) { + console.error(`GET status: Code: ${resp.statusCode}`); resolve('not-available'); + } else if (!contentType?.startsWith('text/html')) { + console.error(`GET received ${contentType}`); + resolve('not-html'); + } + + // Get the data + let data = ''; + + // A chunk of data has been recieved. + resp.on('data', (chunk) => { + data += chunk; }); + + // The whole response has been received. Print out the result. + resp.on('end', () => { + console.log('GET success'); + resolve(data); + }); + }).on('error', (err) => { + console.error('GET error: ' + err.message); + resolve('not-available'); }); + }); - // Set the content length header. - response.set('Content-Length', `${new Blob([result]).size}`); + // Set the content length header. + response.set('Content-Length', `${new Blob([result]).size}`); - // Send the HTML back as JSON-encoded text. - response.json(result); - } - }, -); + // Send the HTML back as JSON-encoded text. + response.json(result); + } +}); const PurgeDayDelay = 30; const MillisecondsPerDay = 24 * 60 * 60 * 1000; @@ -202,7 +199,7 @@ export const purgeArchivedProjects = onSchedule( export const createClass = onCall< CreateClassInputs, Promise ->(async (request) => { +>(cors, async (request) => { const auth = admin.auth(); const { teacher, name, description, students, existing } = request.data; diff --git a/src/app.html b/src/app.html index fde6be00e..fa1b438fe 100644 --- a/src/app.html +++ b/src/app.html @@ -303,17 +303,34 @@ :root { --color-blue: #648fff; + --color-blue-transparent: color-mix( + in srgb, + var(--color-blue) 10%, + transparent + ); --color-purple: #785ef0; + --color-purple-transparent: color-mix( + in srgb, + var(--color-purple) 10%, + transparent + ); --color-pink: #dc267f; - --color-light-pink: #f7d3e4; + --color-pink-transparent: color-mix( + in srgb, + var(--color-pink) 2%, + transparent + ); --color-orange: #fe6100; --color-yellow: #ffb000; + --color-yellow-transparent: #ffae004b; --color-white: #ffffff; --color-black: #000000; --color-light-grey: #9f9f9f; + --color-light-grey-transparent: #9f9f9f36; --color-very-light-grey: #f3f3f3; --color-dark-grey: #303030; --color-shadow: rgb(0, 0, 0, 0.2); + --color-shadow-transparent: rgb(0, 0, 0, 0.05); /* Colors */ --wordplay-foreground: var(--color-black); @@ -324,7 +341,7 @@ --wordplay-evaluation-color: var(--color-pink); --wordplay-highlight-color: var(--color-yellow); --wordplay-focus-color: var(--color-blue); - --wordplay-hover: #feebc3; + --wordplay-hover: var(--color-yellow-transparent); --wordplay-error: var(--color-orange); --wordplay-warning: var(--color-yellow); --wordplay-inactive-color: var(--color-light-grey); @@ -346,12 +363,13 @@ --wordplay-editor-indent: 3em; --wordplay-editor-radius: 3px; --wordplay-spacing: 0.5em; + --wordplay-spacing-half: 0.25em; --wordplay-font-weight: 400; --wordplay-font-size: 12pt; --wordplay-small-font-size: 10pt; --wordplay-border-width: 1px; --wordplay-focus-width: 4px; - --wordplay-border-radius: 5px; + --wordplay-border-radius: 8px; --wordplay-code-line-height: 1.6; --wordplay-palette-min-width: 5em; --wordplay-palette-max-width: 15em; @@ -368,15 +386,17 @@ --color-purple: #5b3ce6; --color-purple-transparent: #261862; --color-pink: #ee68a9; - --color-light-pink: rgb(77, 15, 45); + --color-pink-transparent: rgb(77, 15, 45); --color-orange: #993800; - --color-yellow: #d69600; + --color-yellow: #b67c00; + --color-yellow-transparent: #b67c004b; --color-white: #ffffff; --color-black: #000000; --color-light-grey: #b4b4b4; --color-very-light-grey: #373636; --color-dark-grey: #eaeaea; - --color-shadow: rgb(32, 32, 32, 0.2); + --color-shadow: rgba(227, 227, 227, 0.8); + --color-shadow-transparent: rgba(227, 227, 227, 0.95); --wordplay-foreground: var(--color-white); --wordplay-background: var(--color-black); @@ -386,7 +406,7 @@ --wordplay-evaluation-color: var(--color-pink); --wordplay-highlight-color: var(--color-yellow); --wordplay-focus-color: var(--color-blue); - --wordplay-hover: #463000; + --wordplay-hover: var(--color-yellow-transparent); --wordplay-error: var(--color-orange); --wordplay-warning: var(--color-yellow); --wordplay-inactive-color: var(--color-light-grey); @@ -486,7 +506,7 @@ @keyframes pulse { 0% { - stroke-width: calc(var(--wordplay-border-width) * 2); + stroke-width: var(--wordplay-focus-width); } 70% { @@ -494,7 +514,7 @@ } 100% { - stroke-width: calc(var(--wordplay-border-width) * 2); + stroke-width: var(--wordplay-focus-width); } } @@ -653,6 +673,27 @@ border-bottom: var(--wordplay-border-width) solid var(--wordplay-border-color); } + + a { + color: var(--wordplay-highlight-color); + text-decoration: none; + /* In case a parent disables pointer events, we need to enable them here. */ + pointer-events: auto; + } + + /* Links in paragraphs should have underlines for visibility. */ + p > a { + text-decoration: calc(var(--wordplay-focus-width) / 2) underline + var(--wordplay-highlight-color); + } + + a:focus, + a:hover { + outline: none; + text-decoration: underline; + text-decoration-thickness: var(--wordplay-focus-width); + text-decoration-color: var(--wordplay-focus-color); + } %sveltekit.head% diff --git a/src/basis/Basis.test.ts b/src/basis/Basis.test.ts index 8ca3f3334..49c14c677 100644 --- a/src/basis/Basis.test.ts +++ b/src/basis/Basis.test.ts @@ -1,7 +1,6 @@ import Templates from '@concepts/Templates'; import UnusedBind from '@conflicts/UnusedBind'; import DefaultLocales from '@locale/DefaultLocales'; -import Context from '@nodes/Context'; import type Node from '@nodes/Node'; import Source from '@nodes/Source'; import UnparsableExpression from '@nodes/UnparsableExpression'; @@ -16,7 +15,7 @@ const basis = Basis.getLocalizedBasis(DefaultLocales); const source = new Source('basis', ''); const project = Project.make(null, 'test', source, [], DefaultLocale); -const context = new Context(project, source); +const context = project.getContext(source); function checkBasisNodes(node: Node) { // Check for syntax errors diff --git a/src/basis/InternalExpression.ts b/src/basis/InternalExpression.ts index a9a4ee340..2d1bb1725 100644 --- a/src/basis/InternalExpression.ts +++ b/src/basis/InternalExpression.ts @@ -52,7 +52,7 @@ export default class InternalExpression extends SimpleExpression { } getPurpose() { - return Purpose.Evaluate; + return Purpose.Hidden; } computeType(): Type { diff --git a/src/basis/Iteration.ts b/src/basis/Iteration.ts index 5dab3ea7b..a54829b24 100644 --- a/src/basis/Iteration.ts +++ b/src/basis/Iteration.ts @@ -87,7 +87,7 @@ export class Iteration extends Expression { } getPurpose(): Purpose { - return Purpose.Evaluate; + return Purpose.Hidden; } computeType() { diff --git a/src/basis/NumberBasis.ts b/src/basis/NumberBasis.ts index fa88cb541..7c7ea4510 100644 --- a/src/basis/NumberBasis.ts +++ b/src/basis/NumberBasis.ts @@ -146,6 +146,7 @@ export default function bootstrapNumber(locales: Locales) { names, UnionType.make( NoneType.None, + //The type of the operand is the type of the input. NumberType.make((left) => left), ), NoneLiteral.make(), @@ -178,6 +179,7 @@ export default function bootstrapNumber(locales: Locales) { return expression(requestor, left, right); }, ), + // The type of the output is the same as the input type. NumberType.make((left) => left), ); } @@ -269,7 +271,7 @@ export default function bootstrapNumber(locales: Locales) { ), createBinaryOp( (locale) => locale.basis.Number.function.multiply, - // The operand's type can be any unitless measurement + // The operand's type can be any unitless number NumberType.make(), // The output's type is is the unit's product NumberType.make((left, right) => @@ -280,7 +282,9 @@ export default function bootstrapNumber(locales: Locales) { ), createBinaryOp( (locale) => locale.basis.Number.function.divide, + // Divide's operand can be any unitless number NumberType.make(), + // Divide's output's type is the unit's quotient NumberType.make((left, right) => right ? left.quotient(right) : left, ), diff --git a/src/basis/toStructure.ts b/src/basis/toStructure.ts index a8d261ae2..c25d40662 100644 --- a/src/basis/toStructure.ts +++ b/src/basis/toStructure.ts @@ -1,7 +1,11 @@ import type StructureDefinition from '@nodes/StructureDefinition'; +import UnparsableExpression from '@nodes/UnparsableExpression'; import { parseStructure } from '../parser/parseExpression'; import { toTokens } from '../parser/toTokens'; export default function toStructure(wordplay: string): StructureDefinition { - return parseStructure(toTokens(wordplay)); + const def = parseStructure(toTokens(wordplay)); + if (def instanceof UnparsableExpression) + throw new Error('Could not parse structure definition: ' + wordplay); + return def; } diff --git a/src/components/annotations/Annotation.svelte b/src/components/annotations/Annotation.svelte index 232a6819a..f04469a88 100644 --- a/src/components/annotations/Annotation.svelte +++ b/src/components/annotations/Annotation.svelte @@ -35,6 +35,9 @@
{#each annotations as annotation} + {@const secondary = + annotation.kind === 'secondaryMajor' || + annotation.kind === 'secondaryMinor'} {#if annotation.conflict}

{#if editor} @@ -53,9 +56,7 @@

{/if}
{#snippet content()} @@ -128,12 +129,13 @@ border-color: var(--wordplay-evaluation-color); } - .annotation.primary { + .annotation.primaryMajor, + .annotation.secondaryMajor { border-color: var(--wordplay-error); } - .annotation.secondary, - .annotation.minor { + .annotation.primaryMinor, + .annotation.secondaryMinor { border-color: var(--wordplay-warning); } diff --git a/src/components/annotations/Annotations.svelte b/src/components/annotations/Annotations.svelte index 2c10a49e6..b7a9169e3 100644 --- a/src/components/annotations/Annotations.svelte +++ b/src/components/annotations/Annotations.svelte @@ -3,7 +3,12 @@ node: Node; element: Element | null; messages: Markup[]; - kind: 'step' | 'primary' | 'secondary' | 'minor'; + kind: + | 'step' + | 'primaryMajor' + | 'primaryMinor' + | 'secondaryMajor' + | 'secondaryMinor'; context: Context; resolutions: Resolution[]; conflict?: ConflictLocaleAccessor; @@ -18,7 +23,7 @@ IncrementLiteral, ShowMenu, toShortcut, - } from '@components/editor/util/Commands'; + } from '@components/editor/commands/Commands'; import Speech from '@components/lore/Speech.svelte'; import CommandButton from '@components/widgets/CommandButton.svelte'; import Expander from '@components/widgets/Expander.svelte'; @@ -28,7 +33,7 @@ ConflictLocaleAccessor, Resolution, } from '@conflicts/Conflict'; - import type Caret from '@edit/Caret'; + import type Caret from '@edit/caret/Caret'; import { docToMarkup } from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import Context from '@nodes/Context'; @@ -115,9 +120,7 @@ project.getContext(project.getMain()), ), ], - kind: conflict.isMinor() - ? ('minor' as const) - : ('primary' as const), + kind: `primary${conflict.isMinor() ? 'Minor' : 'Major'}` as const, context, // Place the resolutions in the primary node. resolutions: nodes.resolutions ?? [], @@ -137,7 +140,7 @@ ), ], context, - kind: 'secondary' as const, + kind: `secondary${conflict.isMinor() ? 'Minor' : 'Major'}` as const, resolutions: [], }, ] @@ -488,12 +491,13 @@ background: var(--wordplay-evaluation-color); } - .annotation.primary { + .annotation.primaryMajor, + .annotation.secondaryMajor { background: var(--wordplay-error); } - .annotation.secondary, - .annotation.minor { + .annotation.primaryMinor, + .annotation.secondaryMinor { background: var(--wordplay-warning); } diff --git a/src/components/app/Feedback.svelte b/src/components/app/Feedback.svelte index b6794c2cd..7fce8c053 100644 --- a/src/components/app/Feedback.svelte +++ b/src/components/app/Feedback.svelte @@ -9,7 +9,7 @@ import Note from '@components/widgets/Note.svelte'; import TextBox from '@components/widgets/TextBox.svelte'; import TextField from '@components/widgets/TextField.svelte'; - import { locales, Logs } from '@db/Database'; + import { Logs } from '@db/Database'; import { createFeedback, deleteFeedback, @@ -442,12 +442,9 @@ }} > l.ui.dialog.feedback.mode} choice={mode === 'defect' ? 0 : 1} - modes={[ - `${DEFECT_SYMBOL} ${$locales.get((l) => l.ui.dialog.feedback.subheader.defect)}`, - `${IDEA_SYMBOL} ${$locales.get((l) => l.ui.dialog.feedback.subheader.idea)}`, - ]} - descriptions={(l) => l.ui.dialog.feedback.mode} + icons={[DEFECT_SYMBOL, IDEA_SYMBOL]} select={(num) => (mode = num === 0 ? 'defect' : 'idea')} /> diff --git a/src/components/app/HeaderAndExplanation.svelte b/src/components/app/HeaderAndExplanation.svelte new file mode 100644 index 000000000..c67d0c2b3 --- /dev/null +++ b/src/components/app/HeaderAndExplanation.svelte @@ -0,0 +1,21 @@ + + +{#if sub} + text(l).header}> +{:else} +
text(l).header} /> +{/if} + text(l).explanation} /> diff --git a/src/components/app/Link.svelte b/src/components/app/Link.svelte index 712c25032..21d2e3038 100644 --- a/src/components/app/Link.svelte +++ b/src/components/app/Link.svelte @@ -37,38 +37,16 @@ href={to} target={external ? '_blank' : null} class:nowrap - >{@render labelOrChildren()}{#if external}{@render labelOrChildren()}{#if external}{/if} {/if} diff --git a/src/components/concepts/ConceptView.svelte b/src/components/concepts/ConceptView.svelte index 525af4c34..b1a79614b 100644 --- a/src/components/concepts/ConceptView.svelte +++ b/src/components/concepts/ConceptView.svelte @@ -63,7 +63,7 @@ return undefined; } - let node = $derived(concept.getRepresentation()); + let node = $derived(concept.getRepresentation($locales)); // When locales or the concept change, retrieve the URL to the tutorial. $effect(() => { diff --git a/src/components/concepts/ConceptsView.svelte b/src/components/concepts/ConceptsView.svelte index 894c43306..4f6323f78 100644 --- a/src/components/concepts/ConceptsView.svelte +++ b/src/components/concepts/ConceptsView.svelte @@ -8,10 +8,18 @@ category: LocaleTextAccessor; concepts: Concept[]; collapse: boolean; + row: boolean; + describe?: boolean; } - let { category, concepts, collapse }: Props = $props(); + let { + category, + concepts, + collapse, + row, + describe = true, + }: Props = $props(); - + diff --git a/src/components/concepts/Documentation.svelte b/src/components/concepts/Documentation.svelte index 08cfc31da..fa3e2f61f 100644 --- a/src/components/concepts/Documentation.svelte +++ b/src/components/concepts/Documentation.svelte @@ -1,9 +1,17 @@ + + @@ -202,21 +252,49 @@ bind:text={query} fill /> - l.ui.docs.modes} - choice={mode === 'how' ? 0 : 1} - select={(choice) => { - const newMode = choice === 0 ? 'how' : 'language'; - if (mode !== newMode) { - mode = newMode; - path.set([]); - } - }} - modes={[ - $locales.get((l) => l.ui.docs.modes.modes[0]), - $locales.get((l) => l.ui.docs.modes.modes[1]), - ]} - /> + {#if query.length === 0} + l.ui.docs.mode.browse} + icons={[DOCUMENTATION_SYMBOL, IDEA_SYMBOL]} + choice={Modes.indexOf(mode)} + select={(choice) => { + const newMode = Modes[choice]; + if (mode !== newMode) { + mode = newMode; + path.set([]); + } + }} + /> + {#if mode === 'language'} + l.ui.docs.mode.purpose} + choice={Object.keys(Purpose).indexOf(purpose)} + select={(choice) => { + purpose = Object.values(Purpose)[choice]; + path.set([]); + }} + icons={[ + '👤', + '🖥️', + '🖱️', + '?', + BIND_SYMBOL, + getLanguageQuoteOpen($locales.getLocale().language) + + getLanguageQuoteClose($locales.getLocale().language), + MEASUREMENT_SYMBOL, + TRUE_SYMBOL + NONE_SYMBOL, + LIST_OPEN_SYMBOL + LIST_CLOSE_SYMBOL, + SET_OPEN_SYMBOL + SET_CLOSE_SYMBOL, + TABLE_OPEN_SYMBOL + TABLE_CLOSE_SYMBOL, + FORMATTED_SYMBOL + FORMATTED_SYMBOL, + TYPE_SYMBOL, + '', + ]} + wrap + omit={standalone ? [0] : []} + /> + {/if} + {/if} {#if currentConcept} @@ -244,13 +322,18 @@ aria-label={$locales.get((l) => l.ui.docs.label)} onpointerup={handleDrop} bind:this={view} + bind:clientWidth={viewWidth} + bind:clientHeight={viewHeight} >
{#if results} {#each results as [concept, text]}
- + {#if text.length > 1 || concept.getName($locales, false) !== text[0]} {@const match = text[0]} @@ -306,13 +389,13 @@ {:else} {/if} {:else if index} - {#if mode === 'how'} + {#if mode === 'howto'} {#if howTos === undefined} {:else} @@ -336,14 +419,13 @@ node={how.getRepresentation()} concept={how} elide - flip /> {/each}
{/if} {/each} {/if} - {:else} + {:else if purpose === Purpose.Project} {@const projectConcepts = index.getPrimaryConceptsWithPurpose(Purpose.Project)} {#if projectConcepts.length > 0} @@ -351,70 +433,154 @@ category={(l) => l.term.project} concepts={projectConcepts} {collapse} + {row} + describe={false} /> + {:else} + l.ui.docs.purposes.Project.header} + /> + l.ui.docs.note.empty} + /> {/if} - l.term.value} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Value, - )} - {collapse} + {:else if purpose === Purpose.Outputs} + {@const concepts = + index.getPrimaryConceptsWithPurpose(purpose)} + {@const sourceConcept = index.getStructureConcept( + project.shares.output.Data, + )} + {@const outputs: Concept[] = [...index.getInterfaceImplementers( + project.shares.output.Output, + ), ...(sourceConcept ? [ sourceConcept] : [])]} + {@const arrangements: Concept[] = index.getInterfaceImplementers( + project.shares.output.Arrangement, + )} + {@const forms: Concept[] = index.getInterfaceImplementers( + project.shares.output.Form, + )} + {@const styles: Concept[] = concepts.filter( + (c) => c instanceof FunctionConcept || (c instanceof StructureConcept && c.definition === project.shares.output.Sequence) + )} + {@const appearance: Concept[] = concepts.filter((c) => c instanceof StructureConcept && (c.definition === project.shares.output.Color || c.definition === project.shares.output.Aura || c.definition === project.shares.output.Pose))} + {@const other: Concept[] = concepts.filter( + (c) => + !outputs.includes(c) && + !arrangements.includes(c) && + !forms.includes(c) && + !styles.includes(c) && + !appearance.includes(c) && !((c instanceof StructureConcept) && (c.definition === project.shares.output.Form || c.definition === project.shares.output.Arrangement)) + + )} + l.ui.docs.purposes.Outputs} + sub /> - l.term.evaluate} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Evaluate, - )} - {collapse} + + l.ui.docs.header.arrangements} + sub /> - l.term.bind} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Bind, - )} + - l.term.decide} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Decide, - )} - {collapse} + l.ui.docs.header.forms} + sub /> - l.term.input} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Input, - )} - {collapse} + + l.ui.docs.header.appearance} + sub /> - l.term.output} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Output, - )} - {collapse} + + l.ui.docs.header.appearance} + sub /> - l.term.type} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Type, - )} - {collapse} + + l.ui.docs.header.location} + sub /> - l.term.document} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Document, + + {:else if purpose === Purpose.Inputs} + {@const concepts = + index.getPrimaryConceptsWithPurpose(purpose)} + {@const controls: Concept[] = concepts.filter( + (c) => c instanceof NodeConcept, + )} + {@const streams = concepts.filter( + (c) => !controls.includes(c), + )} + l.ui.docs.purposes.Inputs} + sub + /> + + l.ui.docs.header.reactions} + sub + /> + + {:else if [Purpose.Text, Purpose.Numbers, Purpose.Truth, Purpose.Lists, Purpose.Maps, Purpose.Tables].includes(purpose)} + + {@const primary = index + .getPrimaryConceptsWithPurpose(purpose) + .filter( + (s) => + !( + s instanceof NodeConcept && + (s.template instanceof Literal || + s.template instanceof CompositeLiteral) + ), )} + {@const functions = primary + .map((p) => + Array.from(p.getSubConcepts()).filter( + (c) => c instanceof FunctionConcept, + ), + ) + .flat()} + {@const conversions = primary + .map((p) => + Array.from(p.getSubConcepts()).filter( + (c) => c instanceof ConversionConcept, + ), + ) + .flat()} + l.ui.docs.purposes[purpose]} + sub + /> + - l.term.source} - concepts={index.getPrimaryConceptsWithPurpose( - Purpose.Source, - )} + l.ui.docs.header.functions} + sub + /> + + l.ui.docs.header.conversions} + sub + /> + + {:else} + l.ui.docs.purposes[purpose]} + sub + /> + {/if} {:else} @@ -460,15 +626,14 @@ border-bottom: var(--wordplay-border-width) solid var(--wordplay-border-color); padding: var(--wordplay-spacing); - padding-top: 0; display: flex; flex-direction: column; gap: var(--wordplay-spacing); position: sticky; top: 0; z-index: 1; - margin-left: calc(var(--wordplay-spacing) / 2); - margin-right: calc(var(--wordplay-spacing) / 2); + margin-left: var(--wordplay-spacing-half); + margin-right: var(--wordplay-spacing-half); } .path { diff --git a/src/components/concepts/DocumentationText.ts b/src/components/concepts/DocumentationText.ts new file mode 100644 index 000000000..cbc42b7ea --- /dev/null +++ b/src/components/concepts/DocumentationText.ts @@ -0,0 +1,90 @@ +import type { HowToCategories } from '@concepts/HowTo'; +import type Purpose from '@concepts/Purpose'; +import type { Template } from '@locale/LocaleText'; +import type { HeaderAndExplanationText, ModeText } from '@locale/UITexts'; + +type DocumentationText = { + /** The ARIA label for the palette section. */ + label: string; + /** A link to a concept in documentation */ + link: Template; + /** A link to the tutorial for a concept */ + learn: string; + /** Shown if documentation is missing for a concept */ + nodoc: string; + button: { + /** The home button in the docs tile */ + home: string; + /** The back button in the docs tile */ + back: string; + /** The toggle to expand and collapse concept groups */ + toggle: string; + }; + field: { + /** The search text field */ + search: string; + }; + note: { + /** The project has no concepts. */ + empty: string; + }; + /** Modes in the guide */ + mode: { + browse: ModeText<[string, string]>; + purpose: ModeText< + [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + >; + }; + header: { + /** Function inputs header */ + inputs: HeaderAndExplanationText; + /** Structue interfaces header */ + interfaces: HeaderAndExplanationText; + /** Structure properties header */ + properties: HeaderAndExplanationText; + /** Functions */ + functions: HeaderAndExplanationText; + /** Conversions header */ + conversions: HeaderAndExplanationText; + /** Arrangements header */ + arrangements: HeaderAndExplanationText; + /** Forms header */ + forms: HeaderAndExplanationText; + /** Appearance header */ + appearance: HeaderAndExplanationText; + /** Animation header */ + animation: HeaderAndExplanationText; + /** Location header */ + location: HeaderAndExplanationText; + /** Reactions header */ + reactions: HeaderAndExplanationText; + }; + /** Labels for concept categories */ + purposes: { + [key in keyof typeof Purpose]: HeaderAndExplanationText; + }; + /** Everything related to how to content */ + how: { + /** The category names */ + category: Record; + /** The subheader for related how to's */ + related: string; + }; +}; + +export { type DocumentationText as default }; diff --git a/src/components/concepts/ExampleUI.svelte b/src/components/concepts/ExampleUI.svelte index 9ae8fd1a2..5b29071c5 100644 --- a/src/components/concepts/ExampleUI.svelte +++ b/src/components/concepts/ExampleUI.svelte @@ -1,5 +1,5 @@ - -{#if $blocks} -
- -
-{:else} - -{/if} - - diff --git a/src/components/editor/BindView.svelte b/src/components/editor/BindView.svelte deleted file mode 100644 index 4f6697eac..000000000 --- a/src/components/editor/BindView.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -{#if $blocks} -
- -
{#if node.value}{/if} -
-
-{:else} - {#if node.value}{/if} -{/if} - - diff --git a/src/components/editor/BlockView.svelte b/src/components/editor/BlockView.svelte deleted file mode 100644 index b44d9f6e9..000000000 --- a/src/components/editor/BlockView.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if $blocks} -
1 ? 'column' : 'row'} - />
-{:else} - -{/if} - - diff --git a/src/components/editor/BooleanLiteralView.svelte b/src/components/editor/BooleanLiteralView.svelte deleted file mode 100644 index 3d8b2eb42..000000000 --- a/src/components/editor/BooleanLiteralView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/BooleanTypeView.svelte b/src/components/editor/BooleanTypeView.svelte deleted file mode 100644 index 533bf871a..000000000 --- a/src/components/editor/BooleanTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/BorrowView.svelte b/src/components/editor/BorrowView.svelte deleted file mode 100644 index 595d286b5..000000000 --- a/src/components/editor/BorrowView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ChangedView.svelte b/src/components/editor/ChangedView.svelte deleted file mode 100644 index c5143cd1d..000000000 --- a/src/components/editor/ChangedView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/CodeOptions.svelte b/src/components/editor/CodeOptions.svelte deleted file mode 100644 index 7258bc252..000000000 --- a/src/components/editor/CodeOptions.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
- {current.getText()} - -
- - diff --git a/src/components/editor/ConceptLinkView.svelte b/src/components/editor/ConceptLinkView.svelte deleted file mode 100644 index 51b90a98f..000000000 --- a/src/components/editor/ConceptLinkView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/ConditionalView.svelte b/src/components/editor/ConditionalView.svelte deleted file mode 100644 index 720e8060f..000000000 --- a/src/components/editor/ConditionalView.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if $blocks} - - -{:else} - -{/if} diff --git a/src/components/editor/ConversionDefinitionView.svelte b/src/components/editor/ConversionDefinitionView.svelte deleted file mode 100644 index e2442783f..000000000 --- a/src/components/editor/ConversionDefinitionView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ConversionTypeView.svelte b/src/components/editor/ConversionTypeView.svelte deleted file mode 100644 index 14d2270df..000000000 --- a/src/components/editor/ConversionTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ConvertView.svelte b/src/components/editor/ConvertView.svelte deleted file mode 100644 index edab97b9b..000000000 --- a/src/components/editor/ConvertView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/DeleteView.svelte b/src/components/editor/DeleteView.svelte deleted file mode 100644 index faba60459..000000000 --- a/src/components/editor/DeleteView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/DimensionView.svelte b/src/components/editor/DimensionView.svelte deleted file mode 100644 index cc79cc624..000000000 --- a/src/components/editor/DimensionView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/DocView.svelte b/src/components/editor/DocView.svelte deleted file mode 100644 index 7aa05a2fe..000000000 --- a/src/components/editor/DocView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/DocsView.svelte b/src/components/editor/DocsView.svelte deleted file mode 100644 index 10c99f948..000000000 --- a/src/components/editor/DocsView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/DocumentedExpressionView.svelte b/src/components/editor/DocumentedExpressionView.svelte deleted file mode 100644 index a955e1cf8..000000000 --- a/src/components/editor/DocumentedExpressionView.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -{#if $blocks} -
- -
-{:else} - -{/if} - - diff --git a/src/components/editor/Editor.svelte b/src/components/editor/Editor.svelte index 00817302a..0e02cd0a1 100644 --- a/src/components/editor/Editor.svelte +++ b/src/components/editor/Editor.svelte @@ -6,14 +6,29 @@ - -{#if $blocks} -
- - {#each node.inputs as input}{/each}{#if nextBind && menuPosition} -   - {/if} -
-{:else} - {#each node.inputs as input}{/each}{#if nextBind && menuPosition} -   - {/if} -{/if} - - diff --git a/src/components/editor/ExampleView.svelte b/src/components/editor/ExampleView.svelte deleted file mode 100644 index 0bbe893e1..000000000 --- a/src/components/editor/ExampleView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ExpressionPlaceholderView.svelte b/src/components/editor/ExpressionPlaceholderView.svelte deleted file mode 100644 index 2b1b20532..000000000 --- a/src/components/editor/ExpressionPlaceholderView.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -{#if node.placeholder}{:else if placeholder}{placeholder}{/if}{#if node.type}{:else if inferredType && !(inferredType instanceof UnknownType || inferredType instanceof AnyType)}•
{/if}
{#if caret}{/if} - - diff --git a/src/components/editor/FormattedLiteralView.svelte b/src/components/editor/FormattedLiteralView.svelte deleted file mode 100644 index 971eabaf5..000000000 --- a/src/components/editor/FormattedLiteralView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/FormattedTranslationView.svelte b/src/components/editor/FormattedTranslationView.svelte deleted file mode 100644 index de262c121..000000000 --- a/src/components/editor/FormattedTranslationView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/FormattedTypeView.svelte b/src/components/editor/FormattedTypeView.svelte deleted file mode 100644 index 50797d10b..000000000 --- a/src/components/editor/FormattedTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/FunctionDefinitionView.svelte b/src/components/editor/FunctionDefinitionView.svelte deleted file mode 100644 index f9837180a..000000000 --- a/src/components/editor/FunctionDefinitionView.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/src/components/editor/FunctionTypeView.svelte b/src/components/editor/FunctionTypeView.svelte deleted file mode 100644 index 6812bbafb..000000000 --- a/src/components/editor/FunctionTypeView.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/components/editor/InitialView.svelte b/src/components/editor/InitialView.svelte deleted file mode 100644 index 256a7c29e..000000000 --- a/src/components/editor/InitialView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/InputView.svelte b/src/components/editor/InputView.svelte deleted file mode 100644 index c6e21dadb..000000000 --- a/src/components/editor/InputView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/InsertView.svelte b/src/components/editor/InsertView.svelte deleted file mode 100644 index 989cd5dac..000000000 --- a/src/components/editor/InsertView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/IsLocaleView.svelte b/src/components/editor/IsLocaleView.svelte deleted file mode 100644 index 7dc2272b6..000000000 --- a/src/components/editor/IsLocaleView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/IsView.svelte b/src/components/editor/IsView.svelte deleted file mode 100644 index 214d1b26a..000000000 --- a/src/components/editor/IsView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/KeyValueView.svelte b/src/components/editor/KeyValueView.svelte deleted file mode 100644 index 2e1aa2a55..000000000 --- a/src/components/editor/KeyValueView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/LanguageView.svelte b/src/components/editor/LanguageView.svelte deleted file mode 100644 index 1feda7e44..000000000 --- a/src/components/editor/LanguageView.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/src/components/editor/ListAccessView.svelte b/src/components/editor/ListAccessView.svelte deleted file mode 100644 index 808c9aadc..000000000 --- a/src/components/editor/ListAccessView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ListLiteralView.svelte b/src/components/editor/ListLiteralView.svelte deleted file mode 100644 index 8922c09fb..000000000 --- a/src/components/editor/ListLiteralView.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/components/editor/ListTypeView.svelte b/src/components/editor/ListTypeView.svelte deleted file mode 100644 index e5830f9f1..000000000 --- a/src/components/editor/ListTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/MapLiteralView.svelte b/src/components/editor/MapLiteralView.svelte deleted file mode 100644 index 566f14935..000000000 --- a/src/components/editor/MapLiteralView.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/components/editor/MapTypeView.svelte b/src/components/editor/MapTypeView.svelte deleted file mode 100644 index 3ee1271e4..000000000 --- a/src/components/editor/MapTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/MarkupView.svelte b/src/components/editor/MarkupView.svelte deleted file mode 100644 index ef3c4d88d..000000000 --- a/src/components/editor/MarkupView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/MatchView.svelte b/src/components/editor/MatchView.svelte deleted file mode 100644 index a189be134..000000000 --- a/src/components/editor/MatchView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/MenuTrigger.svelte b/src/components/editor/MenuTrigger.svelte deleted file mode 100644 index eb7204090..000000000 --- a/src/components/editor/MenuTrigger.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - event.key === 'Enter' || event.key === ' ' ? show(event) : undefined} - >▾ - - diff --git a/src/components/editor/NameTokenEditor.svelte b/src/components/editor/NameTokenEditor.svelte deleted file mode 100644 index 4e7cb5283..000000000 --- a/src/components/editor/NameTokenEditor.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - { - const tokens = toTokens(newName); - return tokens.remaining() === 2 && - tokens.nextIsOneOf(Sym.Name, Sym.Placeholder) - ? true - : (l) => l.ui.source.error.invalidName; - }} - creator={(text) => { - if (caret && $caret) { - const parent = project.getRoot(name)?.getParent(name); - if (parent instanceof Name) { - const edit = $caret.rename(parent, text, project, 0); - if (edit) return [edit[2].name, edit[0]]; - } - } - - return new NameToken(text); - }} -/> diff --git a/src/components/editor/NameTypeView.svelte b/src/components/editor/NameTypeView.svelte deleted file mode 100644 index 1d5c0a63d..000000000 --- a/src/components/editor/NameTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/NameView.svelte b/src/components/editor/NameView.svelte deleted file mode 100644 index 5dcf89b81..000000000 --- a/src/components/editor/NameView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/NodeSequenceView.svelte b/src/components/editor/NodeSequenceView.svelte deleted file mode 100644 index 10ca17469..000000000 --- a/src/components/editor/NodeSequenceView.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - - -{#if $blocks} -
- {#each nodes as node} - {/each} -
-{:else} - {#if hiddenBefore > 0} - {/if}{#each visible as node}{/each}{#if hiddenAfter > 0}{/if} -{/if} - - diff --git a/src/components/editor/NodeView.svelte b/src/components/editor/NodeView.svelte deleted file mode 100644 index 743e4b5dd..000000000 --- a/src/components/editor/NodeView.svelte +++ /dev/null @@ -1,285 +0,0 @@ - - - -{#snippet blockSpace(firstToken: Token)} - {@const hasSpace = symbolOccurs(space, ' ') || symbolOccurs(space, '\t')} - {@const lines = Array.from( - Array(Math.max(0, countSymbolOccurences(space, '\n'))).keys(), - )}{#if hasSpace || lines.length > 0}{#key $insertion} - {#if hasSpace}
{#if firstToken && $insertion?.token === firstToken}{/if} 
{:else}{#each lines as line}
{#if $insertion && $insertion.list[$insertion.index] === node && $insertion.line === line}{/if}
{/each}{/if}
{/key}{/if} -{/snippet} - - -{#if node !== undefined} - - {#if !hide && firstToken && spaceRoot === node} - {#if $blocks} - {@render blockSpace(firstToken)} - {:else} - - {/if} - {/if}
- {#if value && node.isUndelimited()}{EVAL_OPEN_SYMBOL}{/if}{#if value}{#if node.isUndelimited()}{EVAL_CLOSE_SYMBOL}{/if}
{/if} -
-{/if} - - diff --git a/src/components/editor/NoneLiteralView.svelte b/src/components/editor/NoneLiteralView.svelte deleted file mode 100644 index 3c35434c1..000000000 --- a/src/components/editor/NoneLiteralView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/NoneTypeView.svelte b/src/components/editor/NoneTypeView.svelte deleted file mode 100644 index 00d2977da..000000000 --- a/src/components/editor/NoneTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/NumberLiteralView.svelte b/src/components/editor/NumberLiteralView.svelte deleted file mode 100644 index 6f21feb78..000000000 --- a/src/components/editor/NumberLiteralView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/NumberTokenEditor.svelte b/src/components/editor/NumberTokenEditor.svelte deleted file mode 100644 index 4c4f9c1b1..000000000 --- a/src/components/editor/NumberTokenEditor.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - { - const tokens = toTokens(newNumber); - return tokens.remaining() === 2 && tokens.nextIs(Sym.Number) - ? true - : (l) => l.ui.palette.error.nan; - }} - creator={(text) => new Token(text, Sym.Number)} -/> diff --git a/src/components/editor/NumberTypeView.svelte b/src/components/editor/NumberTypeView.svelte deleted file mode 100644 index 2118ac9cc..000000000 --- a/src/components/editor/NumberTypeView.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -{#if node.unit instanceof Node}{/if} diff --git a/src/components/editor/OperatorEditor.svelte b/src/components/editor/OperatorEditor.svelte deleted file mode 100644 index e5284c2a8..000000000 --- a/src/components/editor/OperatorEditor.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/src/components/editor/OtherwiseView.svelte b/src/components/editor/OtherwiseView.svelte deleted file mode 100644 index da2f1ccee..000000000 --- a/src/components/editor/OtherwiseView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ParagraphView.svelte b/src/components/editor/ParagraphView.svelte deleted file mode 100644 index dddd95e1e..000000000 --- a/src/components/editor/ParagraphView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/PreviousView.svelte b/src/components/editor/PreviousView.svelte deleted file mode 100644 index 0231a9dde..000000000 --- a/src/components/editor/PreviousView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ProgramView.svelte b/src/components/editor/ProgramView.svelte deleted file mode 100644 index 820055adb..000000000 --- a/src/components/editor/ProgramView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/PropertyBindView.svelte b/src/components/editor/PropertyBindView.svelte deleted file mode 100644 index c89300cd2..000000000 --- a/src/components/editor/PropertyBindView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/PropertyReferenceView.svelte b/src/components/editor/PropertyReferenceView.svelte deleted file mode 100644 index fa7605d59..000000000 --- a/src/components/editor/PropertyReferenceView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ReactionView.svelte b/src/components/editor/ReactionView.svelte deleted file mode 100644 index d6e3e75cc..000000000 --- a/src/components/editor/ReactionView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ReferenceTokenEditor.svelte b/src/components/editor/ReferenceTokenEditor.svelte deleted file mode 100644 index 24cde935b..000000000 --- a/src/components/editor/ReferenceTokenEditor.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/RowView.svelte b/src/components/editor/RowView.svelte deleted file mode 100644 index 46b43400b..000000000 --- a/src/components/editor/RowView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/SelectView.svelte b/src/components/editor/SelectView.svelte deleted file mode 100644 index 33c879795..000000000 --- a/src/components/editor/SelectView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/SetLiteralView.svelte b/src/components/editor/SetLiteralView.svelte deleted file mode 100644 index 4a2536192..000000000 --- a/src/components/editor/SetLiteralView.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/components/editor/SetOrMapAccessView.svelte b/src/components/editor/SetOrMapAccessView.svelte deleted file mode 100644 index 62631a2df..000000000 --- a/src/components/editor/SetOrMapAccessView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/SetTypeView.svelte b/src/components/editor/SetTypeView.svelte deleted file mode 100644 index 84ba6957b..000000000 --- a/src/components/editor/SetTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/SourceView.svelte b/src/components/editor/SourceView.svelte deleted file mode 100644 index 8f293de93..000000000 --- a/src/components/editor/SourceView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/Space.svelte b/src/components/editor/Space.svelte deleted file mode 100644 index 077f9280d..000000000 --- a/src/components/editor/Space.svelte +++ /dev/null @@ -1,114 +0,0 @@ - - - -{#key [$spaceIndicator, space, line, $showLines, insertionIndex]} - - {#if first && $showLines}
1
{/if}{#each beforeSpacesByLine as s, index}{#if index > 0}
{#if firstLine !== undefined}
{firstLine + index + 1}
{/if}
{/if}{#if s === ''}​{:else}{s}{/if}{:else}​{/each}{#if insertion}{/if}
{#each afterSpacesByLine as s, index}{#if index > 0}
{#if firstLine !== undefined}
{firstLine + - beforeSpacesByLine.length + - index}
{/if}
{/if}{s}{/each}
-{/key} - - diff --git a/src/components/editor/SpreadView.svelte b/src/components/editor/SpreadView.svelte deleted file mode 100644 index 23339d838..000000000 --- a/src/components/editor/SpreadView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/StreamTypeView.svelte b/src/components/editor/StreamTypeView.svelte deleted file mode 100644 index 7b5d1210a..000000000 --- a/src/components/editor/StreamTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/StructureDefinitionView.svelte b/src/components/editor/StructureDefinitionView.svelte deleted file mode 100644 index 06bec3132..000000000 --- a/src/components/editor/StructureDefinitionView.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - -{#if $blocks} -
- -
-
-{:else} - -{/if} - - diff --git a/src/components/editor/TableLiteralView.svelte b/src/components/editor/TableLiteralView.svelte deleted file mode 100644 index e64b3349d..000000000 --- a/src/components/editor/TableLiteralView.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/src/components/editor/TableTypeView.svelte b/src/components/editor/TableTypeView.svelte deleted file mode 100644 index 2f3321f58..000000000 --- a/src/components/editor/TableTypeView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/TextLiteralView.svelte b/src/components/editor/TextLiteralView.svelte deleted file mode 100644 index bdbe169bd..000000000 --- a/src/components/editor/TextLiteralView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/TextTypeView.svelte b/src/components/editor/TextTypeView.svelte deleted file mode 100644 index 2ee8be8b3..000000000 --- a/src/components/editor/TextTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/ThisView.svelte b/src/components/editor/ThisView.svelte deleted file mode 100644 index e7c569605..000000000 --- a/src/components/editor/ThisView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/TranslationView.svelte b/src/components/editor/TranslationView.svelte deleted file mode 100644 index 85096e7b4..000000000 --- a/src/components/editor/TranslationView.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -{#if $blocks} - -{:else} - -{/if} diff --git a/src/components/editor/TypeInputsView.svelte b/src/components/editor/TypeInputsView.svelte deleted file mode 100644 index 377941a94..000000000 --- a/src/components/editor/TypeInputsView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/TypePlaceholderView.svelte b/src/components/editor/TypePlaceholderView.svelte deleted file mode 100644 index 5e9201d68..000000000 --- a/src/components/editor/TypePlaceholderView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/TypeVariableView.svelte b/src/components/editor/TypeVariableView.svelte deleted file mode 100644 index 89a757f3f..000000000 --- a/src/components/editor/TypeVariableView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/TypeVariablesView.svelte b/src/components/editor/TypeVariablesView.svelte deleted file mode 100644 index 9e1c39098..000000000 --- a/src/components/editor/TypeVariablesView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/UnaryEvaluateView.svelte b/src/components/editor/UnaryEvaluateView.svelte deleted file mode 100644 index cd588ff3b..000000000 --- a/src/components/editor/UnaryEvaluateView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/UnionTypeView.svelte b/src/components/editor/UnionTypeView.svelte deleted file mode 100644 index e0e3b358a..000000000 --- a/src/components/editor/UnionTypeView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/UnitView.svelte b/src/components/editor/UnitView.svelte deleted file mode 100644 index 54c2784c6..000000000 --- a/src/components/editor/UnitView.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/components/editor/UpdateView.svelte b/src/components/editor/UpdateView.svelte deleted file mode 100644 index 147b2f3cf..000000000 --- a/src/components/editor/UpdateView.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/components/editor/VariableTypeView.svelte b/src/components/editor/VariableTypeView.svelte deleted file mode 100644 index 80d53b24e..000000000 --- a/src/components/editor/VariableTypeView.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/editor/WebLinkView.svelte b/src/components/editor/WebLinkView.svelte deleted file mode 100644 index 60ef0651b..000000000 --- a/src/components/editor/WebLinkView.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if editing} - -{:else} - {node.description?.getText() ?? ''} -{/if} diff --git a/src/components/editor/WordsTokenEditor.svelte b/src/components/editor/WordsTokenEditor.svelte deleted file mode 100644 index 003da725b..000000000 --- a/src/components/editor/WordsTokenEditor.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - newWords.length === 0 || !WordsRegEx.test(newWords) - ? (l) => l.ui.source.error.invalidWords - : true} - creator={(text) => (text === '' ? undefined : new Token(text, Sym.Words))} -/> diff --git a/src/components/editor/WordsView.svelte b/src/components/editor/WordsView.svelte deleted file mode 100644 index aa4a2d5f4..000000000 --- a/src/components/editor/WordsView.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - diff --git a/src/components/editor/blocks/EmptyView.svelte b/src/components/editor/blocks/EmptyView.svelte new file mode 100644 index 000000000..d2f6a2f70 --- /dev/null +++ b/src/components/editor/blocks/EmptyView.svelte @@ -0,0 +1,77 @@ + + +{#if style !== 'hide'} +
+ Caret.isTokenTextBlockEditable(new Token('', kind), node), + )} + data-nodeid={node.id} + data-field={field} + > + {#if style === 'label' && $project && format.root} + {@const label = fieldInfo?.label} + {#if label} + + {/if} + {/if} + {#if format.editable} + + {/if} +
+{/if} + + diff --git a/src/components/editor/blocks/Flow.svelte b/src/components/editor/blocks/Flow.svelte new file mode 100644 index 000000000..2d5cd0a40 --- /dev/null +++ b/src/components/editor/blocks/Flow.svelte @@ -0,0 +1,38 @@ + + +
+ {@render children()} +
+ + diff --git a/src/components/editor/CaretView.svelte b/src/components/editor/caret/CaretView.svelte similarity index 90% rename from src/components/editor/CaretView.svelte rename to src/components/editor/caret/CaretView.svelte index 2f031a69e..4dbdbf4ad 100644 --- a/src/components/editor/CaretView.svelte +++ b/src/components/editor/caret/CaretView.svelte @@ -20,13 +20,15 @@ editor: HTMLElement, caret: Caret, getTokenViews: () => HTMLElement[], - ): Caret | undefined { + ): Caret | LocaleTextAccessor { // Find the token view that the caret is in. const currentToken = caret.position instanceof Node ? caret.position : caret.getToken(); - if (currentToken === undefined) return undefined; + if (currentToken === undefined) + return (l) => l.ui.source.cursor.ignored.noMove; const currentTokenView = getNodeView(editor, currentToken); - if (currentTokenView === null) return undefined; + if (currentTokenView === null) + return (l) => l.ui.source.cursor.ignored.noMove; const bounds = currentTokenView.getBoundingClientRect(); const vertical = getVerticalCenterOfBounds(bounds); const horizontal = getHorizontalCenterOfBounds(bounds); @@ -51,7 +53,7 @@ .filter( (node) => node.node instanceof Token && - Caret.isBlockEditable(node.node) && + Caret.isTokenBlockEditable(node.node) && // Filter out nodes in the wrong direction (direction < 0 ? node.vertical < 0 : node.vertical > 0) && // Filter out nodes that are too close vertically @@ -68,7 +70,7 @@ const closest = nearest[0]; if (closest && closest.node) return caret.withPosition(closest.node); - else return undefined; + else return (l) => l.ui.source.cursor.ignored.noMove; } export function getTokenView( @@ -87,6 +89,8 @@ + { + location = computeLocation(); + }} +/> +
{#if !blocks}
{/if} diff --git a/src/components/editor/InsertionPointView.svelte b/src/components/editor/caret/InsertionPointView.svelte similarity index 100% rename from src/components/editor/InsertionPointView.svelte rename to src/components/editor/caret/InsertionPointView.svelte diff --git a/src/components/editor/util/Clipboard.ts b/src/components/editor/commands/Clipboard.ts similarity index 56% rename from src/components/editor/util/Clipboard.ts rename to src/components/editor/commands/Clipboard.ts index fd41c8686..34f0890fe 100644 --- a/src/components/editor/util/Clipboard.ts +++ b/src/components/editor/commands/Clipboard.ts @@ -1,24 +1,25 @@ +import type { LocaleTextAccessor } from '@locale/Locales'; import type Node from '@nodes/Node'; import type Spaces from '../../../parser/Spaces'; export function copyNode( node: Node, spaces: Spaces, -): Promise { +): Promise { return toClipboard(node.toWordplay(spaces).trim()); } -export async function toClipboard(text: string): Promise { +export async function toClipboard( + text: string, +): Promise { if (navigator.clipboard && navigator.clipboard.writeText) { try { navigator.clipboard.writeText(text); return true; } catch (err) { - console.error(err, 'Failed to copy to clipboard'); - return undefined; + return (l) => l.ui.source.cursor.ignored.noClipboard; } } else { - console.error('Clipboard API not supported'); - return undefined; + return (l) => l.ui.source.cursor.ignored.noClipboard; } } diff --git a/src/components/editor/util/Commands.ts b/src/components/editor/commands/Commands.ts similarity index 93% rename from src/components/editor/util/Commands.ts rename to src/components/editor/commands/Commands.ts index 2b5671aa8..9d1cf99f1 100644 --- a/src/components/editor/util/Commands.ts +++ b/src/components/editor/commands/Commands.ts @@ -1,3 +1,4 @@ +import Caret from '@edit/caret/Caret'; import Node from '@nodes/Node'; import { ALL_SYMBOL, @@ -30,10 +31,10 @@ import { TYPE_SYMBOL, UNDO_SYMBOL, } from '@parser/Symbols'; -import type Caret from '../../../edit/Caret'; import { Settings, type Database } from '@db/Database'; -import type LocaleText from '@locale/LocaleText'; +import type Locales from '@locale/Locales'; +import type { LocaleTextAccessor } from '@locale/Locales'; import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; import FunctionDefinition from '@nodes/FunctionDefinition'; import Names from '@nodes/Names'; @@ -44,7 +45,7 @@ import type Evaluator from '@runtime/Evaluator'; import type Project from '../../../db/projects/Project'; import Sym from '../../../nodes/Sym'; import { TileKind } from '../../project/Tile'; -import { moveVisualVertical } from '../CaretView.svelte'; +import { moveVisualVertical } from '../caret/CaretView.svelte'; import { copyNode, toClipboard } from './Clipboard'; import interpret from './interpret'; @@ -52,7 +53,7 @@ export type Command = { /** The iconographic text symbol to use */ symbol: string; /** Gets the locale string from a locale for use in title and aria-label of UI */ - description: (locale: LocaleText) => string; + description: LocaleTextAccessor; /** True if it should be a control in the toolbar */ visible: Visibility; /** The category of command, used to decide where to display controls if visible */ @@ -86,11 +87,13 @@ type CommandResult = // An edit to a whole project | ProjectRevision // An eventual edit to a source file or project - | Promise + | Promise // Handled, but no side effect | true // Not handled - | false; + | false + // Not handled for a reason that should be communicated. + | LocaleTextAccessor; export type CommandContext = { /** The caret for the focused editor */ @@ -99,6 +102,8 @@ export type CommandContext = { editor: boolean; /** The project we're editing */ project: Project; + /** The locales currently selected */ + locales: Locales; /** The evalutor currently evaluating the project */ evaluator: Evaluator; database: Database; @@ -638,6 +643,7 @@ export const ToggleBlocks: Command = { alt: false, control: true, key: 'Backslash', + important: true, execute: ({ toggleBlocks }) => { if (toggleBlocks) { toggleBlocks(!Settings.getBlocks()); @@ -660,7 +666,7 @@ export const InsertSymbol: Command = { typing: true, execute: ({ caret, project, editor, blocks }, key) => { if (editor && caret && key.length === 1) - return caret.insert(key, blocks, project) ?? false; + return caret.insert(key, blocks, project); else return false; }, }; @@ -791,8 +797,8 @@ const Commands: Command[] = [ execute: ({ caret, database, blocks }) => caret ? blocks - ? (caret.moveInlineSemantic(-1) ?? false) - : caret.moveInline( + ? (caret.moveInlineBlock(-1) ?? false) + : caret.moveInlineText( false, database.Locales.getWritingDirection() === 'ltr' ? -1 @@ -826,8 +832,8 @@ const Commands: Command[] = [ execute: ({ caret, database, blocks }) => caret ? blocks - ? (caret.moveInlineSemantic(1) ?? false) - : caret.moveInline( + ? (caret.moveInlineBlock(1) ?? false) + : caret.moveInlineText( false, database.Locales.getWritingDirection() === 'ltr' ? 1 @@ -899,7 +905,7 @@ const Commands: Command[] = [ { symbol: '⬉', description: (l) => l.ui.source.cursor.priorNode, - visible: Visibility.Visible, + visible: Visibility.Invisible, category: Category.Cursor, alt: false, shift: true, @@ -911,7 +917,7 @@ const Commands: Command[] = [ { symbol: '⬈', description: (l) => l.ui.source.cursor.nextNode, - visible: Visibility.Visible, + visible: Visibility.Invisible, category: Category.Cursor, alt: false, control: false, @@ -950,11 +956,10 @@ const Commands: Command[] = [ : caret.getToken(); if (token !== undefined) { const parent = caret.source.root.getParent(token); + if (parent === undefined) return false; return caret .withEntry(undefined) - .withPosition( - parent?.getChildren().length === 1 ? parent : token, - ); + .withPosition(parent.hasOneLeaf() ? parent : token); } } return false; @@ -971,8 +976,42 @@ const Commands: Command[] = [ control: true, key: 'KeyA', keySymbol: 'A', - execute: ({ editor, caret }) => - editor && caret ? caret.withPosition(caret.getProgram()) : false, + execute: ({ editor, caret, blocks }) => { + if (editor && caret) { + // If it blocks mode and in side of a block text editable token, select the whole token.. + if (blocks) { + const tokenAt = caret.tokenExcludingSpace; + const parentAt = tokenAt + ? caret.source.getParentNode(tokenAt) + : undefined; + if ( + tokenAt && + parentAt && + Caret.isTokenTextBlockEditable(tokenAt, parentAt) + ) + return caret.withPosition( + parentAt.hasOneLeaf() ? parentAt : tokenAt, + ); + + const tokenAfter = caret.tokenPrior; + const parentAfter = tokenAfter + ? caret.source.getParentNode(tokenAfter) + : undefined; + if ( + tokenAfter && + parentAfter && + Caret.isTokenTextBlockEditable(tokenAfter, parentAfter) + ) + return caret.withPosition( + parentAfter.hasOneLeaf() ? parentAfter : tokenAfter, + ); + } + + // Select the whole program. + return caret.withPosition(caret.getProgram()); + } + return false; + }, }, { symbol: TAB_SYMBOL, @@ -1032,11 +1071,11 @@ const Commands: Command[] = [ keySymbol: 'F', shift: false, control: false, - execute: ({ caret }) => + execute: ({ caret, locales }) => caret?.insertNode( FunctionDefinition.make( undefined, - Names.make([]), + Names.make([locales.get((l) => l.term.name)]), undefined, [], ExpressionPlaceholder.make(), @@ -1293,7 +1332,7 @@ const Commands: Command[] = [ { symbol: '↲', description: (l) => l.ui.source.cursor.insertLine, - visible: Visibility.Touch, + visible: Visibility.Visible, category: Category.Modify, shift: false, alt: false, @@ -1304,8 +1343,8 @@ const Commands: Command[] = [ !editor || caret === undefined ? false : caret.isNode() - ? caret.enter() - : (caret.insert('\n', blocks, project) ?? false), + ? caret.enter(blocks) + : caret.insert('\n', blocks, project), }, { symbol: '⌫', @@ -1370,7 +1409,7 @@ const Commands: Command[] = [ ).then(() => { return caret.delete(project, false, blocks) ?? true; }); - } else return false; + } else return (l) => l.ui.source.cursor.ignored.noSelection; }, }, { @@ -1389,11 +1428,9 @@ const Commands: Command[] = [ execute: ({ caret }) => { if (caret === undefined) return false; if (caret.isNode()) - return ( - copyNode( - caret.position, - getPreferredSpaces(caret.source), - ) ?? false + return copyNode( + caret.position, + getPreferredSpaces(caret.source), ); else if (caret.isRange()) { return toClipboard( @@ -1404,7 +1441,7 @@ const Commands: Command[] = [ ) .toString(), ); - } else return false; + } else return (l) => l.ui.source.cursor.ignored.noSelection; }, }, { @@ -1423,14 +1460,14 @@ const Commands: Command[] = [ typeof navigator.clipboard !== 'undefined' && navigator.clipboard.read !== undefined, execute: async ({ editor, caret, blocks, project }) => { - if (!editor) return undefined; + if (!editor) return (l) => l.ui.source.cursor.ignored.noEditor; // Make sure clipboard is supported. if ( navigator.clipboard === undefined || caret === undefined || navigator.clipboard.read === undefined ) - return undefined; + return (l) => l.ui.source.cursor.ignored.noClipboard; const items = await navigator.clipboard.read(); for (const item of items) { @@ -1442,7 +1479,7 @@ const Commands: Command[] = [ } } } - return undefined; + return (l) => l.ui.source.cursor.ignored.noClipboardItem; }, }, { @@ -1455,7 +1492,7 @@ const Commands: Command[] = [ alt: undefined, key: '(', active: ({ caret }) => caret?.isNode() ?? false, - execute: ({ caret }) => caret?.wrap('(') ?? false, + execute: ({ project, caret }) => caret?.wrap(project, '(') ?? false, }, { symbol: '[ ]', @@ -1467,7 +1504,7 @@ const Commands: Command[] = [ alt: false, key: '[', active: ({ caret }) => caret?.isNode() ?? false, - execute: ({ caret }) => caret?.wrap('[') ?? false, + execute: ({ project, caret }) => caret?.wrap(project, '[') ?? false, }, IncrementLiteral, DecrementLiteral, diff --git a/src/components/editor/GlyphChooser.svelte b/src/components/editor/commands/GlyphChooser.svelte similarity index 88% rename from src/components/editor/GlyphChooser.svelte rename to src/components/editor/commands/GlyphChooser.svelte index 785a87d05..9dbadbee2 100644 --- a/src/components/editor/GlyphChooser.svelte +++ b/src/components/editor/commands/GlyphChooser.svelte @@ -5,22 +5,22 @@ import { characterToSVG, type Character, - } from '../../db/characters/Character'; - import { CharactersDB, locales } from '../../db/Database'; - import { tokenize } from '../../parser/Tokenizer'; - import NewCharacterButton from '../../routes/characters/NewCharacterButton.svelte'; - import { isEmoji, withColorEmoji } from '../../unicode/emoji'; + } from '../../../db/characters/Character'; + import { CharactersDB, locales } from '../../../db/Database'; + import { tokenize } from '../../../parser/Tokenizer'; + import NewCharacterButton from '../../../routes/characters/NewCharacterButton.svelte'; + import { isEmoji, withColorEmoji } from '../../../unicode/emoji'; import { getCodepoints, getEmoji, type Codepoint, - } from '../../unicode/Unicode'; - import { IdleKind, getEditors } from '../project/Contexts'; - import Button from '../widgets/Button.svelte'; - import CommandButton from '../widgets/CommandButton.svelte'; - import TextField from '../widgets/TextField.svelte'; - import TokenView from './TokenView.svelte'; - import Commands, { Category } from './util/Commands'; + } from '../../../unicode/Unicode'; + import { IdleKind, getEditors } from '../../project/Contexts'; + import Button from '../../widgets/Button.svelte'; + import CommandButton from '../../widgets/CommandButton.svelte'; + import TextField from '../../widgets/TextField.svelte'; + import TokenView from '../tokens/TokenView.svelte'; + import Commands, { Category } from './Commands'; interface Props { sourceID: string; @@ -101,6 +101,7 @@ action={() => insert(glyph)} >{#if token}{:else}{glyph}{/if} {/snippet} diff --git a/src/components/editor/EditorToolbar.svelte b/src/components/editor/commands/Toolbar.svelte similarity index 78% rename from src/components/editor/EditorToolbar.svelte rename to src/components/editor/commands/Toolbar.svelte index c4aa78bb4..3995a7a89 100644 --- a/src/components/editor/EditorToolbar.svelte +++ b/src/components/editor/commands/Toolbar.svelte @@ -1,10 +1,16 @@ + l.ui.dialog.settings.mode.blocks} + choice={$blocks ? 1 : 0} + select={(mode) => Settings.setBlocks(mode === 1)} + labeled={false} + modeLabels={false} +/> + {#each importantNavigateCommands as command} diff --git a/src/components/editor/util/interpret.ts b/src/components/editor/commands/interpret.ts similarity index 100% rename from src/components/editor/util/interpret.ts rename to src/components/editor/commands/interpret.ts diff --git a/src/components/editor/Highlight.svelte b/src/components/editor/highlights/Highlight.svelte similarity index 87% rename from src/components/editor/Highlight.svelte rename to src/components/editor/highlights/Highlight.svelte index b5af3ff8b..d2e26f07d 100644 --- a/src/components/editor/Highlight.svelte +++ b/src/components/editor/highlights/Highlight.svelte @@ -1,6 +1,6 @@
{ + if (event.button !== 0) return; event.preventDefault(); event.stopPropagation(); handleItemClick(entry); }} + onpointerenter={() => { + if (view && menu.getOrganization().includes(entry)) + setKeyboardFocus(view, 'Focusing menu item on pointer enter'); + }} class={`revision ${menu.getSelection() === entry ? 'selected' : ''}`} onfocusin={() => { const index = menu.getSelectionFor(entry); @@ -37,23 +53,13 @@ }} > {#if newNode !== undefined} - {#if entry.isRemoval()} - - {:else} - - {/if} + {:else} {/if} @@ -62,16 +68,23 @@ diff --git a/src/components/editor/nodes/BinaryEvaluateView.svelte b/src/components/editor/nodes/BinaryEvaluateView.svelte new file mode 100644 index 000000000..529dd6c98 --- /dev/null +++ b/src/components/editor/nodes/BinaryEvaluateView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/BindView.svelte b/src/components/editor/nodes/BindView.svelte new file mode 100644 index 000000000..964aef01a --- /dev/null +++ b/src/components/editor/nodes/BindView.svelte @@ -0,0 +1,44 @@ + + +{#if format.block} + {#if !node.docs.isEmpty()} + + {/if} + + {#if node.docs.isEmpty()} + + {/if} + + + + + + + + +{:else} + {#if node.value}{/if} +{/if} diff --git a/src/components/editor/nodes/BlockView.svelte b/src/components/editor/nodes/BlockView.svelte new file mode 100644 index 000000000..e7ec9a2a1 --- /dev/null +++ b/src/components/editor/nodes/BlockView.svelte @@ -0,0 +1,60 @@ + + +{#snippet docs()} + + +{/snippet} + +{#snippet statements()} + 1 + ? 'block' + : 'inline'} + /> +{/snippet} + +{#if format.block} + {#if node.docs.isEmpty()} + + {#if format.editable}{@render docs()}{/if} + 1 ? 'column' : 'row'} + >{@render statements()} + + + {:else} + 1 || !node.docs.isEmpty() + ? 'column' + : 'row'} + >{@render docs()}{@render statements()} + + {/if} +{:else} + +{/if} diff --git a/src/components/editor/nodes/BooleanLiteralView.svelte b/src/components/editor/nodes/BooleanLiteralView.svelte new file mode 100644 index 000000000..619430da4 --- /dev/null +++ b/src/components/editor/nodes/BooleanLiteralView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/BooleanTypeView.svelte b/src/components/editor/nodes/BooleanTypeView.svelte new file mode 100644 index 000000000..30d547b0c --- /dev/null +++ b/src/components/editor/nodes/BooleanTypeView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/BorrowView.svelte b/src/components/editor/nodes/BorrowView.svelte new file mode 100644 index 000000000..e5d6f55c8 --- /dev/null +++ b/src/components/editor/nodes/BorrowView.svelte @@ -0,0 +1,37 @@ + + +{#if format.block} + {#if node}{/if}{#if node.source}{/if} +{:else} + +{/if} diff --git a/src/components/editor/nodes/ChangedView.svelte b/src/components/editor/nodes/ChangedView.svelte new file mode 100644 index 000000000..25aad3118 --- /dev/null +++ b/src/components/editor/nodes/ChangedView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/ConceptLinkView.svelte b/src/components/editor/nodes/ConceptLinkView.svelte new file mode 100644 index 000000000..912b47d00 --- /dev/null +++ b/src/components/editor/nodes/ConceptLinkView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/ConditionalView.svelte b/src/components/editor/nodes/ConditionalView.svelte new file mode 100644 index 000000000..eddef7bbe --- /dev/null +++ b/src/components/editor/nodes/ConditionalView.svelte @@ -0,0 +1,35 @@ + + +{#if format.block} + + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/ConversionDefinitionView.svelte b/src/components/editor/nodes/ConversionDefinitionView.svelte new file mode 100644 index 000000000..9aee40485 --- /dev/null +++ b/src/components/editor/nodes/ConversionDefinitionView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/ConversionTypeView.svelte b/src/components/editor/nodes/ConversionTypeView.svelte new file mode 100644 index 000000000..7a1da5312 --- /dev/null +++ b/src/components/editor/nodes/ConversionTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/ConvertView.svelte b/src/components/editor/nodes/ConvertView.svelte new file mode 100644 index 000000000..349eb9b8d --- /dev/null +++ b/src/components/editor/nodes/ConvertView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/DeleteView.svelte b/src/components/editor/nodes/DeleteView.svelte new file mode 100644 index 000000000..7b1db4471 --- /dev/null +++ b/src/components/editor/nodes/DeleteView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/DimensionView.svelte b/src/components/editor/nodes/DimensionView.svelte new file mode 100644 index 000000000..b567231d7 --- /dev/null +++ b/src/components/editor/nodes/DimensionView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/DocView.svelte b/src/components/editor/nodes/DocView.svelte new file mode 100644 index 000000000..14508fc98 --- /dev/null +++ b/src/components/editor/nodes/DocView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/DocsView.svelte b/src/components/editor/nodes/DocsView.svelte new file mode 100644 index 000000000..018bc74d6 --- /dev/null +++ b/src/components/editor/nodes/DocsView.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/components/editor/nodes/DocumentedExpressionView.svelte b/src/components/editor/nodes/DocumentedExpressionView.svelte new file mode 100644 index 000000000..7bb5a03f0 --- /dev/null +++ b/src/components/editor/nodes/DocumentedExpressionView.svelte @@ -0,0 +1,23 @@ + + +{#if format.block} + +{:else} + +{/if} diff --git a/src/components/editor/nodes/EvaluateView.svelte b/src/components/editor/nodes/EvaluateView.svelte new file mode 100644 index 000000000..42b08b0eb --- /dev/null +++ b/src/components/editor/nodes/EvaluateView.svelte @@ -0,0 +1,49 @@ + + +{#if format.block} + + + + 0 ? 'label' : 'hide'} + wrap + /> + +{:else} + +{/if} diff --git a/src/components/editor/nodes/ExampleView.svelte b/src/components/editor/nodes/ExampleView.svelte new file mode 100644 index 000000000..a41f79e5a --- /dev/null +++ b/src/components/editor/nodes/ExampleView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/ExpressionPlaceholderView.svelte b/src/components/editor/nodes/ExpressionPlaceholderView.svelte new file mode 100644 index 000000000..c45621031 --- /dev/null +++ b/src/components/editor/nodes/ExpressionPlaceholderView.svelte @@ -0,0 +1,54 @@ + + + +{#if node.placeholder}{/if}{#if placeholder}{placeholder}{/if} +{#if format.editable && format.block}{/if} + + diff --git a/src/components/editor/nodes/FormattedLiteralView.svelte b/src/components/editor/nodes/FormattedLiteralView.svelte new file mode 100644 index 000000000..1d869f76e --- /dev/null +++ b/src/components/editor/nodes/FormattedLiteralView.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/components/editor/nodes/FormattedTranslationView.svelte b/src/components/editor/nodes/FormattedTranslationView.svelte new file mode 100644 index 000000000..864b32d3c --- /dev/null +++ b/src/components/editor/nodes/FormattedTranslationView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/FormattedTypeView.svelte b/src/components/editor/nodes/FormattedTypeView.svelte new file mode 100644 index 000000000..46fb756b1 --- /dev/null +++ b/src/components/editor/nodes/FormattedTypeView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/FunctionDefinitionView.svelte b/src/components/editor/nodes/FunctionDefinitionView.svelte new file mode 100644 index 000000000..9f1cfc750 --- /dev/null +++ b/src/components/editor/nodes/FunctionDefinitionView.svelte @@ -0,0 +1,65 @@ + + +{#snippet docs()} + +{/snippet} + +{#if format.block} + {#if !node.docs.isEmpty()}{@render docs()}{/if} + {#if node.docs.isEmpty()}{@render docs()}{/if} + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/FunctionTypeView.svelte b/src/components/editor/nodes/FunctionTypeView.svelte new file mode 100644 index 000000000..10c92e82d --- /dev/null +++ b/src/components/editor/nodes/FunctionTypeView.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/components/editor/nodes/InitialView.svelte b/src/components/editor/nodes/InitialView.svelte new file mode 100644 index 000000000..fa6433535 --- /dev/null +++ b/src/components/editor/nodes/InitialView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/InputView.svelte b/src/components/editor/nodes/InputView.svelte new file mode 100644 index 000000000..2974bbb59 --- /dev/null +++ b/src/components/editor/nodes/InputView.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/components/editor/nodes/InsertView.svelte b/src/components/editor/nodes/InsertView.svelte new file mode 100644 index 000000000..93c2efced --- /dev/null +++ b/src/components/editor/nodes/InsertView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/IsLocaleView.svelte b/src/components/editor/nodes/IsLocaleView.svelte new file mode 100644 index 000000000..e875b2a37 --- /dev/null +++ b/src/components/editor/nodes/IsLocaleView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/IsView.svelte b/src/components/editor/nodes/IsView.svelte new file mode 100644 index 000000000..97e668199 --- /dev/null +++ b/src/components/editor/nodes/IsView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/KeyValueView.svelte b/src/components/editor/nodes/KeyValueView.svelte new file mode 100644 index 000000000..7a88a318e --- /dev/null +++ b/src/components/editor/nodes/KeyValueView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/LanguageView.svelte b/src/components/editor/nodes/LanguageView.svelte new file mode 100644 index 000000000..ca412e678 --- /dev/null +++ b/src/components/editor/nodes/LanguageView.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/src/components/editor/nodes/ListAccessView.svelte b/src/components/editor/nodes/ListAccessView.svelte new file mode 100644 index 000000000..b5392406c --- /dev/null +++ b/src/components/editor/nodes/ListAccessView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/ListLiteralView.svelte b/src/components/editor/nodes/ListLiteralView.svelte new file mode 100644 index 000000000..0d6077714 --- /dev/null +++ b/src/components/editor/nodes/ListLiteralView.svelte @@ -0,0 +1,31 @@ + + +{#if format.block} + + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/ListTypeView.svelte b/src/components/editor/nodes/ListTypeView.svelte new file mode 100644 index 000000000..d79978203 --- /dev/null +++ b/src/components/editor/nodes/ListTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/MapLiteralView.svelte b/src/components/editor/nodes/MapLiteralView.svelte new file mode 100644 index 000000000..6417d6971 --- /dev/null +++ b/src/components/editor/nodes/MapLiteralView.svelte @@ -0,0 +1,30 @@ + + +{#if format.block} + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/MapTypeView.svelte b/src/components/editor/nodes/MapTypeView.svelte new file mode 100644 index 000000000..b4485cce8 --- /dev/null +++ b/src/components/editor/nodes/MapTypeView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/MarkupView.svelte b/src/components/editor/nodes/MarkupView.svelte new file mode 100644 index 000000000..1957def3d --- /dev/null +++ b/src/components/editor/nodes/MarkupView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/MatchView.svelte b/src/components/editor/nodes/MatchView.svelte new file mode 100644 index 000000000..002b26793 --- /dev/null +++ b/src/components/editor/nodes/MatchView.svelte @@ -0,0 +1,39 @@ + + +{#if format.block} + + + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/NameTypeView.svelte b/src/components/editor/nodes/NameTypeView.svelte new file mode 100644 index 000000000..7f445fb38 --- /dev/null +++ b/src/components/editor/nodes/NameTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/NameView.svelte b/src/components/editor/nodes/NameView.svelte new file mode 100644 index 000000000..7cf0122fe --- /dev/null +++ b/src/components/editor/nodes/NameView.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/components/editor/NamesView.svelte b/src/components/editor/nodes/NamesView.svelte similarity index 50% rename from src/components/editor/NamesView.svelte rename to src/components/editor/nodes/NamesView.svelte index 59c05286a..4e89d1388 100644 --- a/src/components/editor/NamesView.svelte +++ b/src/components/editor/nodes/NamesView.svelte @@ -1,12 +1,14 @@ - + diff --git a/src/components/editor/nodes/NodeSequenceView.svelte b/src/components/editor/nodes/NodeSequenceView.svelte new file mode 100644 index 000000000..973ec2444 --- /dev/null +++ b/src/components/editor/nodes/NodeSequenceView.svelte @@ -0,0 +1,272 @@ + + + + +{#snippet insertFeedback()} + +
+{/snippet} + +{#snippet append()}{#if format.editable && nodes.length > 0 && add}
{/if}{/snippet} + +{#snippet list()} + {#if nodes.length > 0 || empty !== 'hide'} + +
+ {@render before()} + {#each visible as node, index} + {#if insertion?.index === index} + {@render insertFeedback()} + {/if} + + {:else} + + {/each} + {@render after()} + {#if nodes.length > 0} + {#if insertion?.index === nodes.length} + {@render insertFeedback()} + {/if} + {#if direction === 'inline'}{@render append()}{/if} + {/if} +
+ {/if} +{/snippet} + +{#snippet before()} + {#if hiddenBefore > 0} + {/if} +{/snippet} + +{#snippet after()} + {#if hiddenAfter > 0}{/if} +{/snippet} + +{#if format.block} + {#if direction === 'block'} +
+ {@render list()} + {@render append()} +
+ {:else} + {@render list()} + {/if} +{:else} + {@render before()}{#each visible as node, index}{/each}{@render after()} +{/if} + + diff --git a/src/components/editor/nodes/NodeView.svelte b/src/components/editor/nodes/NodeView.svelte new file mode 100644 index 000000000..a590782aa --- /dev/null +++ b/src/components/editor/nodes/NodeView.svelte @@ -0,0 +1,398 @@ + + + + +{#snippet textSpace()} + {#if !hide && firstToken !== undefined && spaceRoot === node} + + {/if} +{/snippet} + +{#snippet blockSpace()} + + {#if !hide && firstToken !== undefined && spaceRoot === node && root !== undefined} + {@const tokenPrefersPrecedingSpace = + space.length === 0 && + spaceRoot !== undefined && + root.getFieldOfChild(spaceRoot)?.space === true && + (index === undefined || index > 0)} + + {/if} +{/snippet} + + +{#if node !== undefined} + {#if ComponentView !== undefined} + + {#if !format.block}{@render textSpace()}{:else}{@render blockSpace()}{/if}
{#if value && node.isUndelimited()}{EVAL_OPEN_SYMBOL}{/if}{#if value}{#if node.isUndelimited()}{EVAL_CLOSE_SYMBOL}{/if}
{/if} +
+ {:else} + ! + {/if}{#if replaceable && format.block && node !== undefined}{/if} +{:else if node === undefined && format.block && Array.isArray(path)} + +{/if} + + diff --git a/src/components/editor/nodes/NoneLiteralView.svelte b/src/components/editor/nodes/NoneLiteralView.svelte new file mode 100644 index 000000000..5dca487ea --- /dev/null +++ b/src/components/editor/nodes/NoneLiteralView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/NoneTypeView.svelte b/src/components/editor/nodes/NoneTypeView.svelte new file mode 100644 index 000000000..51749dcb1 --- /dev/null +++ b/src/components/editor/nodes/NoneTypeView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/NumberLiteralView.svelte b/src/components/editor/nodes/NumberLiteralView.svelte new file mode 100644 index 000000000..b6745cbc7 --- /dev/null +++ b/src/components/editor/nodes/NumberLiteralView.svelte @@ -0,0 +1,20 @@ + + +{#if format.editable || (node.unit !== undefined && !node.unit.isEmpty())}{/if} diff --git a/src/components/editor/nodes/NumberTypeView.svelte b/src/components/editor/nodes/NumberTypeView.svelte new file mode 100644 index 000000000..a9913ea07 --- /dev/null +++ b/src/components/editor/nodes/NumberTypeView.svelte @@ -0,0 +1,21 @@ + + +{#if format.editable || (node.unit instanceof Unit && !node.unit.isEmpty())}{/if} diff --git a/src/components/editor/nodes/OtherwiseView.svelte b/src/components/editor/nodes/OtherwiseView.svelte new file mode 100644 index 000000000..9cb72e52f --- /dev/null +++ b/src/components/editor/nodes/OtherwiseView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/ParagraphView.svelte b/src/components/editor/nodes/ParagraphView.svelte new file mode 100644 index 000000000..d7d07fa31 --- /dev/null +++ b/src/components/editor/nodes/ParagraphView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/PreviousView.svelte b/src/components/editor/nodes/PreviousView.svelte new file mode 100644 index 000000000..bad3fbd08 --- /dev/null +++ b/src/components/editor/nodes/PreviousView.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/editor/nodes/ProgramView.svelte b/src/components/editor/nodes/ProgramView.svelte new file mode 100644 index 000000000..6c8119822 --- /dev/null +++ b/src/components/editor/nodes/ProgramView.svelte @@ -0,0 +1,29 @@ + + +{#if format.block} + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/PropertyBindView.svelte b/src/components/editor/nodes/PropertyBindView.svelte new file mode 100644 index 000000000..be95a4227 --- /dev/null +++ b/src/components/editor/nodes/PropertyBindView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/PropertyReferenceView.svelte b/src/components/editor/nodes/PropertyReferenceView.svelte new file mode 100644 index 000000000..aa4d4fca1 --- /dev/null +++ b/src/components/editor/nodes/PropertyReferenceView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/ReactionView.svelte b/src/components/editor/nodes/ReactionView.svelte new file mode 100644 index 000000000..08ca45a98 --- /dev/null +++ b/src/components/editor/nodes/ReactionView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/ReferenceView.svelte b/src/components/editor/nodes/ReferenceView.svelte similarity index 80% rename from src/components/editor/ReferenceView.svelte rename to src/components/editor/nodes/ReferenceView.svelte index 73f537f22..d8f2fbe91 100644 --- a/src/components/editor/ReferenceView.svelte +++ b/src/components/editor/nodes/ReferenceView.svelte @@ -3,16 +3,17 @@ import type Reference from '@nodes/Reference'; import Source from '@nodes/Source'; import type StreamValue from '@values/StreamValue'; - import { animationFactor } from '../../db/Database'; - import type Value from '../../values/Value'; - import { getEvaluation } from '../project/Contexts'; - import NodeView from './NodeView.svelte'; + import { animationFactor } from '../../../db/Database'; + import type Value from '../../../values/Value'; + import { getEvaluation } from '../../project/Contexts'; + import NodeView, { type Format } from './NodeView.svelte'; interface Props { node: Reference; + format: Format; } - let { node }: Props = $props(); + let { node, format }: Props = $props(); let evaluation = getEvaluation(); @@ -58,11 +59,8 @@ }); - - + + diff --git a/src/components/editor/nodes/RowView.svelte b/src/components/editor/nodes/RowView.svelte new file mode 100644 index 000000000..01e7e83b5 --- /dev/null +++ b/src/components/editor/nodes/RowView.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/components/editor/nodes/SelectView.svelte b/src/components/editor/nodes/SelectView.svelte new file mode 100644 index 000000000..6df7c5f09 --- /dev/null +++ b/src/components/editor/nodes/SelectView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/SetLiteralView.svelte b/src/components/editor/nodes/SetLiteralView.svelte new file mode 100644 index 000000000..a853a4c13 --- /dev/null +++ b/src/components/editor/nodes/SetLiteralView.svelte @@ -0,0 +1,30 @@ + + +{#if format.block} + + + + +{:else} + +{/if} diff --git a/src/components/editor/nodes/SetOrMapAccessView.svelte b/src/components/editor/nodes/SetOrMapAccessView.svelte new file mode 100644 index 000000000..8e684b6da --- /dev/null +++ b/src/components/editor/nodes/SetOrMapAccessView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/SetTypeView.svelte b/src/components/editor/nodes/SetTypeView.svelte new file mode 100644 index 000000000..a1c58a7f5 --- /dev/null +++ b/src/components/editor/nodes/SetTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/SourceView.svelte b/src/components/editor/nodes/SourceView.svelte new file mode 100644 index 000000000..86ab97943 --- /dev/null +++ b/src/components/editor/nodes/SourceView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/Space.svelte b/src/components/editor/nodes/Space.svelte new file mode 100644 index 000000000..0a1cf6378 --- /dev/null +++ b/src/components/editor/nodes/Space.svelte @@ -0,0 +1,156 @@ + + + +{#key [$spaceIndicator, space, line, $showLines, insertionIndex]} + + {#if block}{#if space !== ''}{space + .split('') + .map((s) => + s === ' ' + ? invisible || !$spaceIndicator + ? SPACE_TEXT + : EXPLICIT_SPACE_TEXT + : s, + ) + .join('')}{/if} + {:else} + {#if first && $showLines}
1
{/if}{#each beforeSpacesByLine as s, index}{#if index > 0}
{#if firstLine !== undefined}
{firstLine + index + 1}
{/if}
{/if}{#if s === ''}​{:else}{s}{/if}{:else}​{/each}{#if insertion}{/if}
{#each afterSpacesByLine as s, index}{#if index > 0}
{#if firstLine !== undefined}
{firstLine + + beforeSpacesByLine.length + + index}
{/if}
{/if}{s}{/each}
+ {/if}
+{/key} + + diff --git a/src/components/editor/nodes/SpreadView.svelte b/src/components/editor/nodes/SpreadView.svelte new file mode 100644 index 000000000..73fcb7265 --- /dev/null +++ b/src/components/editor/nodes/SpreadView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/StreamTypeView.svelte b/src/components/editor/nodes/StreamTypeView.svelte new file mode 100644 index 000000000..a8fb137f3 --- /dev/null +++ b/src/components/editor/nodes/StreamTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/StructureDefinitionView.svelte b/src/components/editor/nodes/StructureDefinitionView.svelte new file mode 100644 index 000000000..584f4c27b --- /dev/null +++ b/src/components/editor/nodes/StructureDefinitionView.svelte @@ -0,0 +1,72 @@ + + +{#snippet docs()} + +{/snippet} + +{#if format.block} + + {#if !node.docs.isEmpty()}{@render docs()}{/if} + + {#if node.docs.isEmpty()}{@render docs()}{/if} + + {#if node.expression === undefined}{/if} + + {#if node.expression !== undefined} + + {/if} + +{:else} + +{/if} diff --git a/src/components/editor/StructureTypeView.svelte b/src/components/editor/nodes/StructureTypeView.svelte similarity index 61% rename from src/components/editor/StructureTypeView.svelte rename to src/components/editor/nodes/StructureTypeView.svelte index 20404eade..4aebc7a5f 100644 --- a/src/components/editor/StructureTypeView.svelte +++ b/src/components/editor/nodes/StructureTypeView.svelte @@ -1,17 +1,18 @@ - + diff --git a/src/components/editor/nodes/TableLiteralView.svelte b/src/components/editor/nodes/TableLiteralView.svelte new file mode 100644 index 000000000..9c05409db --- /dev/null +++ b/src/components/editor/nodes/TableLiteralView.svelte @@ -0,0 +1,20 @@ + + + 1 ? 'block' : 'inline'} + field="rows" + {format} + empty="label" +/> diff --git a/src/components/editor/nodes/TableTypeView.svelte b/src/components/editor/nodes/TableTypeView.svelte new file mode 100644 index 000000000..b640ceeac --- /dev/null +++ b/src/components/editor/nodes/TableTypeView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/TextLiteralView.svelte b/src/components/editor/nodes/TextLiteralView.svelte new file mode 100644 index 000000000..53018cf4f --- /dev/null +++ b/src/components/editor/nodes/TextLiteralView.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/components/editor/nodes/TextTypeView.svelte b/src/components/editor/nodes/TextTypeView.svelte new file mode 100644 index 000000000..620543724 --- /dev/null +++ b/src/components/editor/nodes/TextTypeView.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/editor/nodes/ThisView.svelte b/src/components/editor/nodes/ThisView.svelte new file mode 100644 index 000000000..577d56df2 --- /dev/null +++ b/src/components/editor/nodes/ThisView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/TranslationView.svelte b/src/components/editor/nodes/TranslationView.svelte new file mode 100644 index 000000000..e4aae2922 --- /dev/null +++ b/src/components/editor/nodes/TranslationView.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/components/editor/nodes/TypeInputsView.svelte b/src/components/editor/nodes/TypeInputsView.svelte new file mode 100644 index 000000000..9c9c93ce4 --- /dev/null +++ b/src/components/editor/nodes/TypeInputsView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/nodes/TypePlaceholderView.svelte b/src/components/editor/nodes/TypePlaceholderView.svelte new file mode 100644 index 000000000..bf9340d5b --- /dev/null +++ b/src/components/editor/nodes/TypePlaceholderView.svelte @@ -0,0 +1,15 @@ + + + +{#if format.editable && format.block}{/if} diff --git a/src/components/editor/nodes/TypeVariableView.svelte b/src/components/editor/nodes/TypeVariableView.svelte new file mode 100644 index 000000000..a809d8d41 --- /dev/null +++ b/src/components/editor/nodes/TypeVariableView.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/components/editor/nodes/TypeVariablesView.svelte b/src/components/editor/nodes/TypeVariablesView.svelte new file mode 100644 index 000000000..fc6569862 --- /dev/null +++ b/src/components/editor/nodes/TypeVariablesView.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/editor/TypeView.svelte b/src/components/editor/nodes/TypeView.svelte similarity index 76% rename from src/components/editor/TypeView.svelte rename to src/components/editor/nodes/TypeView.svelte index 20a9ac6db..b3c2e1491 100644 --- a/src/components/editor/TypeView.svelte +++ b/src/components/editor/nodes/TypeView.svelte @@ -1,9 +1,9 @@ + + diff --git a/src/components/editor/nodes/UnionTypeView.svelte b/src/components/editor/nodes/UnionTypeView.svelte new file mode 100644 index 000000000..da133226b --- /dev/null +++ b/src/components/editor/nodes/UnionTypeView.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/components/editor/nodes/UnitView.svelte b/src/components/editor/nodes/UnitView.svelte new file mode 100644 index 000000000..c137387e3 --- /dev/null +++ b/src/components/editor/nodes/UnitView.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/components/editor/UnknownNodeView.svelte b/src/components/editor/nodes/UnknownNodeView.svelte similarity index 74% rename from src/components/editor/UnknownNodeView.svelte rename to src/components/editor/nodes/UnknownNodeView.svelte index a8c83da28..efa635a1a 100644 --- a/src/components/editor/UnknownNodeView.svelte +++ b/src/components/editor/nodes/UnknownNodeView.svelte @@ -1,11 +1,11 @@ {node.getDescriptor()} diff --git a/src/components/editor/UnparsableExpressionView.svelte b/src/components/editor/nodes/UnparsableExpressionView.svelte similarity index 59% rename from src/components/editor/UnparsableExpressionView.svelte rename to src/components/editor/nodes/UnparsableExpressionView.svelte index 443123bfc..2b76433f8 100644 --- a/src/components/editor/UnparsableExpressionView.svelte +++ b/src/components/editor/nodes/UnparsableExpressionView.svelte @@ -1,12 +1,14 @@ - + diff --git a/src/components/editor/UnparsableTypeView.svelte b/src/components/editor/nodes/UnparsableTypeView.svelte similarity index 57% rename from src/components/editor/UnparsableTypeView.svelte rename to src/components/editor/nodes/UnparsableTypeView.svelte index 60db574a2..e15715596 100644 --- a/src/components/editor/UnparsableTypeView.svelte +++ b/src/components/editor/nodes/UnparsableTypeView.svelte @@ -1,12 +1,14 @@ - + diff --git a/src/components/editor/UnparsableView.svelte b/src/components/editor/nodes/UnparsableView.svelte similarity index 70% rename from src/components/editor/UnparsableView.svelte rename to src/components/editor/nodes/UnparsableView.svelte index 8ff81d797..871e566dc 100644 --- a/src/components/editor/UnparsableView.svelte +++ b/src/components/editor/nodes/UnparsableView.svelte @@ -2,16 +2,23 @@ import type UnparsableExpression from '@nodes/UnparsableExpression'; import type UnparsableType from '@nodes/UnparsableType'; import NodeSequenceView from './NodeSequenceView.svelte'; + import { type Format } from './NodeView.svelte'; interface Props { node: UnparsableExpression | UnparsableType; + format: Format; } - let { node }: Props = $props(); + let { node, format }: Props = $props(); {#if node.unparsables.length > 0} - + {:else} {/if} diff --git a/src/components/editor/nodes/nodeToView.ts b/src/components/editor/nodes/nodeToView.ts new file mode 100644 index 000000000..a1a06579c --- /dev/null +++ b/src/components/editor/nodes/nodeToView.ts @@ -0,0 +1,650 @@ +import type { Component } from 'svelte'; + +/* eslint-disable @typescript-eslint/ban-types */ +import TokenView from '../tokens/TokenView.svelte'; +import BinaryEvaluateView from './BinaryEvaluateView.svelte'; +import BindView from './BindView.svelte'; +import BlockView from './BlockView.svelte'; +import BooleanLiteralView from './BooleanLiteralView.svelte'; +import BooleanTypeView from './BooleanTypeView.svelte'; +import BorrowView from './BorrowView.svelte'; +import ChangedView from './ChangedView.svelte'; +import ConceptLinkView from './ConceptLinkView.svelte'; +import ConditionalView from './ConditionalView.svelte'; +import ConversionDefinitionView from './ConversionDefinitionView.svelte'; +import ConversionTypeView from './ConversionTypeView.svelte'; +import ConvertView from './ConvertView.svelte'; +import DeleteView from './DeleteView.svelte'; +import DimensionView from './DimensionView.svelte'; +import DocView from './DocView.svelte'; +import DocsView from './DocsView.svelte'; +import DocumentedExpressionView from './DocumentedExpressionView.svelte'; +import EvaluateView from './EvaluateView.svelte'; +import ExampleView from './ExampleView.svelte'; +import ExpressionPlaceholderView from './ExpressionPlaceholderView.svelte'; +import FormattedLiteralView from './FormattedLiteralView.svelte'; +import FormattedTranslationView from './FormattedTranslationView.svelte'; +import FormattedTypeView from './FormattedTypeView.svelte'; +import FunctionDefinitionView from './FunctionDefinitionView.svelte'; +import FunctionTypeView from './FunctionTypeView.svelte'; +import InitialView from './InitialView.svelte'; +import InputView from './InputView.svelte'; +import InsertView from './InsertView.svelte'; +import IsLocaleView from './IsLocaleView.svelte'; +import IsView from './IsView.svelte'; +import KeyValueView from './KeyValueView.svelte'; +import LanguageView from './LanguageView.svelte'; +import ListAccessView from './ListAccessView.svelte'; +import ListLiteralView from './ListLiteralView.svelte'; +import ListTypeView from './ListTypeView.svelte'; +import MapLiteralView from './MapLiteralView.svelte'; +import MapTypeView from './MapTypeView.svelte'; +import MarkupView from './MarkupView.svelte'; +import MatchView from './MatchView.svelte'; +import NameTypeView from './NameTypeView.svelte'; +import NameView from './NameView.svelte'; +import NamesView from './NamesView.svelte'; +import NoneLiteralView from './NoneLiteralView.svelte'; +import NoneTypeView from './NoneTypeView.svelte'; +import NumberLiteralView from './NumberLiteralView.svelte'; +import NumberTypeView from './NumberTypeView.svelte'; +import ParagraphView from './ParagraphView.svelte'; +import PreviousView from './PreviousView.svelte'; +import ProgramView from './ProgramView.svelte'; +import PropertyBindView from './PropertyBindView.svelte'; +import PropertyReferenceView from './PropertyReferenceView.svelte'; +import ReactionView from './ReactionView.svelte'; +import ReferenceView from './ReferenceView.svelte'; +import RowView from './RowView.svelte'; +import SelectView from './SelectView.svelte'; +import SetLiteralView from './SetLiteralView.svelte'; +import SetOrMapAccessView from './SetOrMapAccessView.svelte'; +import SetTypeView from './SetTypeView.svelte'; +import SourceView from './SourceView.svelte'; +import SpreadView from './SpreadView.svelte'; +import StreamTypeView from './StreamTypeView.svelte'; +import StructureDefinitionView from './StructureDefinitionView.svelte'; +import StructureTypeView from './StructureTypeView.svelte'; +import TableLiteralView from './TableLiteralView.svelte'; +import TableTypeView from './TableTypeView.svelte'; +import TextLiteralView from './TextLiteralView.svelte'; +import TextTypeView from './TextTypeView.svelte'; +import ThisView from './ThisView.svelte'; +import TranslationView from './TranslationView.svelte'; +import TypeInputsView from './TypeInputsView.svelte'; +import TypePlaceholderView from './TypePlaceholderView.svelte'; +import TypeVariableView from './TypeVariableView.svelte'; +import TypeVariablesView from './TypeVariablesView.svelte'; +import TypeView from './TypeView.svelte'; +import UnaryEvaluateView from './UnaryEvaluateView.svelte'; +import UnionTypeView from './UnionTypeView.svelte'; +import UnitView from './UnitView.svelte'; +import UnknownNodeView from './UnknownNodeView.svelte'; +import UnparsableExpressionView from './UnparsableExpressionView.svelte'; +import UnparsableTypeView from './UnparsableTypeView.svelte'; +import UpdateView from './UpdateView.svelte'; +import WebLinkView from './WebLinkView.svelte'; +import WordsView from './WordsView.svelte'; + +import BinaryEvaluate from '@nodes/BinaryEvaluate'; +import Bind from '@nodes/Bind'; +import Block from '@nodes/Block'; +import BooleanLiteral from '@nodes/BooleanLiteral'; +import BooleanType from '@nodes/BooleanType'; +import Borrow from '@nodes/Borrow'; +import Changed from '@nodes/Changed'; +import ConceptLink from '@nodes/ConceptLink'; +import Conditional from '@nodes/Conditional'; +import ConversionDefinition from '@nodes/ConversionDefinition'; +import ConversionType from '@nodes/ConversionType'; +import Convert from '@nodes/Convert'; +import Delete from '@nodes/Delete'; +import Dimension from '@nodes/Dimension'; +import Doc from '@nodes/Doc'; +import Docs from '@nodes/Docs'; +import DocumentedExpression from '@nodes/DocumentedExpression'; +import Evaluate from '@nodes/Evaluate'; +import Example from '@nodes/Example'; +import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; +import FormattedLiteral from '@nodes/FormattedLiteral'; +import FormattedTranslation from '@nodes/FormattedTranslation'; +import FormattedType from '@nodes/FormattedType'; +import FunctionDefinition from '@nodes/FunctionDefinition'; +import FunctionType from '@nodes/FunctionType'; +import Initial from '@nodes/Initial'; +import Input from '@nodes/Input'; +import Insert from '@nodes/Insert'; +import Is from '@nodes/Is'; +import IsLocale from '@nodes/IsLocale'; +import KeyValue from '@nodes/KeyValue'; +import Language from '@nodes/Language'; +import ListAccess from '@nodes/ListAccess'; +import ListLiteral from '@nodes/ListLiteral'; +import ListType from '@nodes/ListType'; +import MapLiteral from '@nodes/MapLiteral'; +import MapType from '@nodes/MapType'; +import Markup from '@nodes/Markup'; +import Match from '@nodes/Match'; +import Name from '@nodes/Name'; +import NameType from '@nodes/NameType'; +import Names from '@nodes/Names'; +import type Node from '@nodes/Node'; +import NoneLiteral from '@nodes/NoneLiteral'; +import NoneType from '@nodes/NoneType'; +import NumberLiteral from '@nodes/NumberLiteral'; +import NumberType from '@nodes/NumberType'; +import Otherwise from '@nodes/Otherwise'; +import Paragraph from '@nodes/Paragraph'; +import Previous from '@nodes/Previous'; +import Program from '@nodes/Program'; +import PropertyBind from '@nodes/PropertyBind'; +import PropertyReference from '@nodes/PropertyReference'; +import Reaction from '@nodes/Reaction'; +import Reference from '@nodes/Reference'; +import Row from '@nodes/Row'; +import Select from '@nodes/Select'; +import SetLiteral from '@nodes/SetLiteral'; +import SetOrMapAccess from '@nodes/SetOrMapAccess'; +import SetType from '@nodes/SetType'; +import Source from '@nodes/Source'; +import Spread from '@nodes/Spread'; +import StreamType from '@nodes/StreamType'; +import StructureDefinition from '@nodes/StructureDefinition'; +import StructureType from '@nodes/StructureType'; +import TableLiteral from '@nodes/TableLiteral'; +import TableType from '@nodes/TableType'; +import TextLiteral from '@nodes/TextLiteral'; +import TextType from '@nodes/TextType'; +import This from '@nodes/This'; +import Token from '@nodes/Token'; +import Translation from '@nodes/Translation'; +import Type from '@nodes/Type'; +import TypeInputs from '@nodes/TypeInputs'; +import TypePlaceholder from '@nodes/TypePlaceholder'; +import TypeVariable from '@nodes/TypeVariable'; +import TypeVariables from '@nodes/TypeVariables'; +import UnaryEvaluate from '@nodes/UnaryEvaluate'; +import UnionType from '@nodes/UnionType'; +import Unit from '@nodes/Unit'; +import UnparsableExpression from '@nodes/UnparsableExpression'; +import UnparsableType from '@nodes/UnparsableType'; +import Update from '@nodes/Update'; +import VariableType from '@nodes/VariableType'; +import WebLink from '@nodes/WebLink'; +import Words from '@nodes/Words'; +import type { Format } from './NodeView.svelte'; +import NoneOrView from './OtherwiseView.svelte'; +import VariableTypeView from './VariableTypeView.svelte'; + +type NodeViewComponent = Component<{ + node: any; + format: Format; +}>; + +// Block styling for each view +type BlockKind = + | 'plain' + | 'definition' + | 'reference' + | 'data' + | 'evaluate' + | 'type' + | 'predicate' + | 'none'; + +type BlockStyle = { + /** The visual appearance of the block. */ + kind: BlockKind; + /** Whether the layout is block or inline */ + direction: 'block' | 'inline'; + /** Whether the font size is small */ + size: 'normal' | 'small'; +}; + +type BlockConfig = { component: NodeViewComponent; style: BlockStyle }; + +const nodeToView = new Map(); + +function map( + nodeType: Function & { prototype: Kind }, + component: NodeViewComponent, + style: BlockStyle, +) { + nodeToView.set(nodeType, { component, style }); +} + +map(Token, TokenView, { kind: 'none', direction: 'inline', size: 'normal' }); +map(Source, SourceView, { kind: 'none', direction: 'block', size: 'normal' }); +map(Program, ProgramView, { + kind: 'none', + direction: 'block', + size: 'normal', +}); +map(Doc, DocView, { kind: 'plain', direction: 'inline', size: 'normal' }); +map(Docs, DocsView, { kind: 'none', direction: 'inline', size: 'normal' }); +map(Paragraph, ParagraphView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(WebLink, WebLinkView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(ConceptLink, ConceptLinkView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Words, WordsView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(DocumentedExpression, DocumentedExpressionView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(Example, ExampleView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Markup, MarkupView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(FormattedLiteral, FormattedLiteralView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(FormattedTranslation, FormattedTranslationView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); + +map(Borrow, BorrowView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); + +map(Block, BlockView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); + +map(Bind, BindView, { + kind: 'definition', + direction: 'block', + size: 'normal', +}); +map(Name, NameView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(Names, NamesView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(Language, LanguageView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Reference, ReferenceView, { + kind: 'reference', + direction: 'inline', + size: 'normal', +}); + +map(StructureDefinition, StructureDefinitionView, { + kind: 'definition', + direction: 'block', + size: 'normal', +}); +map(PropertyReference, PropertyReferenceView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(PropertyBind, PropertyBindView, { + kind: 'definition', + direction: 'inline', + size: 'normal', +}); +map(NameType, NameTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(TypeVariables, TypeVariablesView, { + kind: 'type', + direction: 'inline', + size: 'normal', +}); +map(TypeVariable, TypeVariableView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(TypeInputs, TypeInputsView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(VariableType, VariableTypeView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); + +map(TextLiteral, TextLiteralView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Translation, TranslationView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(TextType, TextTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(FunctionDefinition, FunctionDefinitionView, { + kind: 'definition', + direction: 'block', + size: 'normal', +}); +map(FunctionType, FunctionTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Evaluate, EvaluateView, { + kind: 'evaluate', + direction: 'inline', + size: 'normal', +}); +map(Input, InputView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(ExpressionPlaceholder, ExpressionPlaceholderView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(BinaryEvaluate, BinaryEvaluateView, { + kind: 'evaluate', + direction: 'inline', + size: 'normal', +}); +map(UnaryEvaluate, UnaryEvaluateView, { + kind: 'evaluate', + direction: 'inline', + size: 'normal', +}); + +map(Convert, ConvertView, { + kind: 'evaluate', + direction: 'inline', + size: 'normal', +}); +map(ConversionDefinition, ConversionDefinitionView, { + kind: 'definition', + direction: 'inline', + size: 'normal', +}); +map(ConversionType, ConversionTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Conditional, ConditionalView, { + kind: 'predicate', + direction: 'block', + size: 'normal', +}); +map(Otherwise, NoneOrView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Match, MatchView, { + kind: 'predicate', + direction: 'block', + size: 'normal', +}); + +map(NumberLiteral, NumberLiteralView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(NumberType, NumberTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Unit, UnitView, { kind: 'none', direction: 'inline', size: 'small' }); +map(Dimension, DimensionView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); + +map(BooleanLiteral, BooleanLiteralView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(BooleanType, BooleanTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(NoneLiteral, NoneLiteralView, { + kind: 'none', + direction: 'inline', + size: 'normal', +}); +map(NoneType, NoneTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(SetLiteral, SetLiteralView, { + kind: 'data', + direction: 'inline', + size: 'normal', +}); +map(MapLiteral, MapLiteralView, { + kind: 'data', + direction: 'inline', + size: 'normal', +}); +map(KeyValue, KeyValueView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(SetOrMapAccess, SetOrMapAccessView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(SetType, SetTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(MapType, MapTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(ListLiteral, ListLiteralView, { + kind: 'data', + direction: 'inline', + size: 'normal', +}); +map(Spread, SpreadView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(ListAccess, ListAccessView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(ListType, ListTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(FormattedType, FormattedTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(TableLiteral, TableLiteralView, { + kind: 'data', + direction: 'inline', + size: 'normal', +}); +map(TableType, TableTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Row, RowView, { + kind: 'data', + direction: 'inline', + size: 'normal', +}); +map(Insert, InsertView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Delete, DeleteView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Update, UpdateView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Select, SelectView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); + +map(Reaction, ReactionView, { + kind: 'evaluate', + direction: 'inline', + size: 'normal', +}); +map(Previous, PreviousView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Changed, ChangedView, { + kind: 'predicate', + direction: 'inline', + size: 'normal', +}); +map(Initial, InitialView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(StreamType, StreamTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(UnparsableType, UnparsableTypeView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(UnparsableExpression, UnparsableExpressionView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); + +map(UnionType, UnionTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(TypePlaceholder, TypePlaceholderView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); +map(Is, IsView, { + kind: 'predicate', + direction: 'inline', + size: 'normal', +}); +map(IsLocale, IsLocaleView, { + kind: 'predicate', + direction: 'inline', + size: 'normal', +}); +map(This, ThisView, { + kind: 'plain', + direction: 'inline', + size: 'normal', +}); +map(Type, TypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +map(StructureType, StructureTypeView, { + kind: 'type', + direction: 'inline', + size: 'small', +}); + +export default function getNodeView(node: Node): BlockConfig { + // Climb the class hierarchy until finding a satisfactory view of the node. + let constructor = node.constructor; + do { + const view = nodeToView.get(constructor); + if (view !== undefined) return view; + constructor = Object.getPrototypeOf(constructor); + } while (constructor); + return { + component: UnknownNodeView, + style: { kind: 'plain', direction: 'inline', size: 'normal' }, + }; +} diff --git a/src/components/editor/pointer/PointerUtilities.ts b/src/components/editor/pointer/PointerUtilities.ts new file mode 100644 index 000000000..7e3181d45 --- /dev/null +++ b/src/components/editor/pointer/PointerUtilities.ts @@ -0,0 +1,687 @@ +import Caret from '@edit/caret/Caret'; +import { + AssignmentPoint, + getInsertionPoint, + InsertionPoint, +} from '@edit/drag/Drag'; +import type { WritingDirection } from '@locale/Scripts'; +import Block from '@nodes/Block'; +import type Context from '@nodes/Context'; +import Expression from '@nodes/Expression'; +import type Node from '@nodes/Node'; +import { ListOf } from '@nodes/Node'; +import type Program from '@nodes/Program'; +import type Source from '@nodes/Source'; +import Token from '@nodes/Token'; + +/** + * Given a rendered source, and a pointer event, find the empty list insertion under the + * pointer and return the corresponding index into the buffer. + * */ +export function getEmptyList( + source: Source, + event: PointerEvent, +): number | undefined { + // If the token is over an empty list, insertion point for that list. + const el = document.elementFromPoint(event.clientX, event.clientY); + + const emptyView = el?.closest(`.empty`); + if ( + emptyView instanceof HTMLElement && + emptyView.dataset.field && + emptyView.dataset.nodeid + ) { + const node = source.getNodeByID(parseInt(emptyView.dataset.nodeid)); + const field = emptyView.dataset.field; + if (node === undefined) return undefined; + const adjacentBefore = node.getAdjacentFieldNode(field, true); + if (adjacentBefore !== undefined) + return source.getNodeLastPosition(adjacentBefore); + const adjacentAfter = node.getAdjacentFieldNode(field, false); + if (adjacentAfter !== undefined) + return source.getNodeFirstPosition(adjacentAfter); + return source.getNodeFirstPosition(node); + } + return undefined; +} + +/** Get the node under the pointer for the given rendered source */ +export function getNodeAt( + source: Source, + event: PointerEvent | MouseEvent, + includeTokens: boolean, +) { + const el = document.elementFromPoint(event.clientX, event.clientY); + // Only return a node if hovering over its text. Space isn't eligible. + if (el instanceof HTMLElement) { + const nodeView = el.closest( + `.node-view${includeTokens ? '' : `:not(.Token)`}`, + ); + if (nodeView instanceof HTMLElement && nodeView.dataset.id) { + return source.expression.getNodeByID(parseInt(nodeView.dataset.id)); + } + } + return undefined; +} + +/** Given a source context, a node being dragged, and a pointer event, identify the list insertion or field assignment under the pointer. */ +/** Given a pointer event, find the insertion points in blocks mode. */ +export function getBlockInsertionPoint( + context: Context, + event: PointerEvent, + candidate: Node, +): InsertionPoint | AssignmentPoint | undefined { + // Find the node under the pointer. If there isn't one, bail. + const nodeUnderPointer = getNodeAt(context.source, event, false); + if (nodeUnderPointer === undefined) return undefined; + + // Don't allow parents to be inserted into their children. + if (candidate.contains(nodeUnderPointer)) return undefined; + + // Does the node under the pointer have an empty or node-list inside it? + const el = document.elementFromPoint(event.clientX, event.clientY); + if (!(el instanceof HTMLElement)) return undefined; + + // Find the empty view closest to the element under the pointer. + const emptyView = el.closest(`.empty`); + if (emptyView instanceof HTMLElement && emptyView.dataset.field) { + const point = getEmptyInsertionPoint( + nodeUnderPointer, + emptyView.dataset.field, + candidate, + context, + ); + if (point) return point; + } + + const list = el.closest('.node-list'); + if (list instanceof HTMLElement) { + const point = getListInsertionPoint( + list, + nodeUnderPointer, + event, + candidate, + context, + ); + return point; + } +} + +function getEmptyInsertionPoint( + nodeUnderPointer: Node, + fieldName: string, + candidate: Node, + context: Context, +): InsertionPoint | AssignmentPoint | undefined { + const fieldValue = nodeUnderPointer.getField(fieldName); + const fieldInfo = nodeUnderPointer.getFieldNamed(fieldName); + const kind = fieldInfo?.kind; + + // If it's a list and it allows the node kind being inserted, return an insertion point. + if ( + fieldInfo !== undefined && + kind !== undefined && + Array.isArray(fieldValue) && + kind instanceof ListOf && + kind.allowsItem(candidate) && + // No type expected, or candidate isn't an expression, or candidate is accepted by the field type. + (fieldInfo.getType === undefined || + !(candidate instanceof Expression) || + fieldInfo + .getType(context, 0) + .accepts(candidate.getType(context), context)) + ) { + // Special case a root block being dragged onto a root block's statements, replacing it with a replacement of the root block. + // Makes it easier to drag onto an empty program. + if ( + nodeUnderPointer instanceof Block && + nodeUnderPointer.isRoot() && + fieldName === 'statements' && + candidate instanceof Block && + candidate.isRoot() + ) { + return new AssignmentPoint(context.source.expression, 'expression'); + } + + return new InsertionPoint( + nodeUnderPointer, + fieldName, + fieldValue, + undefined, + undefined, + 0, + ); + } + // If it's an unassigned field, offer an insertion point. + else if ( + fieldValue === undefined && + kind !== undefined && + kind.allows(candidate) + ) { + return new AssignmentPoint(nodeUnderPointer, fieldName); + } +} + +function getListInsertionPoint( + list: HTMLElement, + nodeUnderPointer: Node, + event: PointerEvent, + candidate: Node, + context: Context, +): InsertionPoint | undefined { + // Get the relevant metadata. + const fieldName = list.dataset.field; + if (fieldName === undefined) return undefined; + + const inline = list.dataset.direction === 'inline'; + const nodeList = nodeUnderPointer.getField(fieldName); + const field = nodeUnderPointer.getFieldNamed(fieldName); + const kind = field?.kind; + + if ( + field === undefined || + kind === undefined || + nodeList === undefined || + !Array.isArray(nodeList) || + !(kind instanceof ListOf) + ) + return; + + // Get all the node views from the list's child nodes. + const children = Array.from(list.childNodes).filter( + (node): node is HTMLElement => + node instanceof HTMLElement && node.classList.contains('node-view'), + ); + + // Find the closest child based on the layout of the children. For block, + // we assume a vertical list, finding the closest vertical child. + // For inline, we assume a column of rows, finding the closest row first, + // and then the closest child within that row. + let closestChild: HTMLElement | undefined; + if (inline) { + // First, organize the children into rows. + const rows: { child: HTMLElement; rect: DOMRect }[][] = []; + for (const child of children) { + const rect = child.getBoundingClientRect(); + // Is this child's top greater than the lowest bottom of the current row. + if ( + rows.length === 0 || + rect.top > + Math.max(...rows[rows.length - 1].map((c) => c.rect.bottom)) + ) { + rows.push([{ child, rect }]); + } else rows[rows.length - 1].push({ child, rect }); + } + // Find the closest vertical row. + const closestRow = rows + .map((row) => { + const top = Math.min(...row.map((c) => c.rect.top)); + const bottom = Math.max(...row.map((c) => c.rect.bottom)); + return { + row, + distance: Math.abs( + event.clientY - (top + (bottom - top) / 2), + ), + }; + }) + .sort((a, b) => a.distance - b.distance)[0]?.row; + if (closestRow !== undefined) { + // Within that row, find the closest child horizontally. + closestChild = closestRow + .map(({ child, rect }) => { + return { + child, + distance: Math.abs( + event.clientX - (rect.left + rect.width / 2), + ), + }; + }) + .sort((a, b) => a.distance - b.distance)[0]?.child; + } + } else { + closestChild = children + .map((child) => { + const rect = child.getBoundingClientRect(); + return { + child, + distance: Math.abs( + event.clientY - (rect.top + rect.height / 2), + ), + }; + }) + .sort((a, b) => a.distance - b.distance)[0]?.child; + } + + // Find the index of the closest child. + let index = 0; + if (closestChild === undefined) return; + + index = children.indexOf(closestChild); + if (index === -1) return; + + // If the pointer is past the center of the closest child, insert after it. + const rect = closestChild.getBoundingClientRect(); + if ( + inline + ? event.clientX > rect.left + rect.width / 2 + : event.clientY > rect.top + rect.height / 2 + ) + index += 1; + + if ( + kind.allowsItem(candidate) && + // No type expected, or candidate isn't an expression, or candidate is accepted by the field type. + (field.getType === undefined || + !(candidate instanceof Expression) || + field + .getType(context, index) + .accepts(candidate.getType(context), context)) + ) { + return new InsertionPoint( + nodeUnderPointer, + fieldName, + nodeList, + undefined, + undefined, + index, + ); + } +} + +/** Given a pointer event, find the insertion points in text mode. */ +export function getTextInsertionPointsAt( + caret: Caret, + editor: HTMLElement, + event: PointerEvent, + getNodeView: (node: Node) => HTMLElement | undefined, + getTokenViews: () => HTMLElement[], + direction: WritingDirection, + blocks: boolean, +): InsertionPoint[] { + const source = caret.source; + + // Is the caret position between tokens? + // If so, are any of the token's parents inside a list in which we could insert something? + const position = getCaretPositionAt( + caret, + editor, + event, + getNodeView, + getTokenViews, + direction, + blocks, + ); + + // If we found a position, find what's between. + if (position !== undefined) { + // Create a caret for the position and get the token it's at. + const caret = new Caret( + source, + position, + undefined, + undefined, + undefined, + ); + const token = caret.getToken(); + if (token === undefined) return []; + + // What is the space prior to this insertion point? + const index = source.getTokenSpacePosition(token); + if (index === undefined) return []; + + // Find what space is prior. + const spacePrior = source.spaces + .getSpace(token) + .substring(0, position - index); + + // How many lines does the space prior include? + const line = spacePrior.split('\n').length - 1; + + // What nodes are between this and are any of them insertion points? + const { before, after } = caret.getNodesBetween(); + + // If there are nodes between the point, construct insertion points + // that exist in lists. + return ( + [ + ...before.map((tree) => + getInsertionPoint(source, tree, false, token, line), + ), + ...after.map((tree) => + getInsertionPoint(source, tree, true, token, line), + ), + ] + // Filter out duplicates and undefineds + .filter( + ( + insertion1: InsertionPoint | undefined, + i1, + insertions, + ): insertion1 is InsertionPoint => + insertion1 !== undefined && + insertions.find( + (insertion2, i2) => + i1 > i2 && + insertion1 !== insertion2 && + insertion2 !== undefined && + insertion1.equals(insertion2), + ) === undefined, + ) + ); + } + return []; +} + +/** Determine an appropriate place for the text caret given a pointer event. */ +export function getCaretPositionAt( + caret: Caret, + editor: HTMLElement, + event: PointerEvent, + getNodeView: (node: Node) => HTMLElement | undefined, + getTokenViews: () => HTMLElement[], + direction: WritingDirection, + /** True if in blocks editing mode */ + blocks: boolean, +): number | undefined { + const source = caret.source; + + // What element is under the mouse? + const elementAtCursor = document.elementFromPoint( + event.clientX, + event.clientY, + ); + + // If there's no element (which should be impossible), return nothing. + if (elementAtCursor === null || !(elementAtCursor instanceof HTMLElement)) + return undefined; + + // Is the pointer over a token? + const tokenPosition = getTokenPosition(elementAtCursor, event, caret); + if (tokenPosition !== undefined) return tokenPosition; + + // Is the pointer over space before a token? + const spacePosition = getSpacePosition(event, elementAtCursor, source); + if (spacePosition !== undefined) return spacePosition; + + if (!blocks) { + const endOfLinePosition = getEndOfLinePosition( + event, + source, + getTokenViews, + editor, + caret, + direction, + ); + if (endOfLinePosition !== undefined) return endOfLinePosition; + } + + // Otherwise, choose the last position if nothing else matches. + return source.getCode().getLength(); +} + +function getTokenPosition( + elementAtCursor: HTMLElement, + event: PointerEvent, + caret: Caret, +): number | undefined { + // If we've selected a token view, figure out what position in the text to place the caret. + if (!elementAtCursor.classList.contains('token-view')) return undefined; + + // Find the token this corresponds to. + const [token, tokenView] = + getTokenFromElement(caret, elementAtCursor) ?? []; + + // If we found a token, find the position in it corresponding to the mouse position. + if (!(token instanceof Token)) return undefined; + if (!(tokenView instanceof HTMLElement)) return undefined; + if (!(event.target instanceof Element)) return undefined; + const startIndex = caret.source.getTokenTextPosition(token); + const lastIndex = caret.source.getTokenLastPosition(token); + if (startIndex === undefined || lastIndex === undefined) return undefined; + + // The mouse event's offset is relative to what was clicked on, not the element handling the click, so we have to compute the real offset. + const targetRect = event.target.getBoundingClientRect(); + const tokenRect = elementAtCursor.getBoundingClientRect(); + const offset = event.offsetX + (targetRect.left - tokenRect.left); + const newPosition = Math.max( + startIndex, + Math.min( + lastIndex, + startIndex + + (tokenRect.width === 0 + ? 0 + : Math.round( + token.getTextLength() * (offset / tokenRect.width), + )), + ), + ); + return newPosition; +} + +function getSpacePosition( + event: PointerEvent, + elementAtCursor: HTMLElement, + source: Source, +): number | undefined { + // Find the space text we're over + const spaceTextView = elementAtCursor.closest('.space-text'); + if (!(spaceTextView instanceof HTMLElement)) return undefined; + + // Find the overall space view it is in. + const spaceView = elementAtCursor.closest('.space'); + if (!(spaceView instanceof HTMLElement)) return undefined; + + // Find the token the space is after. + const tokenID = spaceView.dataset.id + ? parseInt(spaceView.dataset.id) + : null; + if (tokenID === null || isNaN(tokenID)) return undefined; + const token = source.getNodeByID(tokenID); + if (!(token instanceof Token)) return undefined; + + // Get the line number of the space text + const spaceLine = spaceTextView.dataset.line; + if (spaceLine === undefined) return undefined; + + // Get the starting position of the space + const spaceStartPosition = source.getTokenSpacePosition(token); + if (spaceStartPosition === undefined) return undefined; + + // Get the space text overall, on this line, and before this line. + const allSpace = source.spaces.getSpace(token); + const lineSpace = spaceTextView.textContent; + const lines = allSpace.split('\n'); + const spaceBefore = lines.slice(0, parseInt(spaceLine)).join('\n'); + + // Get the percent within the bounds of the space text that the pointer is. + const spaceTextViewBounds = spaceTextView.getBoundingClientRect(); + const proportion = + (event.clientX - spaceTextViewBounds.left) / spaceTextViewBounds.width; + + // Map the proportion to a text buffer position. + return ( + spaceStartPosition + + spaceBefore.length + + Math.round(proportion * lineSpace.length) + ); +} + +function getEndOfLinePosition( + event: PointerEvent, + source: Source, + getTokenViews: () => Iterable, + editor: HTMLElement, + caret: Caret, + direction: WritingDirection, +): number | undefined { + // Otherwise, the pointer is over the editor. We only place the caret + // in text mode, where there is a predictable grid layout. + // We first find the closest line, then find the end of that line. + const closestToken = Array.from(getTokenViews()) + .map((tokenView) => { + const textRect = tokenView.getBoundingClientRect(); + return { + view: tokenView, + textDistance: + textRect === undefined + ? Number.POSITIVE_INFINITY + : Math.abs( + event.clientY - + (textRect.top + textRect.height / 2), + ), + textLeft: + textRect === undefined + ? Number.POSITIVE_INFINITY + : textRect.left, + textRight: + textRect === undefined + ? Number.POSITIVE_INFINITY + : textRect.right, + textTop: + textRect === undefined + ? Number.POSITIVE_INFINITY + : textRect.top, + textBottom: + textRect === undefined + ? Number.POSITIVE_INFINITY + : textRect.bottom, + leftDistance: + textRect === undefined + ? Number.POSITIVE_INFINITY + : Math.abs(event.clientX - textRect.left), + rightDistance: + textRect === undefined + ? Number.POSITIVE_INFINITY + : Math.abs(event.clientX - textRect.right), + hidden: tokenView.closest('.hide') !== null, + }; + }) + // Filter by tokens within the vertical boundaries of the token. + .filter( + (text) => + !text.hidden && + text.textDistance !== Number.POSITIVE_INFINITY && + event.clientY >= text.textTop && + event.clientY <= text.textBottom, + ) + // Sort by increasing horizontal distance from the pointer + .sort( + (a, b) => + Math.min(a.leftDistance, a.rightDistance) - + Math.min(b.leftDistance, b.rightDistance), + )[0]; // Choose the closest. + + // If we found one, choose either the beginnng or end of the line. + if (closestToken) { + const [token] = getTokenFromElement(caret, closestToken.view) ?? []; + if (token === undefined) return undefined; + + return closestToken.textRight < event.clientX + ? source.getEndOfTokenLine(token) + : source.getStartOfTokenLine(token); + } + + // Otherwise, there is no token text on this line, so we have to find the closest line break. + type BreakInfo = { + token: Token; + offset: number; + index: number; + view: HTMLElement; + }; + + // Find all tokens with empty lines and choose the nearest. + const closestLine = + // Find all of the token line breaks, which are wrapped in spans to enable consistent measurement. + // This is because line breaks and getBoundingClientRect() are jumpy depending on what's around them. + Array.from(editor.querySelectorAll('.space .break')) + // Map each one to 1) the token, 2) token view, 3) line break top, 4) index within each token's space + .map((br) => { + const [token, tokenView] = + getTokenFromLineBreak(caret, br) ?? []; + // Check the br container, which gives us a more accurate bounding client rect. + const rect = br.getBoundingClientRect(); + if (tokenView === undefined || token === undefined) + return undefined; + // Skip the line if it doesn't include the pointer's y. + if (event.clientY < rect.top || event.clientY > rect.bottom) + return undefined; + return { + token, + offset: Math.abs( + rect.top + rect.height / 2 - event.clientY, + ), + // Find the index of the break in the space view. + index: Array.from( + tokenView.querySelectorAll('.break'), + ).indexOf(br), + view: br as HTMLElement, + }; + }) + // Filter out any empty breaks that we couldn't find + .filter( + (br: BreakInfo | undefined): br is BreakInfo => + br !== undefined, + ) + // Sort by increasing offset from mouse y + .sort((a, b) => a.offset - b.offset)[0]; // Chose the closest + + // If we have a closest line, find the line number + if (closestLine === undefined) return undefined; + + // Get the line number of the token after the space. + const lineAfter = source.getLine(closestLine.token); + if (lineAfter === undefined) return undefined; + + // Find the space view of the closest line. + // Compute the horizontal position at which to place the caret. + // Find the width of a single space by finding the longest line, + // which determines its width. + const spaceView = closestLine.view.closest('.space'); + if (spaceView == null) return undefined; + + const breaks = Array.from(spaceView.querySelectorAll('.break')); + + // The target line is the first line before the closest line's token, plus the number of breaks into it. + const targetLine = lineAfter - breaks.length + closestLine.index; + + // Find the last position on the target line. + return source.getEndOfLine(targetLine); +} + +function getTokenFromElement( + caret: Caret, + textOrSpace: Element, +): [Token, Element] | undefined { + const tokenView = textOrSpace.closest(`.Token`); + const token = + tokenView === null + ? undefined + : getTokenByView(caret.getProgram(), tokenView); + return tokenView === null || token === undefined + ? undefined + : [token, tokenView]; +} + +function getTokenFromLineBreak( + caret: Caret, + textOrSpace: Element, +): [Token, Element] | undefined { + const spaceView = textOrSpace.closest('.space') as HTMLElement; + const tokenID = + spaceView instanceof HTMLElement && spaceView.dataset.id + ? parseInt(spaceView.dataset.id) + : undefined; + const node = tokenID ? caret.source.getNodeByID(tokenID) : undefined; + return node instanceof Token ? [node, spaceView] : undefined; +} + +function getTokenByView(program: Program, tokenView: Element) { + if ( + tokenView instanceof HTMLElement && + tokenView.dataset.id !== undefined + ) { + const node = program.getNodeByID(parseInt(tokenView.dataset.id)); + return node instanceof Token ? node : undefined; + } + return undefined; +} diff --git a/src/components/editor/BooleanTokenEditor.svelte b/src/components/editor/tokens/BooleanTokenEditor.svelte similarity index 100% rename from src/components/editor/BooleanTokenEditor.svelte rename to src/components/editor/tokens/BooleanTokenEditor.svelte diff --git a/src/components/editor/TextOrPlaceholder.svelte b/src/components/editor/tokens/TextOrPlaceholder.svelte similarity index 61% rename from src/components/editor/TextOrPlaceholder.svelte rename to src/components/editor/tokens/TextOrPlaceholder.svelte index ce0edfbe9..8474bf456 100644 --- a/src/components/editor/TextOrPlaceholder.svelte +++ b/src/components/editor/tokens/TextOrPlaceholder.svelte @@ -1,25 +1,25 @@ {#if placeholder !== undefined}{:else if text.length === 0}​{:else}{rendered}{/if} + >{:else if text.length === 0}{#if !format.block}​{/if}{:else}{rendered}{/if} diff --git a/src/components/editor/TokenCategories.ts b/src/components/editor/tokens/TokenCategories.ts similarity index 100% rename from src/components/editor/TokenCategories.ts rename to src/components/editor/tokens/TokenCategories.ts diff --git a/src/components/editor/TokenEditor.svelte b/src/components/editor/tokens/TokenEditor.svelte similarity index 100% rename from src/components/editor/TokenEditor.svelte rename to src/components/editor/tokens/TokenEditor.svelte diff --git a/src/components/editor/TokenView.svelte b/src/components/editor/tokens/TokenView.svelte similarity index 65% rename from src/components/editor/TokenView.svelte rename to src/components/editor/tokens/TokenView.svelte index 72e578bf9..9d858db61 100644 --- a/src/components/editor/TokenView.svelte +++ b/src/components/editor/tokens/TokenView.svelte @@ -1,34 +1,29 @@ -{#if $blocks && root} +{#if format.block && root} + {@const parent = root.getParent(node)}
- {#if editable && $project && context && (node.isSymbol(Sym.Name) || node.isSymbol(Sym.Operator) || node.isSymbol(Sym.Words) || node.isSymbol(Sym.Number) || node.isSymbol(Sym.Boolean))} - {#if node.isSymbol(Sym.Words)} l.token.Words} - placeholder={placeholder ?? ((l) => l.token.Words)} - /> - {:else if node.isSymbol(Sym.Boolean)} - {:else if node.isSymbol(Sym.Number)} l.token.Number} - placeholder={placeholder ?? ((l) => l.token.Number)} - />{:else} - {@const parent = root.getParent(node)} - - {#if parent instanceof Name} - l.token.Name} - placeholder={placeholder ?? ((l) => l.token.Name)} - /> - {:else if parent instanceof Reference} - {@const grandparent = root.getParent(parent)} - - {#if grandparent && (grandparent instanceof BinaryEvaluate || grandparent instanceof UnaryEvaluate) && grandparent.fun === parent} - - {:else} - - {/if} - {:else}{/if} - {/if} - {:else}{:else}{/if} -
+
{#if format.editable && parent instanceof Reference}{/if} {:else} - + {/if} @@ -219,6 +183,13 @@ cursor: text; } + .token-view.editable.blocks { + cursor: grab; + display: flex; + flex-direction: row; + gap: var(--wordplay-spacing-half); + } + :global(.dragging) .token-view.editable { cursor: grabbing; } @@ -277,11 +248,11 @@ color: var(--color-blue); } - .token-category-docs:first-child { + :global(.Token):has(.token-category-docs):first-child { margin-inline-end: var(--wordplay-spacing); } - .token-category-docs:last-child { + :global(.Token):has(.token-category-docs):last-child { margin-inline-start: var(--wordplay-spacing); } @@ -293,4 +264,18 @@ .active { outline: 1px solid var(--wordplay-border-color); } + + .token-view.editable.blocks.blockText { + border-bottom: solid var(--wordplay-focus-width) + var(--wordplay-border-color); + } + + .StructureDefinition, + .StreamDefinition { + font-style: italic; + } + + .StreamDefinition { + text-decoration: underline dotted; + } diff --git a/src/components/editor/util/nodeToView.ts b/src/components/editor/util/nodeToView.ts deleted file mode 100644 index 3adf15658..000000000 --- a/src/components/editor/util/nodeToView.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { Component } from 'svelte'; - -/* eslint-disable @typescript-eslint/ban-types */ -import BinaryEvaluateView from '../BinaryEvaluateView.svelte'; -import BindView from '../BindView.svelte'; -import BlockView from '../BlockView.svelte'; -import BooleanLiteralView from '../BooleanLiteralView.svelte'; -import BooleanTypeView from '../BooleanTypeView.svelte'; -import BorrowView from '../BorrowView.svelte'; -import ChangedView from '../ChangedView.svelte'; -import ConceptLinkView from '../ConceptLinkView.svelte'; -import ConditionalView from '../ConditionalView.svelte'; -import ConversionDefinitionView from '../ConversionDefinitionView.svelte'; -import ConversionTypeView from '../ConversionTypeView.svelte'; -import ConvertView from '../ConvertView.svelte'; -import DeleteView from '../DeleteView.svelte'; -import DimensionView from '../DimensionView.svelte'; -import DocView from '../DocView.svelte'; -import DocsView from '../DocsView.svelte'; -import DocumentedExpressionView from '../DocumentedExpressionView.svelte'; -import EvaluateView from '../EvaluateView.svelte'; -import ExampleView from '../ExampleView.svelte'; -import ExpressionPlaceholderView from '../ExpressionPlaceholderView.svelte'; -import FormattedLiteralView from '../FormattedLiteralView.svelte'; -import FormattedTranslationView from '../FormattedTranslationView.svelte'; -import FormattedTypeView from '../FormattedTypeView.svelte'; -import FunctionDefinitionView from '../FunctionDefinitionView.svelte'; -import FunctionTypeView from '../FunctionTypeView.svelte'; -import InitialView from '../InitialView.svelte'; -import InputView from '../InputView.svelte'; -import InsertView from '../InsertView.svelte'; -import IsLocaleView from '../IsLocaleView.svelte'; -import IsView from '../IsView.svelte'; -import KeyValueView from '../KeyValueView.svelte'; -import LanguageView from '../LanguageView.svelte'; -import ListAccessView from '../ListAccessView.svelte'; -import ListLiteralView from '../ListLiteralView.svelte'; -import ListTypeView from '../ListTypeView.svelte'; -import MapLiteralView from '../MapLiteralView.svelte'; -import MapTypeView from '../MapTypeView.svelte'; -import MarkupView from '../MarkupView.svelte'; -import MatchView from '../MatchView.svelte'; -import NameTypeView from '../NameTypeView.svelte'; -import NameView from '../NameView.svelte'; -import NamesView from '../NamesView.svelte'; -import NoneLiteralView from '../NoneLiteralView.svelte'; -import NoneTypeView from '../NoneTypeView.svelte'; -import NumberLiteralView from '../NumberLiteralView.svelte'; -import NumberTypeView from '../NumberTypeView.svelte'; -import ParagraphView from '../ParagraphView.svelte'; -import PreviousView from '../PreviousView.svelte'; -import ProgramView from '../ProgramView.svelte'; -import PropertyBindView from '../PropertyBindView.svelte'; -import PropertyReferenceView from '../PropertyReferenceView.svelte'; -import ReactionView from '../ReactionView.svelte'; -import ReferenceView from '../ReferenceView.svelte'; -import RowView from '../RowView.svelte'; -import SelectView from '../SelectView.svelte'; -import SetLiteralView from '../SetLiteralView.svelte'; -import SetOrMapAccessView from '../SetOrMapAccessView.svelte'; -import SetTypeView from '../SetTypeView.svelte'; -import SourceView from '../SourceView.svelte'; -import SpreadView from '../SpreadView.svelte'; -import StreamTypeView from '../StreamTypeView.svelte'; -import StructureDefinitionView from '../StructureDefinitionView.svelte'; -import StructureTypeView from '../StructureTypeView.svelte'; -import TableLiteralView from '../TableLiteralView.svelte'; -import TableTypeView from '../TableTypeView.svelte'; -import TextLiteralView from '../TextLiteralView.svelte'; -import TextTypeView from '../TextTypeView.svelte'; -import ThisView from '../ThisView.svelte'; -import TokenView from '../TokenView.svelte'; -import TranslationView from '../TranslationView.svelte'; -import TypeInputsView from '../TypeInputsView.svelte'; -import TypePlaceholderView from '../TypePlaceholderView.svelte'; -import TypeVariableView from '../TypeVariableView.svelte'; -import TypeVariablesView from '../TypeVariablesView.svelte'; -import TypeView from '../TypeView.svelte'; -import UnaryEvaluateView from '../UnaryEvaluateView.svelte'; -import UnionTypeView from '../UnionTypeView.svelte'; -import UnitView from '../UnitView.svelte'; -import UnknownNodeView from '../UnknownNodeView.svelte'; -import UnparsableExpressionView from '../UnparsableExpressionView.svelte'; -import UnparsableTypeView from '../UnparsableTypeView.svelte'; -import UpdateView from '../UpdateView.svelte'; -import LinkView from '../WebLinkView.svelte'; -import WordsView from '../WordsView.svelte'; - -import BinaryEvaluate from '@nodes/BinaryEvaluate'; -import Bind from '@nodes/Bind'; -import Block from '@nodes/Block'; -import BooleanLiteral from '@nodes/BooleanLiteral'; -import BooleanType from '@nodes/BooleanType'; -import Borrow from '@nodes/Borrow'; -import Changed from '@nodes/Changed'; -import ConceptLink from '@nodes/ConceptLink'; -import Conditional from '@nodes/Conditional'; -import ConversionDefinition from '@nodes/ConversionDefinition'; -import ConversionType from '@nodes/ConversionType'; -import Convert from '@nodes/Convert'; -import Delete from '@nodes/Delete'; -import Dimension from '@nodes/Dimension'; -import Doc from '@nodes/Doc'; -import Docs from '@nodes/Docs'; -import DocumentedExpression from '@nodes/DocumentedExpression'; -import Evaluate from '@nodes/Evaluate'; -import Example from '@nodes/Example'; -import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; -import FormattedLiteral from '@nodes/FormattedLiteral'; -import FormattedTranslation from '@nodes/FormattedTranslation'; -import FormattedType from '@nodes/FormattedType'; -import FunctionDefinition from '@nodes/FunctionDefinition'; -import FunctionType from '@nodes/FunctionType'; -import Initial from '@nodes/Initial'; -import Input from '@nodes/Input'; -import Insert from '@nodes/Insert'; -import Is from '@nodes/Is'; -import IsLocale from '@nodes/IsLocale'; -import KeyValue from '@nodes/KeyValue'; -import Language from '@nodes/Language'; -import ListAccess from '@nodes/ListAccess'; -import ListLiteral from '@nodes/ListLiteral'; -import ListType from '@nodes/ListType'; -import MapLiteral from '@nodes/MapLiteral'; -import MapType from '@nodes/MapType'; -import Markup from '@nodes/Markup'; -import Match from '@nodes/Match'; -import Name from '@nodes/Name'; -import NameType from '@nodes/NameType'; -import Names from '@nodes/Names'; -import type Node from '@nodes/Node'; -import NoneLiteral from '@nodes/NoneLiteral'; -import NoneType from '@nodes/NoneType'; -import NumberLiteral from '@nodes/NumberLiteral'; -import NumberType from '@nodes/NumberType'; -import Otherwise from '@nodes/Otherwise'; -import Paragraph from '@nodes/Paragraph'; -import Previous from '@nodes/Previous'; -import Program from '@nodes/Program'; -import PropertyBind from '@nodes/PropertyBind'; -import PropertyReference from '@nodes/PropertyReference'; -import Reaction from '@nodes/Reaction'; -import Reference from '@nodes/Reference'; -import Row from '@nodes/Row'; -import Select from '@nodes/Select'; -import SetLiteral from '@nodes/SetLiteral'; -import SetOrMapAccess from '@nodes/SetOrMapAccess'; -import SetType from '@nodes/SetType'; -import Source from '@nodes/Source'; -import Spread from '@nodes/Spread'; -import StreamType from '@nodes/StreamType'; -import StructureDefinition from '@nodes/StructureDefinition'; -import StructureType from '@nodes/StructureType'; -import TableLiteral from '@nodes/TableLiteral'; -import TableType from '@nodes/TableType'; -import TextLiteral from '@nodes/TextLiteral'; -import TextType from '@nodes/TextType'; -import This from '@nodes/This'; -import Token from '@nodes/Token'; -import Translation from '@nodes/Translation'; -import Type from '@nodes/Type'; -import TypeInputs from '@nodes/TypeInputs'; -import TypePlaceholder from '@nodes/TypePlaceholder'; -import TypeVariable from '@nodes/TypeVariable'; -import TypeVariables from '@nodes/TypeVariables'; -import UnaryEvaluate from '@nodes/UnaryEvaluate'; -import UnionType from '@nodes/UnionType'; -import Unit from '@nodes/Unit'; -import UnparsableExpression from '@nodes/UnparsableExpression'; -import UnparsableType from '@nodes/UnparsableType'; -import Update from '@nodes/Update'; -import VariableType from '@nodes/VariableType'; -import WebLink from '@nodes/WebLink'; -import Words from '@nodes/Words'; -import NoneOrView from '../OtherwiseView.svelte'; -import VariableTypeView from '../VariableTypeView.svelte'; - -const nodeToView = new Map(); - -nodeToView.set(Token, TokenView); -nodeToView.set(Source, SourceView); -nodeToView.set(Program, ProgramView); -nodeToView.set(Doc, DocView); -nodeToView.set(Docs, DocsView); -nodeToView.set(Paragraph, ParagraphView); -nodeToView.set(WebLink, LinkView); -nodeToView.set(ConceptLink, ConceptLinkView); -nodeToView.set(Words, WordsView); -nodeToView.set(DocumentedExpression, DocumentedExpressionView); -nodeToView.set(Example, ExampleView); -nodeToView.set(Markup, MarkupView); -nodeToView.set(FormattedLiteral, FormattedLiteralView); -nodeToView.set(FormattedTranslation, FormattedTranslationView); - -nodeToView.set(Borrow, BorrowView); - -nodeToView.set(Block, BlockView); - -nodeToView.set(Bind, BindView); -nodeToView.set(Name, NameView); -nodeToView.set(Names, NamesView); -nodeToView.set(Language, LanguageView); -nodeToView.set(Reference, ReferenceView); - -nodeToView.set(StructureDefinition, StructureDefinitionView); -nodeToView.set(PropertyReference, PropertyReferenceView); -nodeToView.set(PropertyBind, PropertyBindView); -nodeToView.set(NameType, NameTypeView); - -nodeToView.set(TypeVariables, TypeVariablesView); -nodeToView.set(TypeVariable, TypeVariableView); -nodeToView.set(TypeInputs, TypeInputsView); -nodeToView.set(VariableType, VariableTypeView); - -nodeToView.set(TextLiteral, TextLiteralView); -nodeToView.set(Translation, TranslationView); -nodeToView.set(TextType, TextTypeView); - -nodeToView.set(FunctionDefinition, FunctionDefinitionView); -nodeToView.set(FunctionType, FunctionTypeView); -nodeToView.set(Evaluate, EvaluateView); -nodeToView.set(Input, InputView); - -nodeToView.set(ExpressionPlaceholder, ExpressionPlaceholderView); -nodeToView.set(BinaryEvaluate, BinaryEvaluateView); -nodeToView.set(UnaryEvaluate, UnaryEvaluateView); - -nodeToView.set(Convert, ConvertView); -nodeToView.set(ConversionDefinition, ConversionDefinitionView); -nodeToView.set(ConversionType, ConversionTypeView); - -nodeToView.set(Conditional, ConditionalView); -nodeToView.set(Otherwise, NoneOrView); -nodeToView.set(Match, MatchView); - -nodeToView.set(NumberLiteral, NumberLiteralView); -nodeToView.set(NumberType, NumberTypeView); -nodeToView.set(Unit, UnitView); -nodeToView.set(Dimension, DimensionView); - -nodeToView.set(BooleanLiteral, BooleanLiteralView); -nodeToView.set(BooleanType, BooleanTypeView); - -nodeToView.set(NoneLiteral, NoneLiteralView); -nodeToView.set(NoneType, NoneTypeView); - -nodeToView.set(SetLiteral, SetLiteralView); -nodeToView.set(MapLiteral, MapLiteralView); -nodeToView.set(KeyValue, KeyValueView); -nodeToView.set(SetOrMapAccess, SetOrMapAccessView); -nodeToView.set(SetType, SetTypeView); -nodeToView.set(MapType, MapTypeView); - -nodeToView.set(ListLiteral, ListLiteralView); -nodeToView.set(Spread, SpreadView); -nodeToView.set(ListAccess, ListAccessView); -nodeToView.set(ListType, ListTypeView); - -nodeToView.set(FormattedType, FormattedTypeView); - -nodeToView.set(TableLiteral, TableLiteralView); -nodeToView.set(TableType, TableTypeView); -nodeToView.set(Row, RowView); -nodeToView.set(Insert, InsertView); -nodeToView.set(Delete, DeleteView); -nodeToView.set(Update, UpdateView); -nodeToView.set(Select, SelectView); - -nodeToView.set(Reaction, ReactionView); -nodeToView.set(Previous, PreviousView); -nodeToView.set(Changed, ChangedView); -nodeToView.set(Initial, InitialView); -nodeToView.set(StreamType, StreamTypeView); -nodeToView.set(UnparsableType, UnparsableTypeView); -nodeToView.set(UnparsableExpression, UnparsableExpressionView); - -nodeToView.set(UnionType, UnionTypeView); -nodeToView.set(TypePlaceholder, TypePlaceholderView); -nodeToView.set(Is, IsView); -nodeToView.set(IsLocale, IsLocaleView); - -nodeToView.set(This, ThisView); - -nodeToView.set(Type, TypeView); - -nodeToView.set(StructureType, StructureTypeView); - -export default function getNodeView(node: Node): Component<{ node: Node }> { - // Climb the class hierarchy until finding a satisfactory view of the node. - let constructor = node.constructor; - do { - const view = nodeToView.get(constructor); - if (view !== undefined) return view as Component<{ node: Node }>; - constructor = Object.getPrototypeOf(constructor); - } while (constructor); - return UnknownNodeView as Component<{ node: Node }>; -} diff --git a/src/components/evaluator/Controls.svelte b/src/components/evaluator/Controls.svelte index 226b35a69..c152b2941 100644 --- a/src/components/evaluator/Controls.svelte +++ b/src/components/evaluator/Controls.svelte @@ -13,7 +13,7 @@ StepOut, StepToPresent, StepToStart, - } from '../editor/util/Commands'; + } from '../editor/commands/Commands'; import { getEvaluation } from '../project/Contexts'; import CommandButton from '../widgets/CommandButton.svelte'; import Switch from '../widgets/Switch.svelte'; diff --git a/src/components/lore/Speech.svelte b/src/components/lore/Speech.svelte index d861833f6..19aabb558 100644 --- a/src/components/lore/Speech.svelte +++ b/src/components/lore/Speech.svelte @@ -189,7 +189,7 @@ .message { border: var(--wordplay-border-width) solid var(--wordplay-border-color); - border-radius: calc(2 * var(--wordplay-border-radius)); + border-radius: var(--wordplay-border-radius); align-self: stretch; position: relative; background: var(--wordplay-background); @@ -790,6 +790,7 @@ .speaker { display: flex; flex-direction: row; - gap: var(--wordplay-spacing); + align-items: baseline; + gap: var(--wordplay-spacing-half); } diff --git a/src/components/output/ShapeView.svelte b/src/components/output/ShapeView.svelte index 314f3403c..891bac4b7 100644 --- a/src/components/output/ShapeView.svelte +++ b/src/components/output/ShapeView.svelte @@ -130,7 +130,7 @@ } .shape.rectangle { - border-radius: calc(2 * var(--wordplay-border-radius)); + border-radius: var(--wordplay-border-radius); } .shape.circle { diff --git a/src/components/palette/AuraEditor.svelte b/src/components/palette/AuraEditor.svelte index 99437fb19..4fbdb0afd 100644 --- a/src/components/palette/AuraEditor.svelte +++ b/src/components/palette/AuraEditor.svelte @@ -2,8 +2,8 @@ import ColorChooser from '@components/widgets/ColorChooser.svelte'; import Slider from '@components/widgets/Slider.svelte'; import type Project from '@db/projects/Project'; - import type OutputProperty from '@edit/OutputProperty'; - import type OutputPropertyValueSet from '@edit/OutputPropertyValueSet'; + import type OutputProperty from '@edit/output/OutputProperty'; + import type OutputPropertyValueSet from '@edit/output/OutputPropertyValueSet'; import { getFirstText } from '@locale/LocaleText'; import type Bind from '@nodes/Bind'; import Evaluate from '@nodes/Evaluate'; diff --git a/src/components/palette/BindCheckbox.svelte b/src/components/palette/BindCheckbox.svelte index 5236ba808..479c952d4 100644 --- a/src/components/palette/BindCheckbox.svelte +++ b/src/components/palette/BindCheckbox.svelte @@ -1,6 +1,6 @@ @@ -8,9 +8,11 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import CollaborateView from '@components/app/chat/CollaborateView.svelte'; - import Link from '@components/app/Link.svelte'; import Subheader from '@components/app/Subheader.svelte'; import Documentation from '@components/concepts/Documentation.svelte'; + import CharacterChooser from '@components/editor/commands/GlyphChooser.svelte'; + import Highlight from '@components/editor/highlights/Highlight.svelte'; + import Menu from '@components/editor/menu/Menu.svelte'; import Speech from '@components/lore/Speech.svelte'; import setKeyboardFocus from '@components/util/setKeyboardFocus'; import LocalizedText from '@components/widgets/LocalizedText.svelte'; @@ -28,7 +30,7 @@ AnimationFactors, } from '@db/settings/AnimationFactorSetting'; import type Locale from '@locale/Locale'; - import Node from '@nodes/Node'; + import Node, { isFieldPosition } from '@nodes/Node'; import Source from '@nodes/Source'; import { CANCEL_SYMBOL } from '@parser/Symbols'; import { isName } from '@parser/Tokenizer'; @@ -53,7 +55,6 @@ } from '../../db/Database'; import { isFlagged } from '../../db/projects/Moderation'; import Arrangement from '../../db/settings/Arrangement'; - import type Caret from '../../edit/Caret'; import Characters from '../../lore/BasisCharacters'; import type Color from '../../output/Color'; import { @@ -64,11 +65,6 @@ import Annotations from '../annotations/Annotations.svelte'; import CreatorView from '../app/CreatorView.svelte'; import Emoji from '../app/Emoji.svelte'; - import Editor from '../editor/Editor.svelte'; - import EditorToolbar from '../editor/EditorToolbar.svelte'; - import CharacterChooser from '../editor/GlyphChooser.svelte'; - import Highlight from '../editor/Highlight.svelte'; - import Menu from '../editor/Menu.svelte'; import { EnterFullscreen, ExitFullscreen, @@ -78,10 +74,13 @@ VisibleModifyCommands, VisibleNavigateCommands, type CommandContext, - } from '../editor/util/Commands'; - import type { HighlightSpec } from '../editor/util/Highlights'; - import type MenuInfo from '../editor/util/Menu'; - import getOutlineOf, { getUnderlineOf } from '../editor/util/outline'; + } from '../editor/commands/Commands'; + + import Toolbar from '@components/editor/commands/Toolbar.svelte'; + import Editor from '@components/editor/Editor.svelte'; + import type MenuInfo from '@edit/menu/Menu'; + import type { HighlightSpec } from '../editor/highlights/Highlights'; + import getOutlineOf, { getUnderlineOf } from '../editor/highlights/outline'; import Timeline from '../evaluator/Timeline.svelte'; import OutputView from '../output/OutputView.svelte'; import type PaintingConfiguration from '../output/PaintingConfiguration'; @@ -675,15 +674,6 @@ let howToStore = Locales.howTos; let howTos = $derived($howToStore[$locales.getLocaleString()]); - /** Update the concept index whenever the project, locales, or how tos change. */ - $effect(() => { - index = ConceptIndex.make( - project, - $locales, - howTos instanceof Promise ? [] : howTos, - ); - }); - /* Keep the index context up to date when it changes.*/ $effect(() => { indexContext.index = index; @@ -706,20 +696,24 @@ let latestProject: Project | undefined; - // When the project changes, languages change, and the keyboard is idle, recompute the concept index. + // When dependencies change, create a new concept index. $effect(() => { - if ($keyboardEditIdle && latestProject !== project) { + if ( + index === undefined || + ($keyboardEditIdle === IdleKind.Idle && latestProject !== project) + ) { latestProject = project; // Make a new concept index with the new project and translations, but the old examples. - const newIndex = - project && index - ? ConceptIndex.make( - project, - $locales, - howTos instanceof Promise ? [] : howTos, - ).withExamples(index.examples) - : undefined; + const newIndex = project + ? ConceptIndex.make( + project, + $locales, + howTos instanceof Promise ? [] : howTos, + ).withExamples( + index === undefined ? new Map() : index.examples, + ) + : undefined; // Set the index index = newIndex; @@ -793,31 +787,40 @@ ); /** - * Any time the evaluator of the project changes, start it, and analyze it after some delay. + * Any time the evaluator of the project changes, start it. * */ let updateTimer = $state(undefined); $effect(() => { // Re-evaluate immediately if not started. if (!$evaluator.isStarted()) $evaluator.start(); + }); - untrack(() => { - if (updateTimer) clearTimeout(updateTimer); - }); - - function updateConflicts() { - // In the middle of analyzing? Check later. - if (project.analyzed === 'analyzing') { - updateTimer = setTimeout(updateConflicts, KeyboardIdleWaitTime); - } - // Done analyzing, or not analyzed? - else if (project.analyzed === 'unanalyzed') { + function updateConflicts() { + // Analyzed? Update the conflicts immediately. + if (project.analyzed === 'analyzed') { + conflicts.set(project.getConflicts()); + } + // Not yet analyzed? Analyze in a bit. + else if (project.analyzed === 'unanalyzed') { + project.analyze(); + updateTimer = setTimeout(() => { project.analyze(); - // Get the resulting conflicts. - conflicts.set(project.getConflicts()); - } + updateConflicts(); + }, KeyboardIdleWaitTime); } + // Still analyzing? Try again later. + else { + if (updateTimer) clearTimeout(updateTimer); + updateTimer = setTimeout(updateConflicts, KeyboardIdleWaitTime); + } + } - updateTimer = setTimeout(updateConflicts, KeyboardIdleWaitTime); + /** Any time the project changes, update the conflicts soon */ + $effect(() => { + if (project) updateConflicts(); + return () => { + if (updateTimer) clearTimeout(updateTimer); + }; }); /** When stepping and the current step changes, change the active source. */ @@ -923,7 +926,7 @@ $effect(() => { if (menu) { // Find the tile corresponding to the menu's source file. - const index = sources.indexOf(menu.getCaret().source); + const index = sources.indexOf(menu.getSource()); const tile = layout?.tiles.find( (tile) => tile.id === Layout.getSourceID(index), ); @@ -951,7 +954,7 @@ /** When the menu changes, compute a menu position. */ $effect(() => { - menuPosition = menu ? getMenuPosition(menu.getCaret()) : undefined; + menuPosition = menu ? getMenuPosition(menu) : undefined; }); // When the locale direction changes, update the output. @@ -962,8 +965,18 @@ if (nodeView instanceof HTMLElement) outline = { types: ['dragging'], - outline: getOutlineOf(nodeView, true, direction === 'rtl'), - underline: getUnderlineOf(nodeView, true, direction === 'rtl'), + outline: getOutlineOf( + nodeView, + true, + direction === 'rtl', + $blocks, + ), + underline: getUnderlineOf( + nodeView, + true, + direction === 'rtl', + $blocks, + ), }; }); @@ -983,6 +996,7 @@ editor: false, /** We intentionally depend on the evaluation store because it updates when the evaluator's state changes */ evaluator: $evaluation.evaluator, + locales: $locales, dragging: dragged !== undefined, database: DB, setFullscreen: (on: boolean) => setBrowserFullscreen(on), @@ -1295,7 +1309,7 @@ } function repositionFloaters() { - menuPosition = menu ? getMenuPosition(menu.getCaret()) : undefined; + menuPosition = menu ? getMenuPosition(menu) : undefined; } function getSourceIndexByID(id: string) { @@ -1316,7 +1330,7 @@ const [, result] = handleKeyCommand(event, commandContext); // If something handled it, consume the event, and reset the modifier state. - if (result !== false) { + if (typeof result !== 'function' && result !== false) { event.stopPropagation(); event.preventDefault(); @@ -1341,10 +1355,13 @@ }); } - function getMenuPosition(caret: Caret) { + function getMenuPosition(menu: MenuInfo) { + const source = menu.getSource(); + const anchor = menu.getAnchor(); + // Find the editor const editor = document.querySelector( - `.editor[data-id="${caret.source.id}"]`, + `.editor[data-id="${source.id}"]`, ); if (editor === null) return undefined; @@ -1353,28 +1370,44 @@ const projectBounds = project.getBoundingClientRect(); - // Is it a node? Position near it's top left. - if (caret.position instanceof Node) { - const view = editor.querySelector( - `.node-view[data-id="${caret.position.id}"]`, + if (isFieldPosition(anchor)) { + // Is it a field position? Position near the field. + const trigger = editor.querySelector( + `.node-view[data-id="${anchor.parent.id}"] .trigger[data-field="${anchor.field}"]`, ); - if (view == null) return undefined; - const nodeBounds = view.getBoundingClientRect(); + if (trigger == null) return undefined; + const triggerBounds = trigger.getBoundingClientRect(); return { - left: nodeBounds.left - projectBounds.left, - top: nodeBounds.bottom - projectBounds.top, - }; - } - // Is it a position? Position at the bottom right of the caret. - else if (caret.isIndex()) { - // Find the position of the caret in the editor. - const caretView = editor.querySelector('.caret'); - if (caretView === null) return undefined; - const caretBounds = caretView.getBoundingClientRect(); - return { - left: caretBounds.left - projectBounds.left, - top: caretBounds.bottom - projectBounds.top, + left: triggerBounds.left - projectBounds.left, + top: + triggerBounds.bottom - + triggerBounds.height / 4 - + projectBounds.top, }; + } else { + // Is it a node? Position near it's top left. + if (anchor instanceof Node) { + const view = editor.querySelector( + `.node-view[data-id="${anchor.id}"]`, + ); + if (view == null) return undefined; + const nodeBounds = view.getBoundingClientRect(); + return { + left: nodeBounds.left - projectBounds.left, + top: nodeBounds.bottom - projectBounds.top, + }; + } + // Is it a position? Position at the bottom right of the caret. + else if (typeof anchor === 'number') { + // Find the position of the caret in the editor. + const caretView = editor.querySelector('.caret'); + if (caretView === null) return undefined; + const caretBounds = caretView.getBoundingClientRect(); + return { + left: caretBounds.left - projectBounds.left, + top: caretBounds.bottom - projectBounds.top, + }; + } } } @@ -1449,7 +1482,11 @@ onpointermove={handlePointerMove} onpointerup={handlePointerUp} onfocus={resetKeyModifiers} - onblur={resetKeyModifiers} + onblur={(event) => { + resetKeyModifiers(); + handlePointerUp(); + event.preventDefault(); + }} /> {#if warn} @@ -1532,7 +1569,7 @@ (l) => l.ui.dialog.settings.mode .animate, - ).modes[$animationFactor]} + ).labels[$animationFactor]} > @@ -1602,7 +1639,7 @@ > + modes={(l) => l.ui.dialog.settings.mode.animate} choice={AnimationFactors.indexOf( $animationFactor, @@ -1611,18 +1648,19 @@ Settings.setAnimationFactor( AnimationFactors[choice], )} - modes={AnimationFactorIcons} + icons={AnimationFactorIcons} + modeLabels={false} labeled={false} /> {#if $animationFactor === 0}{$locales.get( (l) => l.ui.dialog.settings.mode .animate, - ).modes[0]}{/if} + ).labels[0]}{/if} {:else if tile.isSource()} {#if !editable}{/if} - + {:else if tile.kind === TileKind.Palette} {/if} - {#if $blocks} - - {/if} {:else if tile.kind === TileKind.Output && layout.fullscreenID !== tile.id && !requestedPlay && !showOutput} {/if} {/snippet} @@ -1950,7 +1983,6 @@ locale={$locales.getLocale()} blocks={$blocks} /> -
🐲
{/if} {/if} @@ -2030,17 +2062,7 @@ padding: var(--wordplay-spacing); border-radius: var(--wordplay-border-radius); border: var(--wordplay-border-width) solid var(--wordplay-border-color); - opacity: 0.8; - } - - /* A fancy dragon cursor for dragon drop! Get it? */ - .cursor { - position: absolute; - font-size: 2rem; - top: -1.5rem; - left: -1.5rem; - font-family: 'Noto Emoji'; - z-index: 2; + opacity: 0.9; } .empty { diff --git a/src/components/project/Public.svelte b/src/components/project/Public.svelte index 082b881c2..f28a235e3 100644 --- a/src/components/project/Public.svelte +++ b/src/components/project/Public.svelte @@ -62,11 +62,8 @@ {/if} l.ui.dialog.share.mode.public} + modes={(l) => l.ui.dialog.share.mode.public} choice={isPublic ? 1 : 0} select={set} - modes={[ - '🤫 ' + $locales.get((l) => l.ui.dialog.share.mode.public.modes[0]), - `${GLOBE1_SYMBOL} ${$locales.get((l) => l.ui.dialog.share.mode.public.modes[1])}`, - ]} + icons={['🤫', GLOBE1_SYMBOL]} /> diff --git a/src/components/project/RootView.svelte b/src/components/project/RootView.svelte index a2bf634ce..0095b34cf 100644 --- a/src/components/project/RootView.svelte +++ b/src/components/project/RootView.svelte @@ -1,18 +1,18 @@ diff --git a/src/components/project/SourceTileToggle.svelte b/src/components/project/SourceTileToggle.svelte index 500e7d561..1c88a4e65 100644 --- a/src/components/project/SourceTileToggle.svelte +++ b/src/components/project/SourceTileToggle.svelte @@ -3,7 +3,6 @@ import LocalizedText from '@components/widgets/LocalizedText.svelte'; import Templates from '@concepts/Templates'; import type Project from '@db/projects/Project'; - import Context from '@nodes/Context'; import type Source from '@nodes/Source'; import { locales } from '../../db/Database'; import Characters from '../../lore/BasisCharacters'; @@ -32,7 +31,7 @@ if ($conflicts) { for (const conflict of $conflicts) { const nodes = conflict.getConflictingNodes( - new Context(project, source), + project.getContext(source), Templates, ); if (source.has(nodes.primary.node)) { @@ -72,8 +71,8 @@ font-size: small; border-radius: 50%; color: var(--wordplay-background); - min-width: 2em; - min-height: 2em; + min-width: 1.5em; + min-height: 1.5em; display: inline-flex; flex-direction: column; justify-content: center; diff --git a/src/components/project/TileView.svelte b/src/components/project/TileView.svelte index e55293a6d..bc16678a6 100644 --- a/src/components/project/TileView.svelte +++ b/src/components/project/TileView.svelte @@ -316,23 +316,6 @@ {#if !tile.isInvisible()}
- {#if !layout.isFullscreen()} - - {/if} - l.ui.tile.toggle.fullscreen} - on={fullscreen} - background={background !== null} - toggle={() => setFullscreen(!fullscreen)} - > - -
@@ -372,6 +355,25 @@
{@render extra?.()}
+
+ {#if !layout.isFullscreen()} + + {/if} + l.ui.tile.toggle.fullscreen} + on={fullscreen} + background={background !== null} + toggle={() => setFullscreen(!fullscreen)} + > + + +
@@ -544,13 +546,16 @@ flex-wrap: nowrap; align-items: center; padding: var(--wordplay-spacing); - gap: calc(var(--wordplay-spacing) / 2); + gap: var(--wordplay-spacing-half); width: 100%; overflow-x: auto; overflow-y: visible; flex-shrink: 0; /** Dim the header a bit so that they don't demand so much attention */ opacity: 0.8; + + border-block-end: solid var(--wordplay-border-color) + var(--wordplay-border-width); } .focus-indicator { @@ -585,4 +590,13 @@ .name.source { color: var(--wordplay-foreground); } + + .tile-controls { + margin-inline-start: auto; + display: flex; + flex-direction: row; + gap: var(--wordplay-spacing-half); + align-items: center; + flex-wrap: nowrap; + } diff --git a/src/components/settings/Settings.svelte b/src/components/settings/Settings.svelte index 62b4dda48..097eef6a3 100644 --- a/src/components/settings/Settings.svelte +++ b/src/components/settings/Settings.svelte @@ -8,12 +8,18 @@ AnimationFactors, } from '@db/settings/AnimationFactorSetting'; import { FaceSetting } from '@db/settings/FaceSetting'; - import { CANCEL_SYMBOL, CONFIRM_SYMBOL } from '@parser/Symbols'; + import { + BLOCK_EDITING_SYMBOL, + CANCEL_SYMBOL, + CONFIRM_SYMBOL, + TEXT_EDITING_SYMBOL, + } from '@parser/Symbols'; import { onMount } from 'svelte'; import { Creator } from '../../db/creators/CreatorDatabase'; import { animationFactor, arrangement, + blocks, camera, dark, locales, @@ -115,7 +121,7 @@ > l.ui.dialog.settings.mode.layout} + modes={(l) => l.ui.dialog.settings.mode.layout} choice={$arrangement === Arrangement.Responsive ? 0 : $arrangement === Arrangement.Horizontal @@ -141,14 +147,15 @@ ? Arrangement.Single : Arrangement.Free, )} - modes={Object.values(LayoutIcons)} + icons={Object.values(LayoutIcons)} /> l.ui.dialog.settings.mode.animate} + modes={(l) => l.ui.dialog.settings.mode.animate} choice={AnimationFactors.indexOf($animationFactor)} select={(choice) => Settings.setAnimationFactor(AnimationFactors[choice])} - modes={AnimationFactorIcons} + icons={AnimationFactorIcons} + modeLabels={false} /> {#if devicesRetrieved} {/if} l.ui.dialog.settings.mode.dark} + modes={(l) => l.ui.dialog.settings.mode.dark} choice={$dark === false ? 0 : $dark === true ? 1 : 2} select={(choice) => Settings.setDark( choice === 0 ? false : choice === 1 ? true : null, )} - modes={['☼', '☽', '☼/☽']} + icons={['☼', '☽', '☼/☽']} + /> + l.ui.dialog.settings.mode.blocks} + choice={$blocks ? 1 : 0} + select={(choice) => + Settings.setBlocks(choice === 1 ? true : false)} + icons={[TEXT_EDITING_SYMBOL, BLOCK_EDITING_SYMBOL]} /> - l.ui.dialog.settings.mode.space} + modes={(l) => l.ui.dialog.settings.mode.space} choice={$spaceIndicator ? 1 : 0} select={(choice) => Settings.setSpace(choice === 1 ? true : false)} - modes={[CANCEL_SYMBOL, CONFIRM_SYMBOL]} + icons={[CANCEL_SYMBOL, CONFIRM_SYMBOL]} /> l.ui.dialog.settings.mode.lines} + modes={(l) => l.ui.dialog.settings.mode.lines} choice={$showLines ? 1 : 0} select={(choice) => Settings.setLines(choice === 1 ? true : false)} - modes={[CANCEL_SYMBOL, CONFIRM_SYMBOL]} + icons={[CANCEL_SYMBOL, CONFIRM_SYMBOL]} />
diff --git a/src/components/values/SymbolView.svelte b/src/components/values/SymbolView.svelte index 7e40d056f..7ceb6064e 100644 --- a/src/components/values/SymbolView.svelte +++ b/src/components/values/SymbolView.svelte @@ -1,6 +1,6 @@ -{$locales.get(path)} +{withoutAnnotations($locales.get(path))} diff --git a/src/components/widgets/Mode.svelte b/src/components/widgets/Mode.svelte index 8fe8c8070..49e329138 100644 --- a/src/components/widgets/Mode.svelte +++ b/src/components/widgets/Mode.svelte @@ -2,28 +2,44 @@ import { getTip } from '@components/project/Contexts'; import { locales } from '@db/Database'; import type LocaleText from '@locale/LocaleText'; - import type { ModeText } from '../../locale/UITexts'; + import type { ModeText } from '@locale/UITexts'; import { withMonoEmoji } from '../../unicode/emoji'; + import LocalizedText from './LocalizedText.svelte'; interface Props { - descriptions: (locale: LocaleText) => ModeText; - modes: string[]; + /** Localized text for the labels and tooltips */ + modes: (locale: LocaleText) => ModeText; + /** The current mode selected */ choice: number; + /** Callback for when a mode is selected.*/ select: (choice: number) => void; + /** Icons to add as prefixes to labels */ + icons?: readonly string[]; + /** Whether the mode chooser is active */ active?: boolean; + /** Whether to add a label before the mode chooser*/ labeled?: boolean; + /** Whether to add labels to the individual mode buttons */ + modeLabels?: boolean; + /** Whether to wrap the row of buttons. Good if there are many. */ + wrap?: boolean; + /** Buttons to omit, allowing for conditional display of modes */ + omit?: readonly number[]; } let { - descriptions, modes, + icons, choice, select, active = true, labeled = true, + modeLabels = true, + wrap = false, + omit = [], }: Props = $props(); - let descriptionText = $derived($locales.get(descriptions)); + let modeText = $derived($locales.get(modes)); let hint = getTip(); function showTip(view: HTMLButtonElement, tip: string) { @@ -36,61 +52,65 @@
{#if labeled} - + {/if}
- {#each modes as mode, index} - - + {#each modeText.labels, index} + {#if !omit.includes(index)} + + + {/if} {/each}
@@ -102,7 +122,7 @@ flex-wrap: nowrap; gap: var(--wordplay-spacing); white-space: nowrap; - align-items: center; + align-items: baseline; } .label { @@ -127,7 +147,7 @@ button.selected { color: var(--wordplay-background); background: var(--wordplay-highlight-color); - transform: scale(1.1); + transform: scale(1.05); cursor: default; } @@ -149,6 +169,7 @@ button:not(:global(.selected)):hover { transform: scale(1.05); + background: var(--wordplay-hover); } .group { @@ -162,6 +183,12 @@ user-select: none; } + .group.wrap { + flex-wrap: wrap; + white-space: normal; + row-gap: var(--wordplay-focus-width); + } + [aria-disabled='true'] { cursor: default; background: none; diff --git a/src/components/widgets/Switch.svelte b/src/components/widgets/Switch.svelte index 3d2681e9e..60dcab390 100644 --- a/src/components/widgets/Switch.svelte +++ b/src/components/widgets/Switch.svelte @@ -104,7 +104,7 @@ transform-origin: center; cursor: pointer; border-radius: var(--wordplay-border-radius); - padding: calc(var(--wordplay-spacing) / 2); + padding: var(--wordplay-spacing-half); border: 1px solid var(--wordplay-chrome); background: var(--wordplay-background); } @@ -140,6 +140,7 @@ transform: scale(1.05); transform-origin: center; z-index: 1; + background-color: var(--wordplay-hover); } .button.active { diff --git a/src/components/widgets/Toggle.svelte b/src/components/widgets/Toggle.svelte index 907e75e02..f368a845f 100644 --- a/src/components/widgets/Toggle.svelte +++ b/src/components/widgets/Toggle.svelte @@ -4,7 +4,7 @@ import type LocaleText from '@locale/LocaleText'; import { type Snippet } from 'svelte'; import type { ToggleText } from '../../locale/UITexts'; - import { toShortcut, type Command } from '../editor/util/Commands'; + import { toShortcut, type Command } from '../editor/commands/Commands'; interface Props { tips: (locale: LocaleText) => ToggleText; @@ -104,7 +104,7 @@ color: currentColor; stroke: currentColor; fill: var(--wordplay-background); - padding: calc(var(--wordplay-spacing) / 2); + padding: var(--wordplay-spacing-half); cursor: pointer; width: fit-content; max-width: 10em; @@ -144,10 +144,21 @@ background: var(--wordplay-alternating-color); } + .icon { + display: flex; + flex-direction: row; + align-items: baseline; + gap: var(--wordplay-spacing-half); + } + button.on .icon { transform: scale(0.9); } + button:hover { + background: var(--wordplay-hover); + } + button:not(:global(.on)):hover .icon { transform: scale(1.1); } diff --git a/src/components/widgets/Warning.svelte b/src/components/widgets/Warning.svelte index 29acc5258..e0ed8c976 100644 --- a/src/components/widgets/Warning.svelte +++ b/src/components/widgets/Warning.svelte @@ -14,7 +14,7 @@ background: var(--wordplay-highlight-color); color: var(--wordplay-background); border-radius: var(--wordplay-border-radius); - padding: calc(var(--wordplay-spacing) / 2); + padding: var(--wordplay-spacing-half); font-size: medium; } diff --git a/src/concepts/BindConcept.ts b/src/concepts/BindConcept.ts index 91421eb4d..10dde8d6d 100644 --- a/src/concepts/BindConcept.ts +++ b/src/concepts/BindConcept.ts @@ -51,9 +51,7 @@ export default class BindConcept extends Concept { } getDocs(locales: Locales): Markup[] { - return (this.bind.docs?.docs ?? []) - .map((doc) => doc.markup.concretize(locales, [])) - .filter((m) => m !== undefined); + return this.bind.docs.getMarkup(locales); } getNames(): string[] { diff --git a/src/concepts/Concept.ts b/src/concepts/Concept.ts index f6f4277ac..97505bb64 100644 --- a/src/concepts/Concept.ts +++ b/src/concepts/Concept.ts @@ -55,7 +55,7 @@ export default abstract class Concept { /** * Return a node to represent the concept. Usually an example or template. */ - abstract getRepresentation(): Node; + abstract getRepresentation(locales: Locales): Node; /** * Returns a localized creator-facing name or description to represent the concept. diff --git a/src/concepts/ConceptIndex.ts b/src/concepts/ConceptIndex.ts index b5778bd45..d7e36b7d4 100644 --- a/src/concepts/ConceptIndex.ts +++ b/src/concepts/ConceptIndex.ts @@ -131,18 +131,20 @@ export default class ConceptIndex { return new StreamConcept(stream, locales, context); } - const streams = Object.values(project.shares.input).map((def) => - def instanceof StreamDefinition - ? makeStreamConcept(def) - : new FunctionConcept( - Purpose.Input, - undefined, - def, - undefined, - locales, - context, - ), - ); + const streams = Object.values(project.shares.input) + .filter((c) => c !== project.shares.input.Reaction) + .map((def) => + def instanceof StreamDefinition + ? makeStreamConcept(def) + : new FunctionConcept( + Purpose.Inputs, + undefined, + def, + undefined, + locales, + context, + ), + ); const constructs = getNodeConcepts(context); @@ -231,6 +233,14 @@ export default class ConceptIndex { ); } + getInterfaceImplementers(def: StructureDefinition): StructureConcept[] { + return this.concepts.filter( + (concept): concept is StructureConcept => + concept instanceof StructureConcept && + concept.inter.some((s) => s.definition === def), + ); + } + getStreamConcept(fun: StreamDefinition): StreamConcept | undefined { return this.concepts.find( (concept): concept is StreamConcept => @@ -255,6 +265,7 @@ export default class ConceptIndex { return this.primaryConcepts.filter((c) => c.purpose === purpose); } + /** Finds a concept that represents the given type */ getConceptOfType( type: Type, ): FunctionConcept | StructureConcept | undefined { diff --git a/src/concepts/ConversionConcept.ts b/src/concepts/ConversionConcept.ts index 12fa32c88..502343ae3 100644 --- a/src/concepts/ConversionConcept.ts +++ b/src/concepts/ConversionConcept.ts @@ -27,7 +27,7 @@ export default class ConversionConcept extends Concept { context: Context, structure?: StructureConcept, ) { - super(Purpose.Convert, structure?.definition, context); + super(Purpose.Types, structure?.definition, context); this.definition = definition; this.structure = structure; @@ -51,9 +51,7 @@ export default class ConversionConcept extends Concept { } getDocs(locales: Locales): Markup[] { - return (this.definition.docs?.docs ?? []) - .map((doc) => doc.markup.concretize(locales, [])) - .filter((m) => m !== undefined); + return this.definition.docs.getMarkup(locales); } getNames(locales: Locales) { diff --git a/src/concepts/DefaultConcepts.ts b/src/concepts/DefaultConcepts.ts index 0128c871f..60d782fa5 100644 --- a/src/concepts/DefaultConcepts.ts +++ b/src/concepts/DefaultConcepts.ts @@ -13,6 +13,7 @@ import NumberType from '@nodes/NumberType'; import SetLiteral from '@nodes/SetLiteral'; import SetType from '@nodes/SetType'; import StructureDefinition from '@nodes/StructureDefinition'; +import StructureType from '@nodes/StructureType'; import TextLiteral from '@nodes/TextLiteral'; import TextType from '@nodes/TextType'; import TypePlaceholder from '@nodes/TypePlaceholder'; @@ -28,12 +29,12 @@ import StructureConcept from './StructureConcept'; import Templates from './Templates'; export function getNodeConcepts(context: Context): NodeConcept[] { - return Templates.map((node) => { - const typeName = node.getAffiliatedType(); + return Templates.map((template) => { + const typeName = template.getAffiliatedType(); const type = typeName ? context.getBasis().getStructureDefinition(typeName) : undefined; - return new NodeConcept(node.getPurpose(), type, node, context); + return new NodeConcept(template.getPurpose(), type, template, context); }); } @@ -44,7 +45,7 @@ export function getBasisConcepts( ): StructureConcept[] { return [ new StructureConcept( - Purpose.Value, + Purpose.Text, basis.getSimpleDefinition('text'), basis.getSimpleDefinition('text'), TextType.make(), @@ -53,7 +54,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Numbers, basis.getSimpleDefinition('measurement'), basis.getSimpleDefinition('measurement'), NumberType.make(), @@ -66,7 +67,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Truth, basis.getSimpleDefinition('boolean'), basis.getSimpleDefinition('boolean'), BooleanType.make(), @@ -75,7 +76,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Lists, basis.getSimpleDefinition('list'), basis.getSimpleDefinition('list'), ListType.make(), @@ -84,7 +85,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Maps, basis.getSimpleDefinition('set'), basis.getSimpleDefinition('set'), SetType.make(), @@ -93,7 +94,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Maps, basis.getSimpleDefinition('map'), basis.getSimpleDefinition('map'), MapType.make(TypePlaceholder.make(), TypePlaceholder.make()), @@ -102,7 +103,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Truth, basis.getSimpleDefinition('none'), basis.getSimpleDefinition('none'), NoneType.make(), @@ -111,7 +112,7 @@ export function getBasisConcepts( context, ), new StructureConcept( - Purpose.Value, + Purpose.Tables, basis.getSimpleDefinition('table'), basis.getSimpleDefinition('table'), TableType.make(), @@ -119,6 +120,15 @@ export function getBasisConcepts( locales, context, ), + new StructureConcept( + Purpose.Types, + basis.getSimpleDefinition('structure'), + basis.getSimpleDefinition('structure'), + new StructureType(basis.getSimpleDefinition('structure')), + undefined, + locales, + context, + ), ]; } @@ -157,7 +167,9 @@ export function getOutputConcepts( ...Object.values(context.project.shares.output).map((def) => getStructureOrFunctionConcept( def, - Purpose.Output, + def === context.project.shares.output.Output + ? Purpose.Hidden + : Purpose.Outputs, undefined, locales, context, @@ -166,7 +178,7 @@ export function getOutputConcepts( ...Object.values(context.project.shares.sequences).map((def) => getStructureOrFunctionConcept( def, - Purpose.Output, + Purpose.Outputs, undefined, locales, context, diff --git a/src/concepts/FunctionConcept.ts b/src/concepts/FunctionConcept.ts index d26330595..7df284a73 100644 --- a/src/concepts/FunctionConcept.ts +++ b/src/concepts/FunctionConcept.ts @@ -41,6 +41,7 @@ export default class FunctionConcept extends Concept { this.example = this.definition.getEvaluateTemplate( locales, context, + false, this.structure?.type, ); @@ -68,9 +69,7 @@ export default class FunctionConcept extends Concept { } getDocs(locales: Locales): Markup[] { - return (this.definition.docs?.docs ?? []) - .map((doc) => doc.markup.concretize(locales, [])) - .filter((m) => m !== undefined); + return this.definition.docs.getMarkup(locales); } getNames() { diff --git a/src/concepts/NodeConcept.ts b/src/concepts/NodeConcept.ts index 880b2149b..7fe6c24be 100644 --- a/src/concepts/NodeConcept.ts +++ b/src/concepts/NodeConcept.ts @@ -1,7 +1,10 @@ import { docToMarkup } from '@locale/LocaleText'; import type Context from '@nodes/Context'; +import NameToken from '@nodes/NameToken'; +import NameType from '@nodes/NameType'; import type Node from '@nodes/Node'; -import type StructureDefinition from '@nodes/StructureDefinition'; +import StructureDefinition from '@nodes/StructureDefinition'; +import { PLACEHOLDER_SYMBOL } from '@parser/Symbols'; import type Locales from '../locale/Locales'; import type Emotion from '../lore/Emotion'; import type Markup from '../nodes/Markup'; @@ -70,8 +73,28 @@ export default class NodeConcept extends Concept { : [this.template.getLabel(locales)]; } - getRepresentation() { - return this.template; + getRepresentation(locales: Locales): Node { + // Find any names that use _ as a placeholder and replace them with a localized name for name. + const name = this.template.nodes( + (n): n is NameToken => + n instanceof NameToken && n.getText() === PLACEHOLDER_SYMBOL, + )[0]; + const nameTranslation = String(locales.get((l) => l.node.Name.name)); + const template = name + ? this.template.replace( + name, + new NameToken( + this.template instanceof StructureDefinition || + this.template instanceof NameType + ? nameTranslation + .charAt(0) + .toLocaleUpperCase(locales.getLocaleString()) + + nameTranslation.slice(1) + : nameTranslation, + ), + ) + : this.template; + return template; } getNodes(): Set { diff --git a/src/concepts/Purpose.ts b/src/concepts/Purpose.ts index e567598e1..fdaf00b3f 100644 --- a/src/concepts/Purpose.ts +++ b/src/concepts/Purpose.ts @@ -1,16 +1,36 @@ enum Purpose { - Bind = 'bind', - Type = 'type', - Evaluate = 'evaluate', - Value = 'value', - Input = 'input', - Output = 'output', - Decide = 'decide', - Convert = 'convert', - Project = 'project', - Document = 'document', - Source = 'source', - How = 'how', + /** Project-level concepts */ + Project = 'Project', + /** APIs related to program output */ + Outputs = 'Outputs', + /** APIs related to program input */ + Inputs = 'Inputs', + /** Language concepts related to decisions */ + Decisions = 'Decisions', + /** Language concepts related to naming things for later (binds, blocks) */ + Definitions = 'Definitions', + /** Language concepts related to text logic */ + Text = 'Text', + /** Language concepts related to number logic */ + Numbers = 'Numbers', + /** Language concepts related to boolean logic */ + Truth = 'Truth', + /** Language concepts related to list logic */ + Lists = 'Lists', + /** Language concepts related to sets and maps */ + Maps = 'Maps', + /** Language concepts related to tables */ + Tables = 'Tables', + /** Language concepts related to documentation */ + Documentation = 'Documentation', + /** Language concepts related to types and conversion */ + Types = 'Types', + /** Language concepts related to source code */ + Advanced = 'Advanced', + /** Language concepts that are hidden because they aren't helpful to show */ + Hidden = 'Hidden', + /** Tutorials and "how to" guides. Placed at the end since it doesn't appear in the UI. */ + How = 'How', } export default Purpose; diff --git a/src/concepts/StreamConcept.ts b/src/concepts/StreamConcept.ts index 6b4d34947..5f5788443 100644 --- a/src/concepts/StreamConcept.ts +++ b/src/concepts/StreamConcept.ts @@ -26,7 +26,7 @@ export default class StreamConcept extends Concept { readonly inputs: BindConcept[]; constructor(stream: StreamDefinition, locales: Locales, context: Context) { - super(Purpose.Input, undefined, context); + super(Purpose.Inputs, undefined, context); this.definition = stream; this.reference = Evaluate.make( @@ -37,7 +37,7 @@ export default class StreamConcept extends Concept { ); this.inputs = this.definition.inputs.map( - (bind) => new BindConcept(Purpose.Input, bind, locales, context), + (bind) => new BindConcept(Purpose.Inputs, bind, locales, context), ); } @@ -60,9 +60,7 @@ export default class StreamConcept extends Concept { } getDocs(locales: Locales): Markup[] { - return (this.definition.docs?.docs ?? []) - .map((doc) => doc.markup.concretize(locales, [])) - .filter((m) => m !== undefined); + return this.definition.docs.getMarkup(locales); } getNames() { diff --git a/src/concepts/StructureConcept.ts b/src/concepts/StructureConcept.ts index 738330a43..1c5819ea2 100644 --- a/src/concepts/StructureConcept.ts +++ b/src/concepts/StructureConcept.ts @@ -60,7 +60,7 @@ export default class StructureConcept extends Concept { ); this.examples = examples === undefined || examples.length === 0 - ? [this.definition.getEvaluateTemplate(locales, context)] + ? [this.definition.getEvaluateTemplate(locales, context, false)] : examples; this.functions = this.definition @@ -68,7 +68,7 @@ export default class StructureConcept extends Concept { .map( (def) => new FunctionConcept( - Purpose.Evaluate, + purpose, definition, def, this, @@ -124,9 +124,7 @@ export default class StructureConcept extends Concept { } getDocs(locales: Locales): Markup[] { - return (this.definition.docs?.docs ?? []) - .map((doc) => doc.markup.concretize(locales, [])) - .filter((m) => m !== undefined); + return this.definition.docs.getMarkup(locales); } getNames() { diff --git a/src/concepts/Templates.ts b/src/concepts/Templates.ts index 33937cbcf..b68c46a89 100644 --- a/src/concepts/Templates.ts +++ b/src/concepts/Templates.ts @@ -78,90 +78,18 @@ import WebLink from '../nodes/WebLink'; import Words from '../nodes/Words'; import { PLACEHOLDER_SYMBOL } from '../parser/Symbols'; -/** These are ordered by appearance in the docs. */ +/** These are ordered by appearance in the guide. */ const Templates: Node[] = [ - // Evaluation - Evaluate.make(ExpressionPlaceholder.make(), []), - Input.make('_', ExpressionPlaceholder.make()), - - FunctionDefinition.make( - undefined, - Names.make(['_']), - undefined, - [], - ExpressionPlaceholder.make(), - ), - new BinaryEvaluate( - ExpressionPlaceholder.make(), - Reference.make(PLACEHOLDER_SYMBOL), - ExpressionPlaceholder.make(), - ), - new UnaryEvaluate(Reference.make('-'), ExpressionPlaceholder.make()), - Block.make([ExpressionPlaceholder.make()]), - ExpressionPlaceholder.make(), - Convert.make(ExpressionPlaceholder.make(), TypePlaceholder.make()), - ConversionDefinition.make( - undefined, - new TypePlaceholder(), - new TypePlaceholder(), - ExpressionPlaceholder.make(), - ), - ListAccess.make( - ExpressionPlaceholder.make(ListType.make()), - ExpressionPlaceholder.make(), - ), - SetOrMapAccess.make( - ExpressionPlaceholder.make(SetType.make()), - ExpressionPlaceholder.make(), - ), - Insert.make(ExpressionPlaceholder.make(TableType.make())), - Select.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - Update.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - Delete.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - - // Project - Program.make([ExpressionPlaceholder.make()]), - new Source('?', '_'), - new Borrow(), - - // Decisions - Conditional.make( - ExpressionPlaceholder.make(BooleanType.make()), - ExpressionPlaceholder.make(), - ExpressionPlaceholder.make(), - ), - Is.make(ExpressionPlaceholder.make(), TypePlaceholder.make()), - Otherwise.make(ExpressionPlaceholder.make(), ExpressionPlaceholder.make()), - Match.make( - ExpressionPlaceholder.make(), - [ - KeyValue.make( - ExpressionPlaceholder.make(), - ExpressionPlaceholder.make(), - ), - ], - ExpressionPlaceholder.make(), - ), - IsLocale.make(Language.make(undefined)), - Initial.make(), - Changed.make(ExpressionPlaceholder.make(StreamType.make())), + // Inputs Reaction.make( ExpressionPlaceholder.make(), ExpressionPlaceholder.make(BooleanType.make()), ExpressionPlaceholder.make(), ), + Changed.make(ExpressionPlaceholder.make(StreamType.make())), Previous.make( ExpressionPlaceholder.make(StreamType.make()), - ExpressionPlaceholder.make(NumberType.make()), + NumberLiteral.make(1), ), // Bind @@ -171,16 +99,21 @@ const Templates: Node[] = [ undefined, ExpressionPlaceholder.make(), ), - Name.make('a'), - Names.make(['a', 'b']), - Reference.make('_'), + Block.make([ExpressionPlaceholder.make()]), + Evaluate.make(ExpressionPlaceholder.make(), []), + FunctionDefinition.make( + undefined, + Names.make([PLACEHOLDER_SYMBOL]), + undefined, + [], + ExpressionPlaceholder.make(), + ), StructureDefinition.make( undefined, - Names.make(['_']), + Names.make([PLACEHOLDER_SYMBOL]), [], undefined, [], - Block.make([ExpressionPlaceholder.make()]), ), PropertyReference.make( ExpressionPlaceholder.make(), @@ -195,40 +128,75 @@ const Templates: Node[] = [ ), This.make(), - // Types - BooleanType.make(), - TextType.make(), - NumberType.make(), - Unit.reuse(['unit']), - ListType.make(), - SetType.make(), - NoneType.make(), - MapType.make(TypePlaceholder.make(), TypePlaceholder.make()), - UnionType.make(TypePlaceholder.make(), TypePlaceholder.make()), - NameType.make('_'), - TypeInputs.make([]), - TypeVariables.make([]), - ConversionType.make(TypePlaceholder.make(), TypePlaceholder.make()), - Dimension.make(false, PLACEHOLDER_SYMBOL, 1), - new AnyType(), - FunctionType.make(undefined, [], TypePlaceholder.make()), - StreamType.make(), - TableType.make(), + // Decisions + Conditional.make( + ExpressionPlaceholder.make(BooleanType.make()), + ExpressionPlaceholder.make(), + ExpressionPlaceholder.make(), + ), + Otherwise.make(ExpressionPlaceholder.make(), ExpressionPlaceholder.make()), + Match.make( + ExpressionPlaceholder.make(), + [ + KeyValue.make( + ExpressionPlaceholder.make(), + ExpressionPlaceholder.make(), + ), + ], + ExpressionPlaceholder.make(), + ), - // Values - KeyValue.make(ExpressionPlaceholder.make(), ExpressionPlaceholder.make()), - new FormattedLiteral([FormattedTranslation.make([])]), - TextLiteral.make(''), + // Numbers NumberLiteral.make(0), + Dimension.make(false, PLACEHOLDER_SYMBOL, 1), + Unit.reuse(['unit']), + + // Truth BooleanLiteral.make(true), NoneLiteral.make(), + + // Lists ListLiteral.make(), + ListAccess.make( + ExpressionPlaceholder.make(ListType.make()), + ExpressionPlaceholder.make(), + ), Spread.make(ExpressionPlaceholder.make()), + + // Sets SetLiteral.make(), MapLiteral.make(), - TableLiteral.make(), + + // Text + TextLiteral.make(''), + FormattedLiteral.make([FormattedTranslation.make([])]), Translation.make(), FormattedTranslation.make([]), + Language.make('en'), + IsLocale.make(Language.make('en')), + + // Sets and Maps + KeyValue.make(ExpressionPlaceholder.make(), ExpressionPlaceholder.make()), + SetOrMapAccess.make( + ExpressionPlaceholder.make(SetType.make()), + ExpressionPlaceholder.make(), + ), + + // Tables + TableLiteral.make(), + Insert.make(ExpressionPlaceholder.make(TableType.make())), + Select.make( + ExpressionPlaceholder.make(TableType.make()), + ExpressionPlaceholder.make(BooleanType.make()), + ), + Update.make( + ExpressionPlaceholder.make(TableType.make()), + ExpressionPlaceholder.make(BooleanType.make()), + ), + Delete.make( + ExpressionPlaceholder.make(TableType.make()), + ExpressionPlaceholder.make(BooleanType.make()), + ), // Documentation Doc.make([new Paragraph([Words.make()])]), @@ -242,11 +210,53 @@ const Templates: Node[] = [ ]), ConceptLink.make(PLACEHOLDER_SYMBOL), WebLink.make('🔗', 'http://wordplay.dev'), - Language.make('en'), - Example.make(Program.make()), + Example.make(Program.make([ExpressionPlaceholder.make()])), new Paragraph([Words.make()]), Words.make(), + + // Types + Is.make(ExpressionPlaceholder.make(), TypePlaceholder.make()), + TextType.make(), + BooleanType.make(), + NoneType.make(), + NumberType.make(), + ListType.make(), + SetType.make(), + MapType.make(TypePlaceholder.make(), TypePlaceholder.make()), + TableType.make(), + NameType.make(PLACEHOLDER_SYMBOL), + FunctionType.make(undefined, [], TypePlaceholder.make()), + UnionType.make(TypePlaceholder.make(), TypePlaceholder.make()), + ConversionDefinition.make( + undefined, + new TypePlaceholder(), + new TypePlaceholder(), + ExpressionPlaceholder.make(), + ), + TypeInputs.make([]), + TypeVariables.make([]), + new AnyType(), + StreamType.make(), + + // Advanced + Initial.make(), + Input.make('_', ExpressionPlaceholder.make()), + new BinaryEvaluate( + ExpressionPlaceholder.make(), + Reference.make(PLACEHOLDER_SYMBOL), + ExpressionPlaceholder.make(), + ), + new UnaryEvaluate(Reference.make('-'), ExpressionPlaceholder.make()), + ExpressionPlaceholder.make(), + Convert.make(ExpressionPlaceholder.make(), TypePlaceholder.make()), + Name.make(PLACEHOLDER_SYMBOL), + Names.make([PLACEHOLDER_SYMBOL]), + Reference.make(PLACEHOLDER_SYMBOL), + ConversionType.make(TypePlaceholder.make(), TypePlaceholder.make()), new UnparsableExpression([]), + Program.make([ExpressionPlaceholder.make()]), + new Source('?', PLACEHOLDER_SYMBOL), + new Borrow(), ]; export { Templates as default }; diff --git a/src/conflicts/Conflict.ts b/src/conflicts/Conflict.ts index f40160b85..b13e6a335 100644 --- a/src/conflicts/Conflict.ts +++ b/src/conflicts/Conflict.ts @@ -2,7 +2,7 @@ import type Project from '@db/projects/Project'; import type LocaleText from '@locale/LocaleText'; import type { InternalConflictText } from '@locale/NodeTexts'; import type Context from '@nodes/Context'; -import type Node from '@nodes/Node'; +import Node from '@nodes/Node'; import type Locales from '../locale/Locales'; import type Markup from '../nodes/Markup'; @@ -54,5 +54,18 @@ export default abstract class Conflict { return this.constructor.name; } + /** A conflict is equal if it's the same constructor, the same number of nodes, and all nodes are equivalent. */ + isEqualTo(other: Conflict): boolean { + if (this.constructor !== other.constructor) return false; + const theseNodes = Object.values(this).filter((f) => f instanceof Node); + const thoseNodes = Object.values(other).filter( + (f) => f instanceof Node, + ); + if (theseNodes.length !== thoseNodes.length) return false; + return theseNodes.every((these, index) => + these.isEqualTo(thoseNodes[index]), + ); + } + abstract getLocalePath(): ConflictLocaleAccessor; } diff --git a/src/conflicts/IgnoredExpression.ts b/src/conflicts/IgnoredExpression.ts index 234f60ca0..4cef5d1b4 100644 --- a/src/conflicts/IgnoredExpression.ts +++ b/src/conflicts/IgnoredExpression.ts @@ -121,7 +121,7 @@ export class IgnoredExpression extends Conflict { } return { primary: { - node: this.block, + node: this.block.statements.at(-1) ?? this.block, explanation: (locales: Locales, context: Context) => locales.concretize( (l) => IgnoredExpression.LocalePath(l).primary, diff --git a/src/conflicts/PossiblePII.ts b/src/conflicts/PossiblePII.ts index 821d97f9e..ed67ae4a3 100644 --- a/src/conflicts/PossiblePII.ts +++ b/src/conflicts/PossiblePII.ts @@ -15,7 +15,7 @@ export class PossiblePII extends Conflict { readonly pii: PII; constructor(text: Token, pii: PII) { - super(false); + super(true); this.text = text; this.pii = pii; diff --git a/src/conflicts/TestUtilities.ts b/src/conflicts/TestUtilities.ts index c9c6ca156..9e4e61cef 100644 --- a/src/conflicts/TestUtilities.ts +++ b/src/conflicts/TestUtilities.ts @@ -1,5 +1,4 @@ import Block from '@nodes/Block'; -import Context from '@nodes/Context'; import Expression from '@nodes/Expression'; import Source from '@nodes/Source'; import { expect } from 'vitest'; @@ -31,7 +30,7 @@ export function testConflict( expect(goodOp).toBeInstanceOf(nodeType); expect( goodOp - ?.getConflicts(new Context(goodProject, goodSource)) + ?.getConflicts(goodProject.getContext(goodSource)) .filter((n) => n instanceof conflictType)[0], ).toBeUndefined(); @@ -42,7 +41,7 @@ export function testConflict( nodeIndex ]; expect(badOp).toBeInstanceOf(nodeType); - const conflicts = badOp?.getConflicts(new Context(badProject, badSource)); + const conflicts = badOp?.getConflicts(badProject.getContext(badSource)); expect(conflicts?.find((c) => c instanceof conflictType)).toBeInstanceOf( conflictType, ); @@ -61,7 +60,7 @@ export function testTypes( : undefined; const lastIsExpression = last instanceof Expression; if (last instanceof Expression) { - const type = last.getType(new Context(project, source)); + const type = last.getType(project.getContext(source)); const match = type instanceof typeExpected; if (!match) console.log(`Type of expression ${last.toWordplay()} is ${type}`); diff --git a/src/conflicts/UnknownInput.ts b/src/conflicts/UnknownInput.ts index 8f2117028..663a37d17 100644 --- a/src/conflicts/UnknownInput.ts +++ b/src/conflicts/UnknownInput.ts @@ -1,5 +1,4 @@ import type LocaleText from '@locale/LocaleText'; -import NodeRef from '@locale/NodeRef'; import Context from '@nodes/Context'; import type Evaluate from '@nodes/Evaluate'; import type FunctionDefinition from '@nodes/FunctionDefinition'; @@ -37,7 +36,7 @@ export default class UnknownInput extends Conflict { explanation: (locales: Locales, context: Context) => locales.concretize( (l) => UnknownInput.LocalePath(l).primary, - new NodeRef(this.func, locales, context), + this.func.getPreferredName(locales.getLocales()), ), }, secondary: { diff --git a/src/conflicts/UnknownName.ts b/src/conflicts/UnknownName.ts index 3bd383b65..6adde77a3 100644 --- a/src/conflicts/UnknownName.ts +++ b/src/conflicts/UnknownName.ts @@ -1,4 +1,4 @@ -import type Refer from '@edit/Refer'; +import type Refer from '@edit/revision/Refer'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type Context from '@nodes/Context'; diff --git a/src/db/projects/Project.ts b/src/db/projects/Project.ts index e2bc6c4e0..1ae655619 100644 --- a/src/db/projects/Project.ts +++ b/src/db/projects/Project.ts @@ -1,5 +1,6 @@ import Templates from '@concepts/Templates'; import type Conflict from '@conflicts/Conflict'; +import type { CaretPosition } from '@edit/caret/Caret'; import concretize from '@locale/concretize'; import { getBestSupportedLocales } from '@locale/getBestSupportedLocales'; import type Locale from '@locale/Locale'; @@ -23,7 +24,6 @@ import { DOCS_SYMBOL } from '@parser/Symbols'; import type createDefaultShares from '@runtime/createDefaultShares'; import { v4 as uuidv4 } from 'uuid'; import { Basis } from '../../basis/Basis'; -import type { CaretPosition } from '../../edit/Caret'; import DefaultLocale from '../../locale/DefaultLocale'; import Locales from '../../locale/Locales'; import type LocaleText from '../../locale/LocaleText'; @@ -524,10 +524,57 @@ export default class Project { return this.getAnalysis().conflicts; } + getNewConflictsBatch( + oldSource: Source, + newSources: Source[], + // Any conflict types to ignore + negligibleConflicts: (new () => Conflict)[], + ): Map { + // Get the current conflicts. + const currentConflicts = this.getMajorConflictsNow(); + const newConflictsBySource = new Map(); + // For all of the new sources, get the new conflicts caused by the revision. + for (const newSource of newSources) { + let newConflicts = this.withSource(oldSource, newSource) + .getMajorConflictsNow() + .filter( + (conflict) => + !negligibleConflicts.some( + (neglibile) => conflict instanceof neglibile, + ), + ); + + // Remove all current conflicts that are in the new conflicts. + newConflictsBySource.set( + newSource, + newConflicts.filter( + (newConflict) => + !currentConflicts.some((oldConflict) => + oldConflict.isEqualTo(newConflict), + ), + ), + ); + } + return newConflictsBySource; + } + + getNewConflicts( + oldSource: Source, + newSource: Source, + negligibleConflicts: (new () => Conflict)[], + ): Conflict[] { + const newConflicts = this.getNewConflictsBatch( + oldSource, + [newSource], + negligibleConflicts, + ); + return Array.from(newConflicts.values())[0]; + } + getMajorConflictsNow() { let conflicts: Conflict[] = []; for (const source of this.getSources()) { - const context = new Context(this, source); + const context = this.getContext(source); for (const node of source.nodes()) { conflicts = [...conflicts, ...node.computeConflicts(context)]; } @@ -537,7 +584,7 @@ export default class Project { hasMajorConflictsNow() { for (const source of this.getSources()) { - const context = new Context(this, source); + const context = this.getContext(source); for (const node of source.nodes()) { if ( node @@ -962,7 +1009,7 @@ export default class Project { // We changed the documentation symbol. Automatically convert it when deserializing. by seeing if there are 2 or more `` in the code, // and no ¶ and if so, replace them with the new symbol. (source.code.match(/``/g) || []).length >= 2 && - (source.code.match(/¶/g) || []).length === 0 + (source.code.match(/¶/g) || []).length === 0 ? source.code.replaceAll('``', DOCS_SYMBOL) : source.code, ); diff --git a/src/db/projects/ProjectsDatabase.svelte.ts b/src/db/projects/ProjectsDatabase.svelte.ts index ef30c0b3d..bbb647f7c 100644 --- a/src/db/projects/ProjectsDatabase.svelte.ts +++ b/src/db/projects/ProjectsDatabase.svelte.ts @@ -585,6 +585,9 @@ export default class ProjectsDatabase { /** Persist in storage */ async persist() { + // Note that we're saving. + this.database.setStatus(SaveStatus.Saving, undefined); + const userID = this.database.getUserID(); // Before doing anything, ensure all editable projects that don't have an owner have one. @@ -739,9 +742,6 @@ export default class ProjectsDatabase { * Should be called any time this.projects is modified. */ saveSoon() { - // Note that we're saving. - this.database.setStatus(SaveStatus.Saving, undefined); - // Clear pending saves. clearTimeout(this.timer); diff --git a/src/db/settings/BlocksSetting.ts b/src/db/settings/BlocksSetting.ts index 7a8830647..883c5db97 100644 --- a/src/db/settings/BlocksSetting.ts +++ b/src/db/settings/BlocksSetting.ts @@ -3,8 +3,7 @@ import Setting from './Setting'; export const BlocksSetting = new Setting( 'blocks', true, - false, - // Deactivating blocks mode for now; it's too unstable. - (value) => false, //(typeof value === 'boolean' ? value : false), + true, + (value) => (typeof value === 'boolean' ? value : false), (current, value) => current === value, ); diff --git a/src/edit/EditContext.ts b/src/edit/EditContext.ts deleted file mode 100644 index 576d68e66..000000000 --- a/src/edit/EditContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type Context from '@nodes/Context'; -import type Node from '@nodes/Node'; -import type Type from '@nodes/Type'; - -type EditContext = { node: Node; context: Context; type: Type | undefined }; - -export type { EditContext as default }; diff --git a/src/edit/Refer.ts b/src/edit/Refer.ts deleted file mode 100644 index 6da612dcb..000000000 --- a/src/edit/Refer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type Context from '@nodes/Context'; -import type Definition from '@nodes/Definition'; -import type Node from '@nodes/Node'; -import TypeVariable from '@nodes/TypeVariable'; -import type Locales from '../locale/Locales'; - -export default class Refer { - readonly creator: (name: string, operator?: string) => Node; - readonly definition: Definition; - - constructor( - creator: (name: string, op?: string) => Node, - definition: Definition, - ) { - this.creator = creator; - this.definition = definition; - } - - getNode(locales: Locales) { - return this.creator( - this.definition.getPreferredName(locales.getLocales()), - this.definition.names.getSymbolicName(), - ); - } - - getType(context: Context) { - return this.definition instanceof TypeVariable - ? undefined - : this.definition.getType(context); - } - - equals(refer: Refer) { - return refer.definition === this.definition; - } - - toString() { - return `${this.definition.getNames()[0]}`; - } -} diff --git a/src/edit/Revision.ts b/src/edit/Revision.ts deleted file mode 100644 index 59fe16891..000000000 --- a/src/edit/Revision.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type Context from '@nodes/Context'; -import type Node from '@nodes/Node'; -import type Source from '@nodes/Source'; -import type Spaces from '@parser/Spaces'; -import type { Edit } from '../components/editor/util/Commands'; -import type Locales from '../locale/Locales'; -import type Markup from '../nodes/Markup'; - -export default abstract class Revision { - readonly context: Context; - - constructor(context: Context) { - this.context = context; - } - - /** True if the revision will insert some named thing (e.g., a Refer) */ - abstract isReference(): boolean; - - /** True if the revision removes something */ - abstract isRemoval(): boolean; - - /** True if the revision is a completion */ - abstract isCompletion(locales: Locales): boolean; - - /** Create the edit to be processed by Editor. */ - abstract getEdit(locales: Locales): Edit | undefined; - - abstract getDescription(locales: Locales): Markup; - - /** Gets the node to be added, removed, inserted, etc. */ - abstract getNewNode(locales: Locales): Node | undefined; - - /** Gets the added or removed node, and the revised node, which incorporates the new node. May be the same node. Used for the actual edit, but also for previews. */ - abstract getEditedNode(locales: Locales): [Node, Node]; - - abstract equals(transform: Revision): boolean; - - static splitSpace(source: Source, position: number, newNode: Node): Spaces { - const tokenAfter = source.getTokenAt(position); - let newSpaces = source.spaces; - if (tokenAfter !== undefined) { - const indexAfter = source.getTokenSpacePosition(tokenAfter); - if (indexAfter === undefined) return newSpaces; - const spaceAfter = source.spaces.getSpace(tokenAfter); - const spaceOffset = position - indexAfter; - const newSpaceBefore = spaceAfter.substring(0, spaceOffset); - const newSpaceAfter = spaceAfter.substring(spaceOffset); - newSpaces = newSpaces - .withSpace(newNode, newSpaceBefore) - .withSpace(tokenAfter, newSpaceAfter); - } - return newSpaces; - } -} diff --git a/src/edit/Caret.ts b/src/edit/caret/Caret.ts similarity index 81% rename from src/edit/Caret.ts rename to src/edit/caret/Caret.ts index 7b3e769d8..01795b54e 100644 --- a/src/edit/Caret.ts +++ b/src/edit/caret/Caret.ts @@ -1,64 +1,70 @@ +import IncompatibleCellType from '@conflicts/IncompatibleCellType'; +import IncompatibleInput from '@conflicts/IncompatibleInput'; +import IncompatibleType from '@conflicts/IncompatibleType'; import { UnknownName } from '@conflicts/UnknownName'; +import { UnknownTypeName } from '@conflicts/UnknownTypeName'; +import type { LocaleTextAccessor } from '@locale/Locales'; import BinaryEvaluate from '@nodes/BinaryEvaluate'; import Block from '@nodes/Block'; -import Evaluate from '@nodes/Evaluate'; import Expression from '@nodes/Expression'; import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; -import FunctionType from '@nodes/FunctionType'; +import FunctionDefinition from '@nodes/FunctionDefinition'; import ListLiteral from '@nodes/ListLiteral'; -import Node, { Empty, ListOf, type Field } from '@nodes/Node'; +import Node, { Empty, FieldKind, ListOf, type Field } from '@nodes/Node'; +import NumberType from '@nodes/NumberType'; import Program from '@nodes/Program'; -import PropertyReference from '@nodes/PropertyReference'; import Source from '@nodes/Source'; -import StructureDefinitionType from '@nodes/StructureDefinitionType'; -import StructureType from '@nodes/StructureType'; import Sym from '@nodes/Sym'; import Token from '@nodes/Token'; +import Unit from '@nodes/Unit'; import Spaces from '@parser/Spaces'; -import { - CONVERT_SYMBOL, - ELISION_SYMBOL, - EVAL_OPEN_SYMBOL, - LIST_OPEN_SYMBOL, - PROPERTY_SYMBOL, - SET_OPEN_SYMBOL, - STREAM_SYMBOL, -} from '@parser/Symbols'; +import { ELISION_SYMBOL, PROPERTY_SYMBOL } from '@parser/Symbols'; import { DelimiterCloseByOpen, DelimiterOpenByClose, - FormattingSymbols, isName, - OperatorRegEx, TextOpenByTextClose, + tokens, } from '@parser/Tokenizer'; import getPreferredSpaces from '@parser/getPreferredSpaces'; import type { Edit, ProjectRevision, Revision, -} from '../components/editor/util/Commands'; -import type Conflict from '../conflicts/Conflict'; -import type Project from '../db/projects/Project'; -import type LanguageCode from '../locale/LanguageCode'; -import NodeRef from '../locale/NodeRef'; -import Bind from '../nodes/Bind'; -import BooleanLiteral from '../nodes/BooleanLiteral'; -import Context from '../nodes/Context'; -import type Definition from '../nodes/Definition'; -import DefinitionExpression from '../nodes/DefinitionExpression'; -import { LanguageTagged } from '../nodes/LanguageTagged'; -import Literal from '../nodes/Literal'; -import MapLiteral from '../nodes/MapLiteral'; -import Name from '../nodes/Name'; -import NameType from '../nodes/NameType'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Reference from '../nodes/Reference'; -import SetLiteral from '../nodes/SetLiteral'; -import Translation from '../nodes/Translation'; -import Type from '../nodes/Type'; -import TypeVariable from '../nodes/TypeVariable'; -import UnicodeString from '../unicode/UnicodeString'; +} from '../../components/editor/commands/Commands'; +import type Conflict from '../../conflicts/Conflict'; +import Project from '../../db/projects/Project'; +import type LanguageCode from '../../locale/LanguageCode'; +import NodeRef from '../../locale/NodeRef'; +import Bind from '../../nodes/Bind'; +import BooleanLiteral from '../../nodes/BooleanLiteral'; +import Context from '../../nodes/Context'; +import type Definition from '../../nodes/Definition'; +import DefinitionExpression from '../../nodes/DefinitionExpression'; +import { LanguageTagged } from '../../nodes/LanguageTagged'; +import Literal from '../../nodes/Literal'; +import Name from '../../nodes/Name'; +import NameType from '../../nodes/NameType'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Reference from '../../nodes/Reference'; +import SetLiteral from '../../nodes/SetLiteral'; +import Translation from '../../nodes/Translation'; +import Type from '../../nodes/Type'; +import TypeVariable from '../../nodes/TypeVariable'; +import UnicodeString from '../../unicode/UnicodeString'; +import { completeInsertion } from './Complete'; + +/** + * Conflicts that are permitted on insertion. We permit these to allow for some + * flexibility in typing names and for thinking through types. + */ +export const NegligibleConflicts: (new (...args: any[]) => Conflict)[] = [ + UnknownName, + UnknownTypeName, + IncompatibleCellType, + IncompatibleType, + IncompatibleInput, +]; export type InsertionContext = { before: Node[]; after: Node[] }; @@ -68,6 +74,17 @@ export type InsertionContext = { before: Node[]; after: Node[] }; export type CaretPosition = number | Node | [number, number]; export type Entry = 'previous' | 'next' | undefined; +export function isCaretPosition(position: any): position is CaretPosition { + return ( + typeof position === 'number' || + position instanceof Node || + (Array.isArray(position) && + position.length === 2 && + typeof position[0] === 'number' && + typeof position[1] === 'number') + ); +} + export function isPosition(position: CaretPosition): position is number { return typeof position === 'number'; } @@ -87,10 +104,16 @@ export default class Caret { // The node recently added. readonly addition: Node | undefined; - // A cache of the token we're at, since we use it frequently. + // The token we're at, including preceding space. readonly tokenIncludingSpace: Token | undefined; + + // The token before the caret. readonly tokenPrior: Token | undefined; + + // The index of the beginning of the token's preceding space. readonly tokenSpaceIndex: number | undefined; + + // The token the caret is at, excluding preceding space. readonly tokenExcludingSpace: Token | undefined; constructor( @@ -276,6 +299,12 @@ export default class Caret { ); } + getLine() { + return this.source.getLine( + Array.isArray(this.position) ? this.position[1] : this.position, + ); + } + getNodesBetween() { const empty = { before: [], after: [] }; @@ -463,10 +492,10 @@ export default class Caret { } left(sibling: boolean): Caret { - return this.moveInline(sibling, -1); + return this.moveInlineText(sibling, -1); } right(sibling: boolean): Caret { - return this.moveInline(sibling, 1); + return this.moveInlineText(sibling, 1); } nextNewline(direction: -1 | 1): Caret | undefined { @@ -481,7 +510,7 @@ export default class Caret { ); } - moveInline(sibling: boolean, direction: -1 | 1): Caret { + moveInlineText(sibling: boolean, direction: -1 | 1): Caret { // Map the direction onto an entry direction. const entry = direction > 0 ? 'next' : 'previous'; if (this.isNode()) { @@ -584,18 +613,34 @@ export default class Caret { else return this; } - static isBlockEditable(token: Token): boolean { + /** A block editable token is a non-reference name, words, or a number. */ + static isTokenTextBlockEditable( + token: Token, + parent: Node | undefined, + ): boolean { + return ( + token.isSymbol(Sym.Words) || + token.isSymbol(Sym.Number) || + token.isSymbol(Sym.URL) || + (token.isSymbol(Sym.Name) && + parent !== undefined && + !(parent instanceof Reference)) + ); + } + + static isTokenBlockEditable(token: Token): boolean { return ( token.isSymbol(Sym.Name) || token.isSymbol(Sym.Operator) || token.isSymbol(Sym.Words) || token.isSymbol(Sym.Number) || + token.isSymbol(Sym.Text) || token.isSymbol(Sym.Boolean) || token.isSymbol(Sym.Placeholder) ); } - getSourceBlockPositions(): (Node | number)[] { + getBlockPositions(): (Node | number)[] { // Find all the tokens in a series of fields, for identifying positions before and after lists. function getFieldTokens(node: Node, fields: Field[]) { return fields @@ -610,39 +655,52 @@ export default class Caret { } const points: (Node | number)[] = []; - for (const node of this.source.expression.nodes()) { + const nodes = this.source.expression.nodes(); + for (const node of nodes) { if (node instanceof Token) { - // Find the preceding space and include all line breaks, but only if the space root is for a block. - const spaceRoot = this.source.root.getSpaceRoot(node); - const spaceRootParent = spaceRoot - ? this.source.root.getParent(spaceRoot) - : undefined; - if ( - spaceRootParent instanceof Block && - spaceRootParent.isRoot() - ) { - const space = this.source.spaces.getSpace(node); - const position = this.source.getTokenTextPosition(node); - if (position !== undefined) { - for (let index = 0; index < space.length; index++) { - if (space.charAt(index) === '\n') { - points.push(position - space.length + index); - } + const tokenParent = this.source.root.getParent(node); + // Include all preceding spaces, since we render them. + const space = this.source.spaces.getSpace(node); + const position = this.source.getTokenTextPosition(node); + if (position !== undefined) { + for (let index = 0; index < space.length; index++) { + if (space.charAt(index) === ' ') { + points.push(position - space.length + index); } } } - // If the token itself is editable, add it to the list. - if (Caret.isBlockEditable(node)) points.push(node); + + // If the token's individual symbols are editable, include the many positions. + if (Caret.isTokenTextBlockEditable(node, tokenParent)) { + const start = this.source.getTokenTextPosition(node); + const end = this.source.getTokenLastPosition(node); + if (start !== undefined && end !== undefined) { + for (let pos = start; pos <= end; pos++) + points.push(pos); + } + } + // If the token itself is editable and not an only child, add it to the list. + else if (Caret.isTokenBlockEditable(node)) { + // If an only child or a placeholder, include it's parent, not the token itself. + if ( + (tokenParent !== undefined && + tokenParent.hasOneLeaf()) || + tokenParent instanceof ExpressionPlaceholder + ) + points.push(tokenParent); + else points.push(node); + } } // If it's not a token, check it's grammar for insertion points. else { - // Expression or type with a single token? Include it. + // Expression or type with a single child ? Include it. if ( - (node instanceof Expression || node instanceof Type) && - node.leaves().length === 1 + (node instanceof Literal || node instanceof Name) && + node.hasOneLeaf() ) points.push(node); + // Inspect the grammar of the node for a list of insertion points. const grammar = node.getGrammar(); for (let index = 0; index < grammar.length; index++) { const field = grammar[index]; @@ -751,7 +809,7 @@ export default class Caret { } /** Move to the next node or position in blocks mode. */ - moveInlineSemantic(direction: -1 | 1): Caret | undefined { + moveInlineBlock(direction: -1 | 1): Caret | LocaleTextAccessor { // Find the current position. const currentPosition = isPosition(this.position) ? this.position @@ -762,39 +820,47 @@ export default class Caret { : this.source.getNodeLastPosition(this.position); // No current position for some reason? No change; - if (currentPosition === undefined) return undefined; + if (currentPosition === undefined) + return (l) => l.ui.source.cursor.ignored.noMove; // Get all eligible caret positions in blocks mode, in the order in which we'll search for the next position. - const positions = - direction < 0 - ? this.getSourceBlockPositions().reverse() - : this.getSourceBlockPositions(); + const positions = this.getBlockPositions(); + if (direction < 0) positions.reverse(); + + // Go through all of the eligible positions const onNode = this.isNode(); - for (const position of positions) { - const isPosition = typeof position === 'number'; - const thisPosition = isPosition - ? position - : this.source.getNodeFirstPosition(position); + for (const possible of positions) { + const possibleIsIndex = typeof possible === 'number'; + const possibleIndex = possibleIsIndex + ? possible + : this.source.getNodeFirstPosition(possible); // Is this position after the current position, or at the same position, but moving from a node? This is the next position. if ( - thisPosition !== undefined && + possibleIndex !== undefined && + // Moving next? (direction > 0 - ? thisPosition > currentPosition || - (thisPosition === currentPosition && + ? // Is the possible index after the current index? + possibleIndex > currentPosition || + // Is the possible index the same as the current index but we're currently on a node and the possible is an index? + (possibleIndex === currentPosition && onNode && - isPosition) || + possibleIsIndex) || + // Are not on a node and the possible index is a node at the current position? (!onNode && - !isPosition && - thisPosition === currentPosition) - : thisPosition < currentPosition || - (this.position instanceof Node && - typeof position === 'number' && - thisPosition === currentPosition)) + !possibleIsIndex && + possibleIndex === currentPosition) + : // Moving previous? + // Is the possible index before the current position? + possibleIndex < currentPosition || + // Is the current position a node and the possible index is at the node's first position? + (onNode && + typeof possible === 'number' && + possibleIndex === currentPosition)) ) { - return this.withPosition(position); + return this.withPosition(possible); } } - return undefined; + return (l) => l.ui.source.cursor.ignored.noMove; } expandInline(amount: number) { @@ -885,8 +951,7 @@ export default class Caret { getParentOfOnlyChild(token: Token): Node { const parent = this.source.root.getParent(token); - const tokens = parent?.leaves(); - return parent && tokens && tokens.length === 1 && tokens[0] === token + return parent && parent.hasOneLeaf() && parent.leaves()[0] === token ? parent : token; } @@ -1050,13 +1115,19 @@ export default class Caret { insert( text: string, // Whether in blocks mode, meaning no syntax errors allowed. - validOnly: boolean, + blocks: boolean, project: Project, complete = true, - ): Edit | ProjectRevision | undefined { + ): Edit | ProjectRevision | LocaleTextAccessor { // Normalize the mystery string, ensuring it follows Unicode normalization form. text = text.normalize(); + if (blocks) { + // Don't permit tabs or newlines unless inside a block editable token. + if (text == '\t') + return (l) => l.ui.source.cursor.ignored.blockSpace; + } + // See if it's a rename. const renameEdit = project ? this.insertRename(text, project) @@ -1073,16 +1144,41 @@ export default class Caret { // Before doing insertion, see if a node is selected, and if so, wrap or remove it. if (this.position instanceof Node) { + // Is this a placeholder being replaced with numbers? Replace it, preserving units. + if ( + tokens(text)[0].isSymbol(Sym.Number) && + this.position instanceof ExpressionPlaceholder && + this.position.type instanceof NumberType && + this.position.type.unit instanceof Unit + ) { + newSource = this.source.replace( + this.position, + new NumberLiteral( + new Token(text, Sym.Number), + this.position.type.unit.clone(), + ), + ); + newPosition = + (this.source.getNodeFirstPosition(this.position) ?? 0) + 1; + return [ + newSource, + this.withSource(newSource).withPosition(newPosition), + ]; + } + // Try wrapping the node - const wrap = this.wrap(text); + const wrap = this.wrap(project, text); if (wrap !== undefined) return wrap; // If that didn't do anything, try deleting the node. const edit = this.deleteNode(this.position, false, project); // If that didn't do anything, do nothing; it's not removeable. - if (edit === undefined) return; + if (edit === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; + if (!Array.isArray(edit)) return edit; const [source, caret] = edit; - if (!isPosition(caret.position)) return; + if (!isPosition(caret.position)) + return (l) => l.ui.source.cursor.ignored.noDelete; // Otherwise, we deleted it! Update the source and position. newSource = source; @@ -1110,68 +1206,81 @@ export default class Caret { originalPosition = start; } else { const edit = this.source.withoutGraphemesBetween(start, end); - if (edit === undefined) return; + if (edit === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; newSource = edit; newPosition = start; originalPosition = start; } } - // Let's see if we should do any delimiter completion before we insert. - const autocompletion = this.completeInsertion( - text, - complete, - newSource, - newPosition, - project, - validOnly, - ); + // Construct a new caret based on the revision. + const newCaret = this.withSource(newSource).withPosition(newPosition); - // Keep track of whether we closed a delimiter. - let closed = false; + // Let's see if we should do any delimiter completion before we insert. + const completion = complete + ? completeInsertion(project, newCaret, text, blocks) + : undefined; - // Update the source, position, and text of delimiter completion. - if (autocompletion) - [text, newSource, newPosition, closed] = autocompletion; + // Update the source, position, and text of delimiter completion, if there is one. + if (completion) { + const [newSource, newPosition] = completion; - // Did we somehow get no source? - if (newSource === undefined) return undefined; + // Finally, if we're in blocks mode, verify that the insertion was valid. + if (blocks) { + if ( + project.getNewConflicts( + this.source, + newSource, + NegligibleConflicts, + ).length > 0 + ) + return (l) => l.ui.source.cursor.ignored.noError; + } - // After the autcomplete, are we no longer inserting text, as indicated by empty text insertion? - // Return what autocomplete returned. - if (text === '' || typeof newPosition !== 'number') return [ newSource, this.withSource(newSource).withPosition(newPosition), ]; + } + // No autocomplete, + else { + // Insert the possibly revised text. + newSource = newSource.withGraphemesAt(text, newPosition); - // Insert the possibly revised text. - newSource = newSource.withGraphemesAt(text, newPosition); + // Did we somehow get no source? Bail. + if (newSource === undefined) + return (l) => l.ui.source.cursor.ignored.noInsert; - // Did we somehow get no source? Bail. - if (newSource === undefined) return undefined; + // What's the new token we added? + const newToken = newSource.getTokenAt(originalPosition, false); - // What's the new token we added? - const newToken = newSource.getTokenAt(originalPosition, false); + // Find the position. + newPosition = newPosition + new UnicodeString(text).getLength(); - // Find the position. - newPosition = - newPosition + (closed ? 1 : new UnicodeString(text).getLength()); + // Finally, if we're in blocks mode, verify that the insertion was valid. + if (blocks) { + if ( + project.getNewConflicts( + this.source, + newSource, + NegligibleConflicts, + ).length > 0 + ) + return (l) => l.ui.source.cursor.ignored.noError; + } - // Finally, if we're in blocks mode, verify that the insertion was valid. - if (validOnly) { - const currentConflicts = project.getMajorConflictsNow().length; - const conflicts = project - .withSource(this.source, newSource) - .getMajorConflictsNow() - .filter((conflict) => !(conflict instanceof UnknownName)); - if (conflicts.length > currentConflicts) return undefined; + return [ + newSource, + new Caret( + newSource, + newPosition, + undefined, + undefined, + newToken, + ), + ]; } - - return [ - newSource, - new Caret(newSource, newPosition, undefined, undefined, newToken), - ]; } insertRename(text: string, project: Project) { @@ -1243,144 +1352,6 @@ export default class Caret { ); } - completeInsertion( - text: string, - complete: boolean, - source: Source, - position: number, - project: Project, - validOnly: boolean, - ): [string, Source, number | Node, boolean] | undefined { - let newSource: Source | undefined = source; - let newPosition: number | Node = position; - let closed = false; - - // If the inserted character is an open parenthesis, see if we can construct an evaluate with the preceding expression. - if (text === EVAL_OPEN_SYMBOL) { - // Find the top most expression that ends where the caret is. - const precedingExpression = this.source - .nodes() - .filter( - (node): node is Expression => - (node instanceof Reference || - node instanceof PropertyReference || - (node instanceof Block && !node.isRoot())) && - source.getNodeLastPosition(node) === position, - ) - .at(-1); - - if (precedingExpression) { - const context = project.getNodeContext(precedingExpression); - const fun = precedingExpression.getType(context); - if ( - fun instanceof FunctionType || - fun instanceof StructureType || - fun instanceof StructureDefinitionType - ) { - const evaluate = Evaluate.make(precedingExpression, []); - // Make a new source - newSource = source.replace(precedingExpression, evaluate); - // Place the caret on the placeholder - newPosition = - (evaluate instanceof Evaluate - ? evaluate.close - ? newSource.getNodeFirstPosition(evaluate.close) - : newSource.getNodeLastPosition(evaluate) - : position) ?? position; - // Don't insert anything. - text = ''; - return [text, newSource, newPosition, closed]; - } - } - } - - // If the inserted string matches a single matched delimiter, complete it, unless: - // 1) we’re immediately before an matched closing delimiter, in which case we insert nothing, but move the caret forward - // 2) the character being inserted closes an unmatched delimiter, in which case we just insert the character. - if ( - complete && - text in DelimiterCloseByOpen && - ((!this.isInsideWords() && - (!FormattingSymbols.includes(text) || - // Allow the elision symbol, since it can be completed outside of words. - text === ELISION_SYMBOL)) || - (this.isInsideWords() && FormattingSymbols.includes(text))) && - (this.tokenPrior === undefined || - // The text typed does not close an unmatched delimiter - (this.source.getUnmatchedDelimiter(this.tokenPrior, text) === - undefined && - !( - // The token prior is text or unknown - ( - this.tokenPrior.isSymbol(Sym.Text) || - this.tokenPrior.isSymbol(Sym.Unknown) - ) - ))) - ) { - text += DelimiterCloseByOpen[text]; - closed = true; - } - // If the two preceding characters are dots and this is a dot, delete the last two dots then insert the stream symbol. - else if ( - text === '.' && - source.getGraphemeAt(position - 1) === '.' && - source.getGraphemeAt(position - 2) === '.' - ) { - text = STREAM_SYMBOL; - newSource = source - .withoutGraphemeAt(position - 2) - ?.withoutGraphemeAt(position - 2); - newPosition = position - 2; - } - // If the preceding character is an arrow dash, delete the dash and insert the arrow - else if (text === '>' && source.getGraphemeAt(position - 1) === '-') { - text = CONVERT_SYMBOL; - newSource = source.withoutGraphemeAt(position - 1); - newPosition = position - 1; - } - // If the inserted character is an operator, see if we can construct a binary evaluation with the - // the preceding expression and a placeholder on the right. - else if (validOnly && OperatorRegEx.test(text)) { - // Find the top most expression that ends where the caret is. - const precedingExpression = this.source - .nodes() - .filter( - (node): node is Expression => - node instanceof Expression && - !(node instanceof Program) && - !(node instanceof Source) && - !(node instanceof Block && node.isRoot()) && - source.getNodeLastPosition(node) === position, - )[0]; - - if ( - precedingExpression && - precedingExpression - .getType(project.getNodeContext(precedingExpression)) - .getDefinitionOfNameInScope( - text, - project.getNodeContext(precedingExpression), - ) !== undefined - ) { - const binary = new BinaryEvaluate( - precedingExpression instanceof Literal - ? precedingExpression - : Block.make([precedingExpression]), - Reference.make(text), - ExpressionPlaceholder.make(), - ); - // Make a new source - newSource = source.replace(precedingExpression, binary); - // Place the caret on the placeholder - newPosition = binary.right; - // Don't insert anything. - text = ''; - } - } - - return newSource ? [text, newSource, newPosition, closed] : undefined; - } - isInsideWords() { const isText = this.tokenExcludingSpace !== undefined && @@ -1396,10 +1367,17 @@ export default class Caret { } /** If the caret is a node, set the position to its first index */ - enter() { + enter(blocks: boolean) { if (this.position instanceof Node) { + const parent = this.source.root.getParent(this.position); // Token? Set a position. if (this.position instanceof Token) { + if ( + blocks && + !Caret.isTokenTextBlockEditable(this.position, parent) + ) + return false; + const index = this.source.getTokenTextPosition(this.position); return index !== undefined ? this.withPosition(index + 1) @@ -1412,6 +1390,11 @@ export default class Caret { const first = children[0]; if (first === undefined) return this; if (first instanceof Token && leaves.length === 1) { + if ( + blocks && + !Caret.isTokenTextBlockEditable(first, parent) + ) + return false; const index = this.source.getTokenTextPosition(first); return index !== undefined ? this.withPosition(index + 1) @@ -1424,19 +1407,8 @@ export default class Caret { } else return this; } - getRange(node: Node): [number, number] | undefined { - const tokens = node.nodes((t): t is Token => t instanceof Token); - const first = tokens[0]; - const last = tokens[tokens.length - 1]; - const firstIndex = this.source.getTokenTextPosition(first); - const lastIndex = this.source.getTokenLastPosition(last); - return firstIndex === undefined || lastIndex === undefined - ? undefined - : [firstIndex, lastIndex]; - } - replace(old: Node, replacement: string): Edit | undefined { - const range = this.getRange(old); + const range = this.source.getRange(old); if (range === undefined) return; const newCode = replacement; @@ -1563,7 +1535,7 @@ export default class Caret { project: Project, forward: boolean, validOnly: boolean, - ): Edit | ProjectRevision | undefined { + ): Edit | ProjectRevision | LocaleTextAccessor | undefined { const offset = forward ? 0 : -1; // If the position is a number, see if this is a rename @@ -1721,9 +1693,10 @@ export default class Caret { const node = this.position; const parent = this.source.root.getParent(node); - // Is the parent a wrapper? Unwrap it. + // Is the parent a wrapper and the node is a block delimiter? Unwrap it. if ( parent instanceof Block && + (node === parent.open || node === parent.close) && !parent.isRoot() && parent.statements.length === 1 ) { @@ -1732,12 +1705,13 @@ export default class Caret { this.withPosition(parent.statements[0]), ]; } - // Is the parent a list with on element? Unwrap the list. + // Is the parent a list or set with a single element amd the deletion is one of it's delimiters? Unwrap the list or set. else if ( (parent instanceof ListLiteral || - parent instanceof SetLiteral || - parent instanceof MapLiteral) && - parent.values.length === 1 + parent instanceof SetLiteral) && + parent.values.length === 1 && + (this.position === parent.open || + this.position === parent.close) ) { return [ this.source.replace(parent, parent.values[0]), @@ -1774,7 +1748,7 @@ export default class Caret { kind.allowsKind(Expression) && parent instanceof Expression && node instanceof Token && - parent.leaves().length === 1 + parent.hasOneLeaf() ) { const placeholder = ExpressionPlaceholder.make(); return [ @@ -1836,49 +1810,94 @@ export default class Caret { node: Node, validOnly: boolean, project: Project, - ): Revision | undefined { + ): Revision | LocaleTextAccessor { // If valid only, check to see if the node is in a list or represents an optional field. const parent = this.source.root.getParent(node); + // Keep track if there's an empty field that has a dependency. + let dependency: Node | Node[] | undefined = undefined; if (validOnly) { if (parent) { const field = parent.getFieldOfChild(node); if (field !== undefined) { const value = parent.getField(field.name); - // If the deletion isn't valid, then return the parent, in case it can be deleted. + let permittingKind: FieldKind | undefined = undefined; if ( - !( - (field.kind instanceof ListOf && + field.kind instanceof ListOf && + ((Array.isArray(value) && value.length > 1) || + field.kind.allowsEmpty) + ) { + permittingKind = field.kind; + } else { + // If the deletion isn't a list with more than one element or a list that allows empty, or an emptyable field, ignore the deletion. + const kinds = field.kind?.enumerateFieldKinds() ?? []; + for (const kind of kinds) { + if ( + kind instanceof ListOf && ((Array.isArray(value) && value.length > 1) || - field.kind.allowsEmpty)) || - field.kind instanceof Empty - ) - ) - return [this.source, this.withPosition(parent)]; + kind.allowsEmpty) + ) { + permittingKind = kind; + break; + } + if (kind instanceof Empty) { + permittingKind = kind; + if (kind.dependency) + dependency = parent.getField( + kind.dependency.name, + ); + break; + } + } + } + if (permittingKind === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; } - } else return undefined; + } else return (l) => l.ui.source.cursor.ignored.noDelete; } - const range = this.getRange(node); - if (range === undefined) return; - const newSource = this.source.withoutGraphemesBetween( - range[0], - range[1], + let nodesToDelete = [ + node, + ...(Array.isArray(dependency) + ? dependency + : dependency + ? [dependency] + : []), + ]; + let newSource: Source | undefined = this.source; + let firstPosition = Math.min( + ...nodesToDelete + .map((n) => this.source.getNodeFirstPosition(n) ?? undefined) + .filter((p): p is number => p !== undefined), ); - if (newSource === undefined) return undefined; + for (const nodeToRemove of nodesToDelete) { + const range = newSource.getRange(nodeToRemove); + if (range === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; + newSource = newSource.withoutGraphemesBetween(range[0], range[1]); + if (newSource === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; + } + + if (firstPosition === undefined) + return (l) => l.ui.source.cursor.ignored.noDelete; // If only valid, ensure the edit is valid. if ( validOnly && - project.withSource(this.source, newSource).getMajorConflictsNow() - .length > project.getMajorConflictsNow().length + project.getNewConflicts(this.source, newSource, NegligibleConflicts) + .length > 0 ) - return parent - ? [this.source, this.withPosition(parent)] - : undefined; + return (l) => l.ui.source.cursor.ignored.noError; return [ newSource, - new Caret(newSource, range[0], undefined, undefined, undefined), + new Caret( + newSource, + firstPosition, + undefined, + undefined, + undefined, + ), ]; } @@ -1913,7 +1932,7 @@ export default class Caret { : undefined; } - wrap(key: string): Edit | undefined { + wrap(project: Project, key: string): Revision | undefined { let node = this.position instanceof Node ? this.position : undefined; if (node instanceof Token && !node.isSymbol(Sym.End)) node = this.source.root.getParent(node); @@ -1921,12 +1940,35 @@ export default class Caret { return undefined; let wrapper: Expression | undefined = undefined; let position: Expression | undefined; - if (key === EVAL_OPEN_SYMBOL) wrapper = Block.make([node]); - else if (key === LIST_OPEN_SYMBOL) wrapper = ListLiteral.make([node]); - else if (key === SET_OPEN_SYMBOL) wrapper = SetLiteral.make([node]); - else if (OperatorRegEx.test(key)) { - position = ExpressionPlaceholder.make(); - wrapper = new BinaryEvaluate(node, Reference.make(key), position); + + // Tokenize the insertion + const token = tokens(key)[0]; + if (token === undefined) return; + + // Wrap in a block + if (token.isSymbol(Sym.EvalOpen)) wrapper = Block.make([node]); + // Wrap in a list + else if (token.isSymbol(Sym.ListOpen)) + wrapper = ListLiteral.make([node]); + // Wrap in a set + else if (token.isSymbol(Sym.SetOpen)) wrapper = SetLiteral.make([node]); + // Wrap in a binary evlauate if an operator + else if (token.isSymbol(Sym.Operator)) { + const context = project.getNodeContext(node); + const type = node.getType(context); + const definition = type.getDefinitionOfNameInScope(key, context); + if ( + definition && + definition instanceof FunctionDefinition && + definition.inputs.length === 1 + ) { + position = ExpressionPlaceholder.make(); + wrapper = new BinaryEvaluate( + node, + Reference.make(key), + position, + ); + } } if (wrapper === undefined) return; diff --git a/src/edit/caret/Complete.ts b/src/edit/caret/Complete.ts new file mode 100644 index 000000000..3f5376479 --- /dev/null +++ b/src/edit/caret/Complete.ts @@ -0,0 +1,503 @@ +/** Functionality related to automatically completing a text insertion */ + +import type Project from '@db/projects/Project'; +import BinaryEvaluate from '@nodes/BinaryEvaluate'; +import Bind from '@nodes/Bind'; +import Block from '@nodes/Block'; +import Convert from '@nodes/Convert'; +import Evaluate from '@nodes/Evaluate'; +import Example from '@nodes/Example'; +import Expression from '@nodes/Expression'; +import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; +import FunctionType from '@nodes/FunctionType'; +import Is from '@nodes/Is'; +import ListAccess from '@nodes/ListAccess'; +import ListType from '@nodes/ListType'; +import Literal from '@nodes/Literal'; +import MapType from '@nodes/MapType'; +import Names from '@nodes/Names'; +import Node from '@nodes/Node'; +import NumberLiteral from '@nodes/NumberLiteral'; +import NumberType from '@nodes/NumberType'; +import Paragraph, { type Segment } from '@nodes/Paragraph'; +import Program from '@nodes/Program'; +import PropertyReference from '@nodes/PropertyReference'; +import Reference from '@nodes/Reference'; +import SetOrMapAccess from '@nodes/SetOrMapAccess'; +import SetType from '@nodes/SetType'; +import Source from '@nodes/Source'; +import StreamDefinitionType from '@nodes/StreamDefinitionType'; +import StructureDefinitionType from '@nodes/StructureDefinitionType'; +import StructureType from '@nodes/StructureType'; +import Sym from '@nodes/Sym'; +import TypePlaceholder from '@nodes/TypePlaceholder'; +import WebLink from '@nodes/WebLink'; +import Words from '@nodes/Words'; +import { + BIND_SYMBOL, + CODE_SYMBOL, + CONVERT_SYMBOL, + ELISION_SYMBOL, + EVAL_CLOSE_SYMBOL, + EVAL_OPEN_SYMBOL, + EXPONENT_SYMBOL, + PLACEHOLDER_SYMBOL, + PRODUCT_SYMBOL, + STREAM_SYMBOL, + TAG_OPEN_SYMBOL, + TYPE_SYMBOL, +} from '@parser/Symbols'; +import { + DelimiterCloseByOpen, + FormattingSymbols, + tokens, +} from '@parser/Tokenizer'; +import type Caret from './Caret'; + +type InsertInfo = { + /** The caret where the insertion is happening */ + caret: Caret; + /** The project of the source being inserted into */ + project: Project; + /** The source being inserted into */ + source: Source; + /** The position in the source's glyph sequence */ + position: number; + /** The text being inserted */ + text: string; + /** Whether to permit only syntactically and semantically valid edits */ + validOnly: boolean; +}; + +/** The text inserted, the revised source, the new caret position or node, and whether a symbol was "closed" by adding a single character. */ +type Revision = [Source, number | Node]; + +type Trigger = { + /** The symbol that triggers this autocomplete */ + symbol: string | string[] | ((text: string) => boolean); + /** The function that generates the revision for this autocomplete */ + revise: (info: InsertInfo) => Revision | undefined; +}; + +/** A list of autocompletions by symbol triggers, and the order in which to consider them. */ +const AutocompleteTriggers: Trigger[] = [ + { + symbol: EVAL_OPEN_SYMBOL, + revise: completeEvaluate, + }, + { symbol: CONVERT_SYMBOL, revise: completeConvert }, + { symbol: Object.keys(DelimiterCloseByOpen), revise: completeDelimiter }, + { symbol: '.', revise: completeStream }, + { + symbol: (text) => tokens(text)[0]?.isSymbol(Sym.Operator), + revise: completeBinaryEvaluate, + }, + { symbol: TYPE_SYMBOL, revise: completeIs }, + { symbol: BIND_SYMBOL, revise: completeBindOrKeyValue }, + { symbol: TAG_OPEN_SYMBOL, revise: completeLink }, + { symbol: CODE_SYMBOL, revise: completeExample }, +]; + +/** Given some text to insert, get a revision based on any eligible autocompletions. */ +export function completeInsertion( + /** The project the source is in */ + project: Project, + /** The caret where the insertion is happening */ + caret: Caret, + /** The text being inserted */ + text: string, + /** Whether to permit only syntactically and semantically valid edits */ + validOnly: boolean, +): Revision | undefined { + const source = caret.source; + const position = caret.position; + + if (typeof position !== 'number') return undefined; + + // Iterate through the autocomplete triggers to see if any apply. + for (const trigger of AutocompleteTriggers) { + if ( + Array.isArray(trigger.symbol) + ? trigger.symbol.includes(text) + : typeof trigger.symbol === 'function' + ? trigger.symbol(text) + : text === trigger.symbol + ) { + const result = trigger.revise({ + text, + caret, + project, + source, + position, + validOnly, + }); + if (result !== undefined) { + return result; + } + } + } +} + +function getPrecedingExpression( + source: Source, + position: number, + exact: boolean, +): Expression[] { + return source.nodes().filter((node): node is Expression => { + const start = source.getNodeLastPosition(node); + if (start === undefined) return false; + return ( + node instanceof Expression && + !(node instanceof Program) && + !(node instanceof Source) && + !(node instanceof Block && node.isRoot()) && + !(node instanceof Bind) && + (start === position || (!exact && start + 1 === position)) + ); + }); +} + +function getPrecedingMarkup(source: Source, position: number): Words[] { + return source + .nodes() + .filter( + (node): node is Words => + node instanceof Words && + source.getNodeLastPosition(node) === position, + ); +} + +function completeEvaluate({ + project, + source, + position, +}: InsertInfo): Revision | undefined { + // If the inserted character is an open parenthesis, see if we can construct an evaluate with the preceding expression. + // Find the top most expression that ends where the caret is. + const precedingExpressions = getPrecedingExpression( + source, + position, + true, + ).filter( + (node) => + node instanceof Reference || + node instanceof PropertyReference || + (node instanceof Block && !node.isRoot()), + ); + + if (precedingExpressions.length === 0) return undefined; + + const propertyReference = precedingExpressions.find( + (node): node is PropertyReference => node instanceof PropertyReference, + ); + const precedingExpression = propertyReference ?? precedingExpressions[0]; + + const context = project.getNodeContext(precedingExpression); + const fun = precedingExpression.getType(context); + if ( + fun instanceof FunctionType || + fun instanceof StructureType || + fun instanceof StructureDefinitionType || + fun instanceof StreamDefinitionType + ) { + const definition = + fun instanceof FunctionType + ? fun.definition + : fun instanceof StructureType + ? fun.definition + : fun instanceof StructureDefinitionType + ? fun.type.definition + : fun instanceof StreamDefinitionType + ? fun.definition + : undefined; + const evaluate = definition + ? definition.getEvaluateTemplate( + context.getBasis().locales, + context, + false, + precedingExpression, + ) + : Evaluate.make(precedingExpression, []); + // Make a new source + const newSource = source.replace(precedingExpression, evaluate); + if (newSource === source) return undefined; + const firstPlaceholder = evaluate.nodes( + (n) => n instanceof ExpressionPlaceholder, + )[0]; + // Place the caret on the first placeholder, or before the close. + const newPosition = + firstPlaceholder !== undefined + ? firstPlaceholder + : ((evaluate instanceof Evaluate + ? evaluate.close + ? newSource.getNodeFirstPosition(evaluate.close) + : newSource.getNodeLastPosition(evaluate) + : position) ?? position); + + return [newSource, newPosition]; + } + return undefined; +} + +function completeConvert({ + source, + position, +}: InsertInfo): Revision | undefined { + // What's the preceding expression? + const precedingExpression = getPrecedingExpression( + source, + position, + false, + )[0]; + if (precedingExpression === undefined) return undefined; + + // Replace the preceding expression with a conversion of it. + const placeholder = TypePlaceholder.make(); + const newSource = source.replace( + precedingExpression, + Convert.make(precedingExpression, placeholder), + ); + if (newSource !== source) return [newSource, placeholder]; +} + +function completeDelimiter({ + caret, + text, + source, + project, + position, + validOnly, +}: InsertInfo): Revision | undefined { + // If the inserted string matches a single matched delimiter, complete it, unless: + // 1) we’re immediately before an matched closing delimiter, in which case we insert nothing, but move the caret forward + // 2) the character being inserted closes an unmatched delimiter, in which case we just insert the character. + if ( + ((!caret.isInsideWords() && + (!FormattingSymbols.includes(text) || + // Allow the elision symbol, since it can be completed outside of words. + text === ELISION_SYMBOL)) || + (caret.isInsideWords() && FormattingSymbols.includes(text))) && + (caret.tokenPrior === undefined || + // The text typed does not close an unmatched delimiter + (caret.source.getUnmatchedDelimiter(caret.tokenPrior, text) === + undefined && + !( + // The token prior is text or unknown + ( + caret.tokenPrior.isSymbol(Sym.Text) || + caret.tokenPrior.isSymbol(Sym.Unknown) + ) + ))) + ) { + let newPosition: Node | number = position; + let newSource = source; + + const preceding = getPrecedingExpression(source, position, false).map( + (node) => ({ + expression: node, + type: node.getType(project.getNodeContext(node)), + }), + ); + const precedingList = preceding.filter( + (preceding) => preceding.type instanceof ListType, + )[0]?.expression; + const precedingSet = preceding.filter( + (preceding) => + preceding.type instanceof SetType || + preceding.type instanceof MapType, + )[0]?.expression; + + // Insert an empty block in valid only mode and place the caret at the placeholder. + if (validOnly && text === EVAL_OPEN_SYMBOL) { + text += PLACEHOLDER_SYMBOL + EVAL_CLOSE_SYMBOL; + const newSource = source.withGraphemesAt(text, position); + const placeholder = newSource + ?.nodes() + .find( + (n) => newSource?.getNodeFirstPosition(n) === position + 1, + ); + newPosition = placeholder ?? position + text.length; + if (newSource) return [newSource, newPosition]; + } + // Is the preceding expression a list? Complete a list close + else if (precedingList) { + const placeholder = ExpressionPlaceholder.make(NumberType.make()); + const newSource = source.replace( + precedingList, + ListAccess.make(precedingList, placeholder), + ); + if (newSource) return [newSource, placeholder]; + } else if (precedingSet) { + const placeholder = ExpressionPlaceholder.make(); + const newSource = source.replace( + precedingSet, + SetOrMapAccess.make(precedingSet, placeholder), + ); + if (newSource) return [newSource, placeholder]; + } else { + text += DelimiterCloseByOpen[text]; + newSource = source.withGraphemesAt(text, position) ?? newSource; + if (newSource) return [newSource, position + 1]; + } + } + return undefined; +} + +function completeStream({ + source, + position, +}: InsertInfo): Revision | undefined { + // If the two preceding characters are dots and this is a dot, delete the last two dots then insert the stream symbol. + if ( + source.getGraphemeAt(position - 1) === '.' && + source.getGraphemeAt(position - 2) === '.' + ) { + const newSource = source + .withoutGraphemeAt(position - 2) + ?.withoutGraphemeAt(position - 2) + ?.withGraphemesAt(STREAM_SYMBOL, position - 2); + if (newSource) return [newSource, position - 1]; + } + return undefined; +} + +/** + * If the inserted character is an operator, see if we can construct a binary evaluation with the + * preceding expression and a placeholder on the right. + */ +function completeBinaryEvaluate({ + text, + source, + position, + project, +}: InsertInfo): Revision | undefined { + // Find the top most expression that ends where the caret is. + const precedingExpression = getPrecedingExpression( + source, + position, + false, + ).filter( + (node): node is Expression => + node instanceof Expression && + !(node instanceof Program) && + !(node instanceof Source) && + !(node instanceof Block && node.isRoot()), + )[0]; + + if ( + precedingExpression instanceof NumberLiteral && + !precedingExpression.unit?.isEmpty() && + (text === PRODUCT_SYMBOL || text === EXPONENT_SYMBOL) + ) + return undefined; + + if ( + precedingExpression && + precedingExpression + .getType(project.getNodeContext(precedingExpression)) + .getDefinitionOfNameInScope( + text, + project.getNodeContext(precedingExpression), + ) !== undefined + ) { + const binary = new BinaryEvaluate( + precedingExpression instanceof Literal || + precedingExpression instanceof Reference + ? precedingExpression + : Block.make([precedingExpression]), + new Reference(tokens(text)[0]), + ExpressionPlaceholder.make(), + ); + // Make a new source + const newSource = source.replace(precedingExpression, binary); + // Place the caret on the placeholder + const newPosition = binary.right; + if (newSource) return [newSource, newPosition]; + } +} + +/** Complete an is type check on the preceding expression */ +function completeIs({ source, position }: InsertInfo): Revision | undefined { + // Find the top most expression that ends where the caret is. + const precedingExpression = getPrecedingExpression( + source, + position, + true, + )[0]; + if (precedingExpression === undefined) return undefined; + // Expression placeholders use •Type to type themselves. + const isExpressionPlaceholder = + precedingExpression instanceof ExpressionPlaceholder; + + const placeholder = TypePlaceholder.make(); + // Make a new source + const newSource = source.replace( + precedingExpression, + isExpressionPlaceholder + ? precedingExpression.withType(placeholder) + : Is.make(precedingExpression, placeholder), + ); + // Place the caret on the placeholder + if (newSource !== source) return [newSource, placeholder]; +} + +/** On a :, complete a Bind or KeyValue */ +function completeBindOrKeyValue({ + source, + position, +}: InsertInfo): Revision | undefined { + const preceding = getPrecedingExpression(source, position, true).filter( + (node) => node instanceof Reference || node instanceof Is, + )[0]; + if (preceding === undefined) return undefined; + const reference = preceding.nodes((node) => node instanceof Reference)[0]; + if (reference === undefined) return undefined; + + const placeholder = ExpressionPlaceholder.make( + preceding instanceof Is ? preceding.type : undefined, + ); + const bind = Bind.make( + undefined, + Names.make([reference.name.getText()]), + preceding instanceof Is ? preceding.type.clone() : undefined, + placeholder, + ); + // Make a new source + const newSource = source.replace(preceding, bind); + // Place the caret on the placeholder + if (newSource !== source) return [newSource, placeholder]; +} + +/** Complete a web link inside a paragraph */ +function completeLink(info: InsertInfo): Revision | undefined { + return completeMarkup(info, WebLink.make('', 'https://')); +} + +/** Complete a example program inside a paragraph */ +function completeExample(info: InsertInfo): Revision | undefined { + return completeMarkup(info, Example.make(Program.make([]))); +} + +function completeMarkup( + { source, position }: InsertInfo, + segment: Segment, +): Revision | undefined { + const precedingMarkup = getPrecedingMarkup(source, position); + const content = precedingMarkup[0]; + const parent = source.root.getParent(content); + + if (!(parent instanceof Words || parent instanceof Paragraph)) + return undefined; + + const index = parent.segments.indexOf(content); + if (index < 0) return undefined; + + const newSource = source.replace( + parent, + parent.withSegmentInsertedAt(index + 1, segment), + ); + + if (newSource !== source) return [newSource, position + 1]; + + return undefined; +} diff --git a/src/edit/Drag.test.ts b/src/edit/drag/Drag.test.ts similarity index 64% rename from src/edit/Drag.test.ts rename to src/edit/drag/Drag.test.ts index 13ef1b953..578ac0b0b 100644 --- a/src/edit/Drag.test.ts +++ b/src/edit/drag/Drag.test.ts @@ -1,15 +1,16 @@ import Project from '@db/projects/Project'; +import DefaultLocale from '@locale/DefaultLocale'; +import Evaluate from '@nodes/Evaluate'; import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; import ListLiteral from '@nodes/ListLiteral'; import type Node from '@nodes/Node'; import NumberLiteral from '@nodes/NumberLiteral'; import Source from '@nodes/Source'; import Token from '@nodes/Token'; +import parseExpression from '@parser/parseExpression'; +import { toTokens } from '@parser/toTokens'; import { expect, test } from 'vitest'; -import DefaultLocale from '../locale/DefaultLocale'; -import parseExpression from '../parser/parseExpression'; -import { toTokens } from '../parser/toTokens'; -import { dropNodeOnSource, InsertionPoint } from './Drag'; +import { dropNodeOnSource, InsertionPoint, isValidDropTarget } from './Drag'; test.each([ // Replace placeholder with rootless expression @@ -52,7 +53,7 @@ test.each([ node, 'values', node.values, - node.find(Token, 2), + node.find(Token, 2), 0, 1, ); @@ -69,7 +70,7 @@ test.each([ node, 'values', node.values, - node.find(Token, 2), + node.find(Token, 2), 0, 1, ); @@ -86,7 +87,7 @@ test.each([ node, 'values', node.values, - node.find(Token, 2), + node.find(Token, 2), 0, 1, ); @@ -94,6 +95,37 @@ test.each([ '[1 2 3 4 5]', '', ], + // Drop reaction with placeholders onto a typed bind. + [ + ['a•#: _'], + () => parseExpression(toTokens('_ … _•? … _')), + (sources) => sources[0].find(ExpressionPlaceholder), + 'a•#: _ … _•? … _', + ], + // Drop list onto typed list + [ + ['a•[#]: _'], + () => parseExpression(toTokens('[]')), + (sources) => sources[0].find(ExpressionPlaceholder), + 'a•[#]: []', + ], + // Insert number into unit-typed number list, despite type error. + [ + ['Place()'], + () => parseExpression(toTokens('1')), + (sources) => { + const node = sources[0].find(Evaluate); + return new InsertionPoint( + node, + 'inputs', + node.inputs, + node.find(Token, 2), + 0, + 0, + ); + }, + 'Place(1)', + ], ])( 'Drop on %s should yield %s', ( @@ -111,13 +143,31 @@ test.each([ sources.slice(1), DefaultLocale, ); + + const draggedNode: Node = dragged(sources); + const targetNode: Node | InsertionPoint = target(sources); + + // Assert that the drop target is valid. + expect( + isValidDropTarget( + project, + draggedNode, + targetNode instanceof InsertionPoint + ? targetNode.node + : targetNode, + targetNode instanceof InsertionPoint ? targetNode : undefined, + true, + ), + ).toBe(true); + const [newProject] = dropNodeOnSource( project, sources[0], - dragged(sources), - target(sources), + draggedNode, + targetNode, ) ?? [undefined, undefined]; + // Assert that the new project matches expectations. expect(newProject).toBeDefined(); expect(newProject?.getMain().toWordplay()).toBe(mainExpected); if (supplementExpected) diff --git a/src/edit/Drag.ts b/src/edit/drag/Drag.ts similarity index 75% rename from src/edit/Drag.ts rename to src/edit/drag/Drag.ts index ed0831be5..13964bb69 100644 --- a/src/edit/Drag.ts +++ b/src/edit/drag/Drag.ts @@ -10,7 +10,6 @@ import Token from '@nodes/Token'; import Type from '@nodes/Type'; import TypePlaceholder from '@nodes/TypePlaceholder'; import getPreferredSpaces from '@parser/getPreferredSpaces'; -import Bind from '../nodes/Bind'; /** * Represents a node, list on the node, and index in the list at which to insert a node. @@ -19,14 +18,14 @@ import Bind from '../nodes/Bind'; export class InsertionPoint { /** The node before the insertion point. */ readonly node: Node; - /** The token before the insertion point. */ - readonly token: Token; + /** The optional token before the insertion point, if in text mode. */ + readonly token: Token | undefined; /** The field the insertion point corresponds to. */ readonly field: string; /** The list being inserted into */ readonly list: Node[]; /** The local line index in the space prior the node, from 0 to n */ - readonly line: number; + readonly line: number | undefined; /** The index into the list being inserted into. */ readonly index: number; @@ -34,8 +33,8 @@ export class InsertionPoint { node: Node, field: string, list: Node[], - token: Token, - line: number, + token: Token | undefined, + line: number | undefined, index: number, ) { this.node = node; @@ -57,6 +56,17 @@ export class InsertionPoint { } } +/** Represents a field that could be assigned. */ +export class AssignmentPoint { + readonly parent: Node; + readonly field: string; + + constructor(parent: Node, field: string) { + this.parent = parent; + this.field = field; + } +} + /** * Given a project, a source in that project, a node being dragged, and either a node hovered over or an insertion point, * drop the node hover the hovered node or at the insertion point, returning a revised project and a reference to the @@ -66,7 +76,7 @@ export function dropNodeOnSource( project: Project, source: Source, dragged: Node, - target: Node | InsertionPoint, + target: Node | InsertionPoint | AssignmentPoint, ): [Project, Source, Node] { const root = project.getRoot(dragged); const draggedRoot = root?.root; @@ -112,7 +122,7 @@ export function dropNodeOnSource( const sourceReplacements: [Source, Source][] = []; // This should be the node to pretty print after dropping, to ensure semantic spacing is intact. - let nodeToFormat: Node; + let nodeToFormat: Node = draggedClone; // Case 1: We're replacing the hovered node with the dragged node. if (target instanceof Node) { @@ -126,7 +136,7 @@ export function dropNodeOnSource( nodeToFormat = draggedClone; } // Case 2: We're inserting into a list - else { + else if (target instanceof InsertionPoint) { const insertion = target; // Replace the old list with a new one that has the insertion. editedProgram = editedProgram.replace(insertion.list, [ @@ -162,7 +172,7 @@ export function dropNodeOnSource( let count = 0; for (; index < space.length; index++) { if (space.charAt(index) === '\n') count++; - if (count > insertion.line) break; + if (insertion.line !== undefined && count > insertion.line) break; } // Split it based on the line number in the preceding space. @@ -176,6 +186,19 @@ export function dropNodeOnSource( editedSpace = editedSpace.withSpace(nodeAtIndex, afterSpace); } } + // Case 3: We're assigning to an unassigned field. + else if (target instanceof AssignmentPoint) { + // Set the field to the dragged clone. + const revisedParent = target.parent.replace(target.field, draggedClone); + // Update the edited program (or if the revised parent is a program, use that). + editedProgram = + editedProgram instanceof Program && revisedParent instanceof Program + ? revisedParent + : editedProgram.replace(target.parent, revisedParent); + + // Format the parent node + nodeToFormat = revisedParent; + } // If the dragged node came from a Source we have a replacement (undefined or a Node) // update the the source. We handle it differently based on whether it was this editors source or another editor's source. @@ -205,9 +228,9 @@ export function dropNodeOnSource( // Make a new source let newSource = source.withProgram(editedProgram, editedSpace); - newSource = newSource.withSpaces( - getPreferredSpaces(nodeToFormat, editedSpace), - ); + newSource = nodeToFormat + ? newSource.withSpaces(getPreferredSpaces(nodeToFormat, editedSpace)) + : newSource; // Finally, add this editor's updated source to the list of sources to replace in the project. sourceReplacements.push([source, newSource]); @@ -259,47 +282,57 @@ export function getInsertionPoint( ); } +/** + * Given a project, a dragged node, a target node, and a possible insertion point, determine whether the target is valid. + * Valid means that it is syntactically correct, but it may still result in a type error. We permit type errors to allow + * for more flexible editing, and to help learners reason through what the type should be. + */ export function isValidDropTarget( project: Project, - dragged: Node | undefined, - target: Node | undefined, - insertion: InsertionPoint | undefined, + dragged: Node, + target: Node, + insertion: InsertionPoint | AssignmentPoint | undefined, + permissiveTypes: boolean, ): boolean { - if (dragged === undefined) return false; - - // Allow expressions to be dropped on expressions. - // Find the field the hovered node corresponds to. - if (target) { - const field = project - .getRoot(target) - ?.getParent(target) - ?.getFieldOfChild(target); - - // If we found a field and the dragged node is an instanceof one of the allowed types, it's a valid drop target. - if ( - field && - field.kind.allowsKind(dragged.constructor) && - // Don't allow drops on nodes that are children of the dragged node. - !dragged.contains(target) - ) - return true; - } - - // Allow binds to be dropped on children of blocks. - if (target && dragged instanceof Bind) { - const hoverParent = project.getRoot(target)?.getParent(target); - if ( - hoverParent instanceof Block && - hoverParent.statements.includes(target as Expression) - ) - return true; - } - - // Allow types to be dropped on types. - if (dragged instanceof Type && target instanceof Type) return true; + // Is the target inside the dragged node? If so, we can't drop it there. + if (dragged.contains(target)) return false; + + // What field is the target currently set on? + const field = project + .getRoot(target) + ?.getParent(target) + ?.getFieldOfChild(target); + + // No field? That's weird. Bail. + if (field === undefined) return false; + + // Field doesn't allow the dragged node? Not a valid target. + if (!field.kind.allowsKind(dragged.constructor)) return false; + + // What's the type context of the target? + const source = project.getSourceOf(target); + if (source === undefined) return false; + const context = project.getContext(source); + + // If the field permits the dragged node's kind, and either isn't typed or the dragged node's type is accepted by the field's type, allow the drop. + if ( + permissiveTypes || + field.getType === undefined || + !(dragged instanceof Expression) || + field + .getType( + context, + insertion instanceof InsertionPoint + ? insertion.index + : undefined, + ) + .accepts(dragged.getType(context), context) + ) + return true; // Allow inserts to be inserted. if (insertion) return true; + // Otherwise, not a valid match. return false; } diff --git a/src/components/editor/util/Menu.ts b/src/edit/menu/Menu.ts similarity index 71% rename from src/components/editor/util/Menu.ts rename to src/edit/menu/Menu.ts index 7858014cb..dcd600803 100644 --- a/src/components/editor/util/Menu.ts +++ b/src/edit/menu/Menu.ts @@ -1,9 +1,14 @@ import type ConceptIndex from '@concepts/ConceptIndex'; -import type Purpose from '@concepts/Purpose'; +import Purpose from '@concepts/Purpose'; +import type Project from '@db/projects/Project'; +import { type CaretPosition } from '@edit/caret/Caret'; import type Locales from '@locale/Locales'; -import type Caret from '../../../edit/Caret'; -import Revision from '../../../edit/Revision'; -import type { Edit } from './Commands'; +import type { FieldPosition } from '@nodes/Node'; +import Reference from '@nodes/Reference'; +import type Source from '@nodes/Source'; +import Token from '@nodes/Token'; +import type { Edit } from '../../components/editor/commands/Commands'; +import Revision from '../revision/Revision'; /** The first number is the selected revision or revision set, the second number is the optional revision in a selected revision set. */ export type MenuSelection = [number, number | undefined]; @@ -12,24 +17,34 @@ export type MenuOrganization = (Revision | RevisionSet)[]; // A relevance ordering of purposes. const PurposeRelevance: Record = { - project: 0, - value: 1, - how: 1, - input: 2, - bind: 3, - evaluate: 4, - output: 5, - decide: 6, - convert: 7, - type: 8, - document: 9, - source: 10, + Project: 0, + Outputs: 1, + Inputs: 2, + Decisions: 3, + Text: 4, + Numbers: 5, + Truth: 6, + Definitions: 7, + Lists: 8, + Maps: 9, + Tables: 10, + Documentation: 13, + Types: 14, + Advanced: 15, + Hidden: 16, + How: 16, }; /** An immutable container for menu state. */ export default class Menu { + /** The project this menu was generated for */ + private readonly project: Project; + + /** The source in which the menu was requested */ + private readonly source: Source; + /** The caret at which the menu was generated. */ - private readonly caret: Caret; + private readonly anchor: CaretPosition | FieldPosition; /** The transforms generated from the caret */ private readonly revisions: Revision[]; @@ -57,57 +72,75 @@ export default class Menu { private readonly organization: MenuOrganization; constructor( - caret: Caret, + project: Project, + source: Source, + anchor: CaretPosition | FieldPosition, revisions: Revision[], organization: MenuOrganization | undefined, concepts: ConceptIndex, selection: [number, number | undefined], action: (selection: Edit | RevisionSet | undefined) => boolean, ) { - this.caret = caret; + this.project = project; + this.source = source; + this.anchor = anchor; this.revisions = revisions; this.concepts = concepts; this.selection = selection; this.action = action; if (organization === undefined) { + const visibleRevisions = this.revisions.filter( + (revision) => + revision.getPurpose(this.concepts) !== Purpose.Hidden || + revision.getNewNode(this.concepts.locales) instanceof Token, + ); + // The organization is divided into the following groups and order: // 1. Anything involving a reference (e.g., revisions that insert a Refer) // 2. RevisionSets organized by node kind, or the single Revision if there's only one, sorted by Purpose. // 3. Any removals, which are likely the least relevant. // RevisionSets are organized alphabetically by locale. - const priority = this.revisions.filter((revision) => - revision.isCompletion(this.concepts.locales), - ); - const removals = this.revisions.filter((revision) => + const priority = visibleRevisions.filter((revision) => { + if (revision.isCompletion(this.concepts.locales)) return true; + const newNode = revision.getNewNode(this.concepts.locales); + return newNode instanceof Reference; + }); + const removals = visibleRevisions.filter((revision) => revision.isRemoval(), ); - const others = this.revisions.filter( + const others = visibleRevisions.filter( (revision) => !priority.includes(revision) && !revision.isRemoval(), ); + + // Organize by purpose. const kinds: Map = new Map(); for (const other of others) { - const node = other.getNewNode(this.concepts.locales); - if (node) { - const purpose = node.getPurpose(); + const purpose = other.getPurpose(this.concepts); + if (purpose !== undefined) { const revisions = kinds.get(purpose); if (revisions) kinds.set(purpose, [...revisions, other]); else kinds.set(purpose, [other]); } } + // Make an sorted array of the revision sets. + const grouped = Array.from(kinds.entries()) + .toSorted( + (a, b) => PurposeRelevance[a[0]] - PurposeRelevance[b[0]], + ) + .map( + ([purpose, revisions]) => + new RevisionSet(purpose, revisions), + ); + organization = [ ...priority, - ...Array.from(kinds.entries()) - .sort( - (a, b) => - PurposeRelevance[a[0]] - PurposeRelevance[b[0]], - ) - .map( - ([purpose, revisions]) => - new RevisionSet(purpose, revisions), - ), + ...// We only do this grouping if there are more than 7 other revisions and more than 1 group + (others.length > 7 && kinds.size > 1 + ? grouped + : grouped.flatMap((set) => set.revisions)), ...removals, ]; } @@ -115,12 +148,26 @@ export default class Menu { this.organization = organization; } + getProject() { + return this.project; + } + + getSource(): Source { + return this.source; + } + + getAnchor(): CaretPosition | FieldPosition { + return this.anchor; + } + withSelection(selection: MenuSelection) { const [index, subindex] = selection; const submenu = this.organization[index]; return new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, @@ -134,10 +181,6 @@ export default class Menu { ); } - getCaret(): Caret { - return this.caret; - } - getOrganization() { return this.organization; } @@ -222,7 +265,9 @@ export default class Menu { const newIndex = index + direction; return newIndex >= 0 && newIndex < this.organization.length ? new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, @@ -234,7 +279,9 @@ export default class Menu { const newSubindex = subindex + direction; return newSubindex >= -1 && newSubindex < submenu.size() ? new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, @@ -249,7 +296,9 @@ export default class Menu { out() { return this.selection[1] !== undefined ? new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, @@ -264,7 +313,9 @@ export default class Menu { return this.getSelection() instanceof RevisionSet && this.selection[1] === undefined ? new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, @@ -277,7 +328,9 @@ export default class Menu { back() { return this.selection[1] !== undefined ? new Menu( - this.caret, + this.project, + this.source, + this.anchor, this.revisions, this.organization, this.concepts, diff --git a/src/edit/Autocomplete.test.ts b/src/edit/menu/PossibleEdits.test.ts similarity index 54% rename from src/edit/Autocomplete.test.ts rename to src/edit/menu/PossibleEdits.test.ts index a0dc2ddb0..94bcf5867 100644 --- a/src/edit/Autocomplete.test.ts +++ b/src/edit/menu/PossibleEdits.test.ts @@ -1,29 +1,40 @@ import Project from '@db/projects/Project'; +import Caret from '@edit/caret/Caret'; import DefaultLocales from '@locale/DefaultLocales'; +import BooleanLiteral from '@nodes/BooleanLiteral'; +import Evaluate from '@nodes/Evaluate'; +import ExpressionPlaceholder from '@nodes/ExpressionPlaceholder'; import type Node from '@nodes/Node'; import Source from '@nodes/Source'; +import StreamType from '@nodes/StreamType'; +import Unit from '@nodes/Unit'; import getPreferredSpaces from '@parser/getPreferredSpaces'; +import { TRUE_SYMBOL } from '@parser/Symbols'; import { expect, test } from 'vitest'; -import DefaultLocale from '../locale/DefaultLocale'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Reference from '../nodes/Reference'; -import Append from './Append'; -import Assign from './Assign'; -import { getEditsAt } from './Autocomplete'; -import Caret from './Caret'; -import Replace from './Replace'; -import type Revision from './Revision'; +import DefaultLocale from '../../locale/DefaultLocale'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Append from '../revision/Append'; +import Assign from '../revision/Assign'; +import Replace from '../revision/Replace'; +import type Revision from '../revision/Revision'; +import { getEditsAt } from './PossibleEdits'; test.each([ - ['blank program suggestions', '**', undefined, Append, '0'], + ['blank programs suggest numbers', '**', undefined, Append, '0'], + ['blank programs suggest booleans', '**', undefined, Append, TRUE_SYMBOL], + ['blank programs suggest text', '**', undefined, Append, "''"], + ['blank programs suggest lists', '**', undefined, Append, '[]'], + ['blank programs suggest sets', '**', undefined, Append, '{}'], + ['blank programs suggest maps', '**', undefined, Append, '[]'], + ['blank programs suggest tables', '**', undefined, Append, '[]'], ['set unset bind value', 'a:**', undefined, Assign, '0'], ['suggest binary evaluate completions', '1 + **', undefined, Assign, '1'], [ 'suggest conditional on boolean value', - 'b: ⊥\nb', - (node: Node) => node instanceof Reference, + 'b: ⊥', + (node: Node) => node instanceof BooleanLiteral, Replace, - 'b ? _ _', + '⊥ ? ⊤ ⊥', ], ['suggest phrase on empty program', '**', undefined, Append, "💬('')"], [ @@ -49,47 +60,86 @@ test.each([ ], [ 'suggest evaluate wrap', - `ƒ sum(a•? b•?) a & b\nsum()**`, - undefined, + `ƒ sum(a•? b•?) a & b\nsum()`, + (node) => node instanceof Evaluate, Replace, '(sum())', ], + ['suggest basis function eval', `"hi".**`, undefined, Replace, '"hi".📏()'], [ - 'suggest structure function eval', - `"hi".**`, - undefined, + 'suggest binary evaluate', + `1**`, + (node) => node instanceof NumberLiteral, Replace, - '"hi".📏()', + '1 ÷ _•#', ], - ['suggest structure property', `"hi".**`, undefined, Replace, '"hi" = _'], - ['suggest binary evaluate', `1**`, undefined, Replace, '1 + _'], [ - 'suggest property reference', + 'complete property reference', `•Cat(hat•"")\nboomy: Cat("none")\nboomy.**`, undefined, Replace, 'boomy.hat', ], - // Selecting 2 should offer to replace with c [ - 'suggest bind reference', + 'suggest reference to replace node', `c: 1\n1 + 2`, (node: Node) => node instanceof NumberLiteral && node.toWordplay() === '2', Replace, 'c', ], - // Lists should suggest bindings in scope [ - 'suggest insertion of binding in scope', + 'suggest insertion of in scope bind in list', `a:'hello'\n[ "hi" **]`, undefined, Append, 'a', ], + [ + 'suggest insertion of in scope bind in block', + 'a: 1\n**', + undefined, + Append, + 'a', + ], ['suggest unit', '1**', undefined, Assign, 'ms'], - ['suggest additional denominator', '1m**', undefined, Replace, 'm·min'], - ['suggest denominator', '1m**', undefined, Replace, 'm/s'], + [ + 'suggest additional denominator', + '1m**', + (node) => node instanceof Unit, + Replace, + 'm·min', + ], + [ + 'suggest denominator', + '1m**', + (node) => node instanceof Unit, + Replace, + 'm/s', + ], + [ + 'suggest streams on stream placeholders', + '∆ _•…_', + (node) => + node instanceof ExpressionPlaceholder && + node.type instanceof StreamType, + Replace, + '🖱️()', + ], + [ + 'suggest negation on number expressions', + '5', + (node) => node instanceof NumberLiteral, + Replace, + '-5', + ], + [ + 'suggest addition on number with unit', + '5m', + (node) => node instanceof NumberLiteral, + Replace, + '5m + _•#m', + ], ])( '%s: %s', ( @@ -121,7 +171,12 @@ test.each([ undefined, undefined, ); - const transforms = getEditsAt(project, caret, DefaultLocales); + const transforms = getEditsAt( + project, + caret, + undefined, + DefaultLocales, + ); const match = transforms.find((transform) => { const newNode = transform.getNewNode(DefaultLocales); @@ -146,7 +201,10 @@ test.each([ const newNode = match?.getNewNode(DefaultLocales); - expect(newNode?.toWordplay(getPreferredSpaces(newNode))).toBe(edit); + expect( + newNode?.toWordplay(getPreferredSpaces(newNode)), + description, + ).toBe(edit); } }, ); diff --git a/src/edit/Autocomplete.ts b/src/edit/menu/PossibleEdits.ts similarity index 53% rename from src/edit/Autocomplete.ts rename to src/edit/menu/PossibleEdits.ts index 1831e993e..b832ef10e 100644 --- a/src/edit/Autocomplete.ts +++ b/src/edit/menu/PossibleEdits.ts @@ -1,85 +1,95 @@ -import Append from '@edit/Append'; -import Assign from '@edit/Assign'; -import Refer from '@edit/Refer'; -import Replace from '@edit/Replace'; -import type Revision from '@edit/Revision'; +import type Caret from '@edit/caret/Caret'; +import Append from '@edit/revision/Append'; +import Assign from '@edit/revision/Assign'; +import Refer from '@edit/revision/Refer'; +import Replace from '@edit/revision/Replace'; +import type Revision from '@edit/revision/Revision'; import BooleanLiteral from '@nodes/BooleanLiteral'; import type Context from '@nodes/Context'; +import ConversionType from '@nodes/ConversionType'; +import DocumentedExpression from '@nodes/DocumentedExpression'; import FormattedLiteral from '@nodes/FormattedLiteral'; +import FormattedTranslation from '@nodes/FormattedTranslation'; import Input from '@nodes/Input'; import Match from '@nodes/Match'; -import Node, { Empty, ListOf, type Field, type NodeKind } from '@nodes/Node'; +import Name from '@nodes/Name'; +import NameType from '@nodes/NameType'; +import Node, { + Empty, + ListOf, + type Field, + type FieldPosition, + type NodeKind, +} from '@nodes/Node'; import Otherwise from '@nodes/Otherwise'; import SetOrMapAccess from '@nodes/SetOrMapAccess'; import Spread from '@nodes/Spread'; import TableType from '@nodes/TableType'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import BinaryEvaluate from '../nodes/BinaryEvaluate'; -import Bind from '../nodes/Bind'; -import Block from '../nodes/Block'; -import BooleanType from '../nodes/BooleanType'; -import Changed from '../nodes/Changed'; -import Conditional from '../nodes/Conditional'; -import ConversionDefinition from '../nodes/ConversionDefinition'; -import Convert from '../nodes/Convert'; -import Delete from '../nodes/Delete'; -import Dimension from '../nodes/Dimension'; -import Doc from '../nodes/Doc'; -import Docs from '../nodes/Docs'; -import Evaluate from '../nodes/Evaluate'; -import Example from '../nodes/Example'; -import Expression from '../nodes/Expression'; -import ExpressionPlaceholder from '../nodes/ExpressionPlaceholder'; -import FunctionDefinition from '../nodes/FunctionDefinition'; -import FunctionType from '../nodes/FunctionType'; -import Initial from '../nodes/Initial'; -import Insert from '../nodes/Insert'; -import Is from '../nodes/Is'; -import IsLocale from '../nodes/IsLocale'; -import KeyValue from '../nodes/KeyValue'; -import Language from '../nodes/Language'; -import ListAccess from '../nodes/ListAccess'; -import ListLiteral from '../nodes/ListLiteral'; -import ListType from '../nodes/ListType'; -import MapLiteral from '../nodes/MapLiteral'; -import MapType from '../nodes/MapType'; -import Markup from '../nodes/Markup'; -import Mention from '../nodes/Mention'; -import NoneLiteral from '../nodes/NoneLiteral'; -import NoneType from '../nodes/NoneType'; -import NumberLiteral from '../nodes/NumberLiteral'; -import NumberType from '../nodes/NumberType'; -import Paragraph from '../nodes/Paragraph'; -import Previous from '../nodes/Previous'; -import Program from '../nodes/Program'; -import PropertyBind from '../nodes/PropertyBind'; -import PropertyReference from '../nodes/PropertyReference'; -import Reaction from '../nodes/Reaction'; -import Reference from '../nodes/Reference'; -import Select from '../nodes/Select'; -import SetLiteral from '../nodes/SetLiteral'; -import SetType from '../nodes/SetType'; -import StructureDefinition from '../nodes/StructureDefinition'; -import { WildcardSymbols } from '../nodes/Sym'; -import TableLiteral from '../nodes/TableLiteral'; -import TextLiteral from '../nodes/TextLiteral'; -import TextType from '../nodes/TextType'; -import This from '../nodes/This'; -import Token from '../nodes/Token'; -import type Type from '../nodes/Type'; -import TypeInputs from '../nodes/TypeInputs'; -import TypePlaceholder from '../nodes/TypePlaceholder'; -import TypeVariables from '../nodes/TypeVariables'; -import UnaryEvaluate from '../nodes/UnaryEvaluate'; -import UnionType from '../nodes/UnionType'; -import Unit from '../nodes/Unit'; -import UnknownType from '../nodes/UnknownType'; -import UnparsableExpression from '../nodes/UnparsableExpression'; -import Update from '../nodes/Update'; -import WebLink from '../nodes/WebLink'; -import type Caret from './Caret'; -import Remove from './Remove'; +import Translation from '@nodes/Translation'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import BinaryEvaluate from '../../nodes/BinaryEvaluate'; +import Bind from '../../nodes/Bind'; +import Block from '../../nodes/Block'; +import BooleanType from '../../nodes/BooleanType'; +import Changed from '../../nodes/Changed'; +import Conditional from '../../nodes/Conditional'; +import ConversionDefinition from '../../nodes/ConversionDefinition'; +import Convert from '../../nodes/Convert'; +import Delete from '../../nodes/Delete'; +import Dimension from '../../nodes/Dimension'; +import Doc from '../../nodes/Doc'; +import Docs from '../../nodes/Docs'; +import Evaluate from '../../nodes/Evaluate'; +import Example from '../../nodes/Example'; +import Expression from '../../nodes/Expression'; +import ExpressionPlaceholder from '../../nodes/ExpressionPlaceholder'; +import FunctionDefinition from '../../nodes/FunctionDefinition'; +import FunctionType from '../../nodes/FunctionType'; +import Initial from '../../nodes/Initial'; +import Insert from '../../nodes/Insert'; +import Is from '../../nodes/Is'; +import IsLocale from '../../nodes/IsLocale'; +import KeyValue from '../../nodes/KeyValue'; +import Language from '../../nodes/Language'; +import ListAccess from '../../nodes/ListAccess'; +import ListLiteral from '../../nodes/ListLiteral'; +import ListType from '../../nodes/ListType'; +import MapLiteral from '../../nodes/MapLiteral'; +import MapType from '../../nodes/MapType'; +import Markup from '../../nodes/Markup'; +import NoneLiteral from '../../nodes/NoneLiteral'; +import NoneType from '../../nodes/NoneType'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import NumberType from '../../nodes/NumberType'; +import Paragraph from '../../nodes/Paragraph'; +import Previous from '../../nodes/Previous'; +import Program from '../../nodes/Program'; +import PropertyBind from '../../nodes/PropertyBind'; +import PropertyReference from '../../nodes/PropertyReference'; +import Reaction from '../../nodes/Reaction'; +import Reference from '../../nodes/Reference'; +import Select from '../../nodes/Select'; +import SetLiteral from '../../nodes/SetLiteral'; +import SetType from '../../nodes/SetType'; +import StructureDefinition from '../../nodes/StructureDefinition'; +import { WildcardSymbols } from '../../nodes/Sym'; +import TableLiteral from '../../nodes/TableLiteral'; +import TextLiteral from '../../nodes/TextLiteral'; +import TextType from '../../nodes/TextType'; +import This from '../../nodes/This'; +import Token from '../../nodes/Token'; +import TypeInputs from '../../nodes/TypeInputs'; +import TypePlaceholder from '../../nodes/TypePlaceholder'; +import TypeVariables from '../../nodes/TypeVariables'; +import UnaryEvaluate from '../../nodes/UnaryEvaluate'; +import UnionType from '../../nodes/UnionType'; +import Unit from '../../nodes/Unit'; +import UnparsableExpression from '../../nodes/UnparsableExpression'; +import Update from '../../nodes/Update'; +import WebLink from '../../nodes/WebLink'; +import type { InsertContext, ReplaceContext } from '../revision/EditContext'; +import Remove from '../revision/Remove'; /** A logging flag, helpful for analyzing the control flow of autocomplete when debugging. */ const LOG = false; @@ -87,10 +97,20 @@ function note(message: string, level: number) { if (LOG) console.log(`${' '.repeat(level)}Autocomplete: ${message}`); } +function removeDuplicates(edits: Revision[]): Revision[] { + return edits.filter( + (edit1, index1) => + !edits.some( + (edit2, index2) => index2 > index1 && edit1.equals(edit2), + ), + ); +} + /** Given a project and a caret, generate a set of transforms that can be applied at the location. */ export function getEditsAt( project: Project, caret: Caret, + field: FieldPosition | undefined, locales: Locales, ): Revision[] { const source = caret.source; @@ -98,21 +118,32 @@ export function getEditsAt( const isEmptyLine = caret.isEmptyLine(); - let edits: Revision[] = []; + // If we were given a field position, return edits that are reasonable for that field. + if (field !== undefined) { + note( + `Getting possible field edits for field position ${field.parent.getDescriptor()}.${field.field}`, + 1, + ); - // If the token is a node, find the allowable nodes to replace this node, or whether it's removable - if (caret.position instanceof Node) { + return removeDuplicates(getFieldAssignments(field, context, locales)); + } + // If we have a node selected, find possible replacements or removals. + else if (caret.position instanceof Node) { note( `Getting possible field edits for node selection ${caret.position.toWordplay()}`, 1, ); - edits = getNodeEdits(caret.position, context); + return removeDuplicates( + getNodeRevisions(caret.position, context, locales), + ); } // If the token is a position rather than a node, find edits for the nodes between. else if (caret.isPosition()) { note(`Caret is position, finding nodes before and after.`, 0); + let edits: Revision[] = []; + // If there are no nodes between (because the caret is in the middle of a token) if (caret.insideToken() && caret.tokenExcludingSpace) { note( @@ -120,11 +151,19 @@ export function getEditsAt( 1, ); - edits = getNodeEdits(caret.tokenExcludingSpace, context); + edits = getNodeRevisions( + caret.tokenExcludingSpace, + context, + locales, + ); } - const { before, after } = caret.getNodesBetween(); + // What's before and after the caret? + let { before, after } = caret.getNodesBetween(); + + // True if the caret is immediately after the prior token on the same line. const adjacent = caret.position === caret.tokenSpaceIndex; + const caretLine = caret.getLine(); // For each node before, get edits for what can come after. for (const node of before) { @@ -135,7 +174,7 @@ export function getEditsAt( true, node, caret.position, - adjacent, + adjacent && caretLine === caret.source.getLine(node), isEmptyLine, context, locales, @@ -152,7 +191,7 @@ export function getEditsAt( false, node, caret.position, - adjacent, + adjacent && caretLine === caret.source.getLine(node), isEmptyLine, context, locales, @@ -163,7 +202,7 @@ export function getEditsAt( // We have to special case empty Program Blocks. This is because all other sequences are // delimited except for program blocks, so when we're in an empty program block, there is no // delimiter to anchor off of. - if (context.source.leaves().length === 1) { + if (context.source.hasOneLeaf()) { note(`Getting edits for empty block`, 1); const programField = @@ -174,14 +213,14 @@ export function getEditsAt( ...programField.kind .enumerate() .map((kind) => - getPossibleNodes( - programField, - kind, - undefined, - source.expression.expression, - false, + getPossibleNodes(programField, kind, { + type: undefined, context, - ) + parent: source.expression.expression, + field: 'statements', + index: 0, + locales, + }) .filter( (kind): kind is Node | Refer => kind !== undefined, @@ -192,7 +231,8 @@ export function getEditsAt( context, caret.position as number, source.expression.expression, - source.expression.expression.statements, + source.expression.expression + .statements, 0, insertion, ), @@ -202,28 +242,106 @@ export function getEditsAt( ]; } } + + return removeDuplicates(edits); } + return []; +} - note(`Removing duplicates`, 0); +/** Given a field position, get possible values for the field */ +function getFieldAssignments( + fieldPosition: FieldPosition, + context: Context, + locales: Locales, +) { + const { parent, field, index } = fieldPosition; + // Get the field of the parent node's grammar. + const fieldInfo = parent.getFieldNamed(field); + if (fieldInfo === undefined) return []; - return edits.filter( - (edit1, index1) => - !edits.some( - (edit2, index2) => index2 > index1 && edit1.equals(edit2), - ), - ); + // Get the current value of the field. + const fieldValue = parent.getField(field); + + // Match the type of the current node + const expectedType = fieldInfo.getType + ? fieldInfo.getType(context, undefined) + : undefined; + + let edits: Revision[] = []; + + // Is the field currently set? Add a removal. + if (fieldValue instanceof Node) { + edits.push(new Remove(context, parent, fieldValue)); + } + + // Get possible assignments for this field. + if ( + fieldValue === undefined || + (Array.isArray(fieldValue) && index !== undefined) + ) { + // Figure out where the insertion is happening, so we know how to split space. + let position = + fieldValue === undefined || fieldValue.length === 0 + ? (context.source.getFieldPosition(parent, field) ?? 0) + : (context.source.getNodeLastPosition( + fieldValue[(index ?? 1) - 1], + ) ?? 0); + + for (const kind of fieldInfo.kind.enumerate()) { + if (kind !== undefined) + edits = [ + ...edits, + ...getPossibleNodes(fieldInfo, kind, { + type: expectedType, + context, + field, + parent, + index, + locales, + }) + .filter((r) => r !== undefined) + .map((replacement) => + // No value? Create an assign. + fieldValue === undefined + ? new Assign(context, position, parent, [ + { field: field, node: replacement }, + // Are there any other fields required to be set when this one is set? + // Include it in the proposed assignment. + ...generateDependentAssignments( + fieldInfo, + locales, + replacement, + ), + ]) + : new Append( + context, + position, + parent, + fieldValue, + index ?? 0, + replacement, + ), + ), + ]; + } + } + + return edits; } /** Given a node, get possible replacements */ -function getNodeEdits(anchor: Node, context: Context) { +function getNodeRevisions(anchor: Node, context: Context, locales: Locales) { let edits: Revision[] = []; // Get the allowed kinds on this node and then translate them into replacement edits. edits = getFieldEdits(anchor, context, (field, parent, node) => { - // Match the type of the current node + // The expected type is the type expected of the field the node is currently in, or if there isn't an expected + // type, the type of the node itself, if it is an expression. const expectedType = field.getType ? field.getType(context, undefined) - : undefined; + : anchor instanceof Expression + ? anchor.getType(context).generalize(context) + : undefined; // Get the value of the field. const fieldValue = parent.getField(field.name); @@ -239,16 +357,15 @@ function getNodeEdits(anchor: Node, context: Context) { ...field.kind .enumerate() .map((kind) => - getPossibleNodes( - field, - kind, - expectedType, + getPossibleNodes(field, kind, { + type: expectedType, node, - true, context, - ).map( - (replacement) => - new Replace(context, parent, node, replacement), + locales, + }).map((replacement) => + replacement === undefined + ? new Remove(context, parent, node) + : new Replace(context, parent, node, replacement), ), ) .flat(), @@ -339,7 +456,7 @@ function getRelativeFieldEdits( // Generate possible nodes that could replace the token prior // (e.g., autocomplete References, create binary operations) // We only do this if this is before, and we're immediately after - // a node, and for replacements that "complete" the existing parent. + // a node, and on not in a list on a different line and for replacements that "complete" the existing parent. if (isAfterAnchor && adjacent) { // If the anchor is in a list, get it's index. const list = parent.getField(field.name); @@ -357,46 +474,32 @@ function getRelativeFieldEdits( 2, ); - edits = [ - ...edits, - ...field.kind - .enumerate() - .map((kind) => - getPossibleNodes( - field, - kind, - expectedType instanceof UnknownType - ? undefined - : expectedType, - anchorNode, - true, - context, - ) - // If not on an empty line, only include recommendations that "complete" the selection - .filter( - (replacement) => - empty || - (replacement !== undefined && - completes( - anchorNode, - replacement instanceof Node - ? replacement - : replacement.getNode(locales), - )), - ) - // Convert the matching nodes to replacements. - .map( - (replacement) => - new Replace( - context, - parent, - anchorNode, - replacement, - ), - ), - ) - .flat(), - ]; + if (anchorNode instanceof Reference) { + edits = [ + ...edits, + ...Reference.getPossibleReferences( + expectedType, + anchorNode, + true, + context, + ).map( + (replacement) => + new Replace(context, parent, anchorNode, replacement), + ), + ]; + } else if (anchorNode instanceof PropertyReference) { + edits = [ + ...edits, + ...PropertyReference.getPossibleReferences( + expectedType, + anchorNode, + context, + ).map( + (replacement) => + new Replace(context, parent, anchorNode, replacement), + ), + ]; + } } // Scan the parent's grammar for fields before or after to the current node the caret is it. @@ -438,7 +541,7 @@ function getRelativeFieldEdits( 1; if (index >= 0) { // Find the expected type of the position in the list. - // Some lists don't care, other lists do (e.g., Evaluate has very specific type expectations based on it's function definnition). + // Some lists don't care, other lists do (e.g., Evaluate has very specific type expectations based on it's function definition). // If this field is before, then we do the index after. If the field we're analyzing is after, we keep the current index as the insertion point. const expectedType = relativeField.getType ? relativeField.getType(context, index) @@ -448,14 +551,14 @@ function getRelativeFieldEdits( ...relativeField.kind .enumerate() .map((kind) => - getPossibleNodes( - relativeField, - kind, - expectedType, - anchorNode, - false, + getPossibleNodes(relativeField, kind, { + type: expectedType, context, - ) + locales, + parent, + field: relativeField.name, + index, + }) // Some nodes will suggest removals. We filter those here. .filter( (kind): kind is Node | Refer => @@ -493,42 +596,17 @@ function getRelativeFieldEdits( ...relativeField.kind .enumerate() .map((kind) => - getPossibleNodes( - relativeField, - kind, - expectedType, - anchorNode, - false, + getPossibleNodes(relativeField, kind, { + type: expectedType, + parent: anchorNode, + field: relativeField.name, context, - ) + locales, + index: undefined, + }) // Filter out any undefined values, since the field is already undefined. .filter((node) => node !== undefined) .map((addition) => { - // Are there any other fields required to be set when this one is set? - // Include it in the proposed assignment. - const otherNodes = relativeField.kind - .enumerateFieldKinds() - .filter( - (kind): kind is Empty => - kind instanceof Empty && - kind.dependency !== undefined && - parent.getField( - kind.dependency.name, - ) === undefined, - ) - .map((kind) => { - if (kind.dependency) { - return { - field: kind.dependency.name, - node: kind.dependency.createDefault(), - }; - } else return undefined; - }) - .filter( - (addition) => - addition !== undefined, - ); - return new Assign( context, position, @@ -538,7 +616,13 @@ function getRelativeFieldEdits( field: relativeField.name, node: addition, }, - ...otherNodes, + // Are there any other fields required to be set when this one is set? + // Include it in the proposed assignment. + ...generateDependentAssignments( + relativeField, + locales, + addition, + ), ], ); }), @@ -553,32 +637,32 @@ function getRelativeFieldEdits( } /** - * Given two nodes, determines if some part of the original node appears in the replacement node. - * "Appears" in this case means that one of the replacement's name tokens starts with one of the original's name tokens, - * or that one of the non-token nodes in the replacement is equal to one of the non-token nodes in the original. + * Generate assignments for a field based on dependencies */ -function completes(original: Node, replacement: Node): boolean { - // Completes if it contains a node equal to the original node - const replacementNodes = replacement.nodes(); - if (replacementNodes.some((node) => node.isEqualTo(original))) return true; - - // Completes if there's a name in the replacement that completes the original node. - const originalNodes = original.nodes(); - return replacementNodes.some((n1) => - originalNodes.some((n2) => { - const n1isToken = n1 instanceof Token; - const n2isToken = n2 instanceof Token; - const n1isName = n1isToken && n1.isName(); - const n2isName = n2isToken && n2.isName(); - return ( - (n1isToken && - n1isName && - n2isName && - n1.getText().startsWith(n2.getText())) || - (!n1isToken && !n2isToken && n1.isEqualTo(n2)) - ); - }), - ); +function generateDependentAssignments( + field: Field, + locales: Locales, + addition: Node | Refer, +): { field: string; node: Node | Refer }[] { + return field.kind + .enumerateFieldKinds() + .filter( + (kind): kind is Empty => + kind instanceof Empty && kind.dependency !== undefined, + ) + .map((kind) => { + if (kind.dependency) { + return { + field: kind.dependency.name, + node: kind.dependency.createDefault( + addition instanceof Node + ? addition + : addition.getNode(locales), + ), + }; + } else return undefined; + }) + .filter((addition) => addition !== undefined); } /** A list of node types from which we can generate replacements. Order affects where they appear in autocomplete menus. */ @@ -589,7 +673,9 @@ const PossibleNodes = [ NumberLiteral, BooleanLiteral, TextLiteral, + Translation, FormattedLiteral, + FormattedTranslation, NoneLiteral, ListLiteral, ListAccess, @@ -605,6 +691,7 @@ const PossibleNodes = [ ConversionDefinition, // Binds and blocks Bind, + Name, Block, PropertyReference, PropertyBind, @@ -630,9 +717,9 @@ const PossibleNodes = [ // Docs, Doc, Docs, + DocumentedExpression, Example, Markup, - Mention, Paragraph, WebLink, // Tables @@ -642,77 +729,86 @@ const PossibleNodes = [ Delete, Update, // Types - TypeInputs, - TypeVariables, - FunctionType, - TypePlaceholder, - UnionType, + TextType, + NumberType, Unit, Dimension, BooleanType, - ListType, - MapType, NoneType, - NumberType, + ListType, SetType, - TextType, + MapType, TableType, + UnionType, + NameType, + FunctionType, + TypePlaceholder, + TypeInputs, + TypeVariables, + ConversionType, ]; +/** Given a field, a kind of node, an optional expected type, an optional selected node, and a context, + * get the nodes that are possible to set, insert, or replace. */ function getPossibleNodes( field: Field, kind: NodeKind, - expectedType: Type | undefined, - anchor: Node, - selected: boolean, - context: Context, + action: InsertContext | ReplaceContext, ): (Node | Refer | undefined)[] { - // Undefined? That's just undefined, + // Looking for a node kind that is undefined? That's just undefined. if (kind === undefined) return [undefined]; - // Symbol? That's just a token. We use the symbol's string as the text. Don't recommend it if it's already that. + + // Symbol? That represents a token. We use the symbol's string as the text. Don't recommend it if it's already that. if (!(kind instanceof Function)) { const newToken = new Token(kind.toString(), kind); // Don't generate tokens on uncompletable fields, tokens that are equal to the existing token, or tokens that are numbers, text, or names. return field.uncompletable || - newToken.isEqualTo(anchor) || + ('node' in action && + action.node !== undefined && + newToken.isEqualTo(action.node)) || WildcardSymbols.has(kind) ? [] : [newToken]; } - const menuContext = { node: anchor, context, type: expectedType }; + const anchor = 'node' in action ? action.node : undefined; // Otherwise, it's a non-terminal. Let's find all the nodes that we can make that satisify the node kind, // creating nodes or node references that are compatible with the requested kind. - return ( - // Filter nodes by the kind provided. - PossibleNodes.filter( - (possibleKind) => - possibleKind.prototype instanceof kind || kind === possibleKind, + + const possible = PossibleNodes.filter( + (possibleKind) => + possibleKind.prototype instanceof kind || kind === possibleKind, + ) + // Convert each node type to possible nodes. Each node implements a static function that generates possibilities + // from the context given. + .map((possibleKind) => + 'node' in action + ? possibleKind.getPossibleReplacements(action) + : possibleKind.getPossibleInsertions(action), ) - // Convert each node type to possible nodes. Each node implements a static function that generates possibilities - // from the context given. - .map((possibleKind) => - selected - ? possibleKind.getPossibleReplacements(menuContext) - : possibleKind.getPossibleAppends(menuContext), - ) - // Flatten the list of possible nodes. - .flat() - .filter( - (node) => - // Filter out nodes that don't match the given type, if provided. - (expectedType === undefined || - !(node instanceof Expression) || - expectedType.accepts(node.getType(context), context)) && - // Filter out nodes that are equivalent to the selection node - (anchor === undefined || - (node instanceof Refer && - (!(anchor instanceof Reference) || - (anchor instanceof Reference && - node.definition !== - anchor.resolve(context)))) || - (node instanceof Node && !anchor.isEqualTo(node))), - ) + // Flatten the list of possible nodes. + .flat(); + + const filtered = possible.filter( + (node) => + // Filter out nodes that don't match the given type, if provided. + (action.type === undefined || + !(node instanceof Expression) || + action.type.accepts( + node.getType(action.context), + action.context, + )) && + // Filter out nodes that are equivalent to the selection node, if there is one. + (anchor === undefined || + (node instanceof Refer && + (!(anchor instanceof Reference) || + (anchor instanceof Reference && + node.definition !== + anchor.resolve(action.context)))) || + (node instanceof Node && + !(anchor !== undefined && anchor.isEqualTo(node)))), ); + + return filtered; } diff --git a/src/edit/getPossibleUnits.ts b/src/edit/menu/getPossibleUnits.ts similarity index 95% rename from src/edit/getPossibleUnits.ts rename to src/edit/menu/getPossibleUnits.ts index 234859c21..bad21ed5c 100644 --- a/src/edit/getPossibleUnits.ts +++ b/src/edit/menu/getPossibleUnits.ts @@ -1,8 +1,8 @@ import NumberType from '@nodes/NumberType'; import Unit from '@nodes/Unit'; -import type Project from '../db/projects/Project'; -import type Context from '../nodes/Context'; -import Dimension from '../nodes/Dimension'; +import type Project from '../../db/projects/Project'; +import type Context from '../../nodes/Context'; +import Dimension from '../../nodes/Dimension'; export function getPossibleUnits(context: Context) { const project = context.project; diff --git a/src/edit/GroupProperties.ts b/src/edit/output/GroupProperties.ts similarity index 85% rename from src/edit/GroupProperties.ts rename to src/edit/output/GroupProperties.ts index 2ba534fc9..e8db1c219 100644 --- a/src/edit/GroupProperties.ts +++ b/src/edit/output/GroupProperties.ts @@ -1,9 +1,9 @@ -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import Evaluate from '../nodes/Evaluate'; -import type Expression from '../nodes/Expression'; -import ListLiteral from '../nodes/ListLiteral'; -import Reference from '../nodes/Reference'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import Evaluate from '../../nodes/Evaluate'; +import type Expression from '../../nodes/Expression'; +import ListLiteral from '../../nodes/ListLiteral'; +import Reference from '../../nodes/Reference'; import { getTypeOutputProperties } from './OutputProperties'; import OutputProperty from './OutputProperty'; import OutputPropertyOptions from './OutputPropertyOptions'; diff --git a/src/edit/OutputExpression.ts b/src/edit/output/OutputExpression.ts similarity index 97% rename from src/edit/OutputExpression.ts rename to src/edit/output/OutputExpression.ts index fe9610b4b..d04e160e9 100644 --- a/src/edit/OutputExpression.ts +++ b/src/edit/output/OutputExpression.ts @@ -6,14 +6,14 @@ import StructureDefinition from '@nodes/StructureDefinition'; import NumberValue from '@values/NumberValue'; import TextValue from '@values/TextValue'; import type Value from '@values/Value'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import Evaluate from '../nodes/Evaluate'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import Evaluate from '../../nodes/Evaluate'; +import getShapeProperties from './getShapeProperties'; import getGroupProperties from './GroupProperties'; import type OutputProperty from './OutputProperty'; import getPhraseProperties from './PhraseProperties'; import getStageProperties from './StageProperties'; -import getShapeProperties from './getShapeProperties'; /** * Represents the value of a property. If given is true, it means its set explicitly. diff --git a/src/edit/OutputProperties.ts b/src/edit/output/OutputProperties.ts similarity index 94% rename from src/edit/OutputProperties.ts rename to src/edit/output/OutputProperties.ts index fe1475b4e..3b7c64277 100644 --- a/src/edit/OutputProperties.ts +++ b/src/edit/output/OutputProperties.ts @@ -6,12 +6,12 @@ import TextLiteral from '@nodes/TextLiteral'; import Unit from '@nodes/Unit'; import { DefaultStyle } from '@output/Output'; import { createPoseLiteral } from '@output/Pose'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import type { LocaleTextsAccessor } from '../locale/Locales'; -import type { NameText } from '../locale/LocaleText'; -import BooleanLiteral from '../nodes/BooleanLiteral'; -import Reference from '../nodes/Reference'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import type { LocaleTextsAccessor } from '../../locale/Locales'; +import type { NameText } from '../../locale/LocaleText'; +import BooleanLiteral from '../../nodes/BooleanLiteral'; +import Reference from '../../nodes/Reference'; import OutputProperty from './OutputProperty'; import OutputPropertyOptions from './OutputPropertyOptions'; import OutputPropertyRange from './OutputPropertyRange'; diff --git a/src/edit/OutputProperty.ts b/src/edit/output/OutputProperty.ts similarity index 92% rename from src/edit/OutputProperty.ts rename to src/edit/output/OutputProperty.ts index 8f852329e..e88410cf2 100644 --- a/src/edit/OutputProperty.ts +++ b/src/edit/output/OutputProperty.ts @@ -1,8 +1,8 @@ import type Context from '@nodes/Context'; import type Expression from '@nodes/Expression'; -import { getFirstText } from '../locale/LocaleText'; -import type Locales from '../locale/Locales'; -import type { LocaleTextsAccessor } from '../locale/Locales'; +import { getFirstText } from '../../locale/LocaleText'; +import type Locales from '../../locale/Locales'; +import type { LocaleTextsAccessor } from '../../locale/Locales'; import type OutputPropertyOptions from './OutputPropertyOptions'; import type OutputPropertyRange from './OutputPropertyRange'; import type OutputPropertyText from './OutputPropertyText'; diff --git a/src/edit/OutputPropertyOptions.ts b/src/edit/output/OutputPropertyOptions.ts similarity index 94% rename from src/edit/OutputPropertyOptions.ts rename to src/edit/output/OutputPropertyOptions.ts index 43fcc85cc..d45e0b645 100644 --- a/src/edit/OutputPropertyOptions.ts +++ b/src/edit/output/OutputPropertyOptions.ts @@ -1,4 +1,4 @@ -import type Expression from '../nodes/Expression'; +import type Expression from '../../nodes/Expression'; export default class OutputPropertyOptions { readonly values: { value: string; label: string }[]; diff --git a/src/edit/OutputPropertyRange.ts b/src/edit/output/OutputPropertyRange.ts similarity index 100% rename from src/edit/OutputPropertyRange.ts rename to src/edit/output/OutputPropertyRange.ts diff --git a/src/edit/OutputPropertyText.ts b/src/edit/output/OutputPropertyText.ts similarity index 100% rename from src/edit/OutputPropertyText.ts rename to src/edit/output/OutputPropertyText.ts diff --git a/src/edit/OutputPropertyValueSet.ts b/src/edit/output/OutputPropertyValueSet.ts similarity index 91% rename from src/edit/OutputPropertyValueSet.ts rename to src/edit/output/OutputPropertyValueSet.ts index 256ab0573..0035e4eee 100644 --- a/src/edit/OutputPropertyValueSet.ts +++ b/src/edit/output/OutputPropertyValueSet.ts @@ -5,15 +5,15 @@ import MarkupValue from '@values/MarkupValue'; import NumberValue from '@values/NumberValue'; import TextValue from '@values/TextValue'; import type Value from '@values/Value'; -import type { Database } from '../db/Database'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import type LocaleText from '../locale/LocaleText'; -import type Bind from '../nodes/Bind'; -import ListLiteral from '../nodes/ListLiteral'; -import MapLiteral from '../nodes/MapLiteral'; -import type StreamDefinition from '../nodes/StreamDefinition'; -import type StructureDefinition from '../nodes/StructureDefinition'; +import type { Database } from '../../db/Database'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import type LocaleText from '../../locale/LocaleText'; +import type Bind from '../../nodes/Bind'; +import ListLiteral from '../../nodes/ListLiteral'; +import MapLiteral from '../../nodes/MapLiteral'; +import type StreamDefinition from '../../nodes/StreamDefinition'; +import type StructureDefinition from '../../nodes/StructureDefinition'; import type { OutputPropertyValue } from './OutputExpression'; import OutputExpression from './OutputExpression'; import type OutputProperty from './OutputProperty'; @@ -166,7 +166,7 @@ export default class OutputPropertyValueSet { } getDocs(locales: Locales) { - return this.values[0]?.bind.docs?.getPreferredLocale(locales); + return this.values[0]?.bind.docs.getPreferredLocale(locales); } /** Given a project, unsets this property on expressions on which it is set. */ diff --git a/src/edit/PhraseProperties.ts b/src/edit/output/PhraseProperties.ts similarity index 94% rename from src/edit/PhraseProperties.ts rename to src/edit/output/PhraseProperties.ts index 040b6e524..3144dbc12 100644 --- a/src/edit/PhraseProperties.ts +++ b/src/edit/output/PhraseProperties.ts @@ -8,11 +8,11 @@ import Evaluate from '@nodes/Evaluate'; import FormattedLiteral from '@nodes/FormattedLiteral'; import Reference from '@nodes/Reference'; import TextLiteral from '@nodes/TextLiteral'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import Language from '../nodes/Language'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Unit from '../nodes/Unit'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import Language from '../../nodes/Language'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Unit from '../../nodes/Unit'; import { getTypeOutputProperties } from './OutputProperties'; import OutputProperty from './OutputProperty'; import OutputPropertyOptions from './OutputPropertyOptions'; diff --git a/src/edit/PoseProperties.ts b/src/edit/output/PoseProperties.ts similarity index 89% rename from src/edit/PoseProperties.ts rename to src/edit/output/PoseProperties.ts index 62c4996fa..73aa0b365 100644 --- a/src/edit/PoseProperties.ts +++ b/src/edit/output/PoseProperties.ts @@ -1,11 +1,11 @@ -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import BooleanLiteral from '../nodes/BooleanLiteral'; -import Evaluate from '../nodes/Evaluate'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Reference from '../nodes/Reference'; -import Unit from '../nodes/Unit'; -import { createColorLiteral } from '../output/Color'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import BooleanLiteral from '../../nodes/BooleanLiteral'; +import Evaluate from '../../nodes/Evaluate'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Reference from '../../nodes/Reference'; +import Unit from '../../nodes/Unit'; +import { createColorLiteral } from '../../output/Color'; import OutputProperty from './OutputProperty'; import OutputPropertyRange from './OutputPropertyRange'; diff --git a/src/edit/SequenceProperties.ts b/src/edit/output/SequenceProperties.ts similarity index 82% rename from src/edit/SequenceProperties.ts rename to src/edit/output/SequenceProperties.ts index 05d500314..5553e0935 100644 --- a/src/edit/SequenceProperties.ts +++ b/src/edit/output/SequenceProperties.ts @@ -1,11 +1,11 @@ import TextLiteral from '@nodes/TextLiteral'; -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import KeyValue from '../nodes/KeyValue'; -import MapLiteral from '../nodes/MapLiteral'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Unit from '../nodes/Unit'; -import { createPoseLiteral } from '../output/Pose'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import KeyValue from '../../nodes/KeyValue'; +import MapLiteral from '../../nodes/MapLiteral'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Unit from '../../nodes/Unit'; +import { createPoseLiteral } from '../../output/Pose'; import { getDurationProperty, getStyleProperty } from './OutputProperties'; import OutputProperty from './OutputProperty'; import OutputPropertyRange from './OutputPropertyRange'; diff --git a/src/edit/ShadowProperties.ts b/src/edit/output/ShadowProperties.ts similarity index 85% rename from src/edit/ShadowProperties.ts rename to src/edit/output/ShadowProperties.ts index 605f36aa2..a7dbbdffc 100644 --- a/src/edit/ShadowProperties.ts +++ b/src/edit/output/ShadowProperties.ts @@ -1,11 +1,11 @@ // import BooleanLiteral from '../nodes/BooleanLiteral'; -import Evaluate from '../nodes/Evaluate'; -import NumberLiteral from '../nodes/NumberLiteral'; +import Evaluate from '../../nodes/Evaluate'; +import NumberLiteral from '../../nodes/NumberLiteral'; // import Reference from '../nodes/Reference'; // import Unit from '../nodes/Unit'; -import type Project from '../db/projects/Project'; -import type LocaleText from '../locale/LocaleText'; -import { createColorLiteral } from '../output/Color'; +import type Project from '../../db/projects/Project'; +import type LocaleText from '../../locale/LocaleText'; +import { createColorLiteral } from '../../output/Color'; import OutputProperty from './OutputProperty'; import OutputPropertyRange from './OutputPropertyRange'; diff --git a/src/edit/ShapeProperties.ts b/src/edit/output/ShapeProperties.ts similarity index 77% rename from src/edit/ShapeProperties.ts rename to src/edit/output/ShapeProperties.ts index 3cd8d79c8..ad053021e 100644 --- a/src/edit/ShapeProperties.ts +++ b/src/edit/output/ShapeProperties.ts @@ -1,6 +1,6 @@ -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import ListLiteral from '../nodes/ListLiteral'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import ListLiteral from '../../nodes/ListLiteral'; import { getOutputProperties } from './OutputProperties'; import OutputProperty from './OutputProperty'; diff --git a/src/edit/StageProperties.ts b/src/edit/output/StageProperties.ts similarity index 78% rename from src/edit/StageProperties.ts rename to src/edit/output/StageProperties.ts index 0445b6966..e84630acd 100644 --- a/src/edit/StageProperties.ts +++ b/src/edit/output/StageProperties.ts @@ -1,8 +1,8 @@ -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import ListLiteral from '../nodes/ListLiteral'; -import NumberLiteral from '../nodes/NumberLiteral'; -import Unit from '../nodes/Unit'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import ListLiteral from '../../nodes/ListLiteral'; +import NumberLiteral from '../../nodes/NumberLiteral'; +import Unit from '../../nodes/Unit'; import { getTypeOutputProperties } from './OutputProperties'; import OutputProperty from './OutputProperty'; import OutputPropertyRange from './OutputPropertyRange'; diff --git a/src/edit/getShapeProperties.ts b/src/edit/output/getShapeProperties.ts similarity index 77% rename from src/edit/getShapeProperties.ts rename to src/edit/output/getShapeProperties.ts index e41602c20..86b9c4a02 100644 --- a/src/edit/getShapeProperties.ts +++ b/src/edit/output/getShapeProperties.ts @@ -1,6 +1,6 @@ -import type Project from '../db/projects/Project'; -import type Locales from '../locale/Locales'; -import ListLiteral from '../nodes/ListLiteral'; +import type Project from '../../db/projects/Project'; +import type Locales from '../../locale/Locales'; +import ListLiteral from '../../nodes/ListLiteral'; import { getOutputProperties } from './OutputProperties'; import OutputProperty from './OutputProperty'; diff --git a/src/edit/Append.ts b/src/edit/revision/Append.ts similarity index 92% rename from src/edit/Append.ts rename to src/edit/revision/Append.ts index 66079fd72..9a2ce6497 100644 --- a/src/edit/Append.ts +++ b/src/edit/revision/Append.ts @@ -1,18 +1,19 @@ +import Caret from '@edit/caret/Caret'; import type Context from '@nodes/Context'; import Node from '@nodes/Node'; import getPreferredSpaces from '@parser/getPreferredSpaces'; -import type { Edit } from '../components/editor/util/Commands'; -import type Locales from '../locale/Locales'; -import Bind from '../nodes/Bind'; -import Caret from './Caret'; +import type { Edit } from '../../components/editor/commands/Commands'; +import type Locales from '../../locale/Locales'; +import Bind from '../../nodes/Bind'; import Refer from './Refer'; import Revision from './Revision'; export default class Append extends Revision { - readonly parent: Node; + /** The source index where the insertion occurs */ readonly position: number; /** Undefined means after the last child. Otherwise, the node should be whatever child we're inserting before, even if it's not part of the list. */ readonly index: number; + /** The current list of nodes being appended to */ readonly list: Node[]; readonly insertion: NodeType | Refer; @@ -24,9 +25,8 @@ export default class Append extends Revision { index: number, insertion: NodeType | Refer, ) { - super(context); + super(parent, context); - this.parent = parent; this.position = position; this.list = list; this.index = index; @@ -41,6 +41,10 @@ export default class Append extends Revision { return false; } + getRemoved(): Node[] { + return []; + } + isCompletion(): boolean { return ( this.insertion instanceof Refer && diff --git a/src/edit/Assign.ts b/src/edit/revision/Assign.ts similarity index 78% rename from src/edit/Assign.ts rename to src/edit/revision/Assign.ts index e923f58e2..00bc94bd2 100644 --- a/src/edit/Assign.ts +++ b/src/edit/revision/Assign.ts @@ -1,27 +1,29 @@ +import Caret from '@edit/caret/Caret'; import type Context from '@nodes/Context'; import Node from '@nodes/Node'; import getPreferredSpaces from '@parser/getPreferredSpaces'; -import type { Edit } from '../components/editor/util/Commands'; -import type Locales from '../locale/Locales'; -import Caret from './Caret'; +import type { Edit } from '../../components/editor/commands/Commands'; +import type Locales from '../../locale/Locales'; import Refer from './Refer'; import Revision from './Revision'; +type Addition = { field: string; node: Node | Refer }; + /** Set a field on a child */ -export default class Assign extends Revision { - readonly parent: Node; +export default class Assign extends Revision { + /** The source index where the assignment occurs */ readonly position: number; - // A list of field name and value pairs to assign. - readonly additions: { field: string; node: NodeType | Refer | undefined }[]; + /** A list of field name and value pairs to assign. */ + readonly additions: [Addition, ...Addition[]]; + constructor( context: Context, position: number, parent: Node, - additions: { field: string; node: NodeType | Refer | undefined }[], + additions: [Addition, ...Addition[]], ) { - super(context); + super(parent, context); - this.parent = parent; this.position = position; this.additions = additions; } @@ -31,7 +33,15 @@ export default class Assign extends Revision { } isRemoval(): boolean { - return this.additions.some(({ node }) => node === undefined); + return false; + } + + getRemoved(): Node[] { + return this.additions + .filter(({ node }) => node === undefined) + .map(({ field }) => this.parent.getField(field)) + .flat() + .filter((node): node is Node => node instanceof Node); } isCompletion(): boolean { @@ -43,7 +53,7 @@ export default class Assign extends Revision { return this.realize(this.additions[0].node, locales); } - realize(node: NodeType | Refer | undefined, locales: Locales) { + realize(node: Node | Refer | undefined, locales: Locales) { return node === undefined ? undefined : node instanceof Node @@ -57,7 +67,7 @@ export default class Assign extends Revision { for (const { field, node } of this.additions) { const newNode = this.realize(node, locales); if (firstNewNode === undefined) firstNewNode = newNode; - newParent = newParent.replace(field, this.realize(node, locales)); + newParent = newParent.replace(field, newNode); } return [firstNewNode ?? newParent, newParent]; } @@ -138,13 +148,9 @@ export default class Assign extends Revision { const otherNode = transform.additions[index].node; return ( field === transform.additions[index].field && - (node === undefined - ? otherNode === undefined - : node instanceof Node - ? otherNode instanceof Node && - node.isEqualTo(otherNode) - : otherNode instanceof Refer && - node.equals(otherNode)) + (node instanceof Node + ? otherNode instanceof Node && node.isEqualTo(otherNode) + : otherNode instanceof Refer && node.equals(otherNode)) ); }) ); diff --git a/src/edit/revision/EditContext.ts b/src/edit/revision/EditContext.ts new file mode 100644 index 000000000..546b06a3a --- /dev/null +++ b/src/edit/revision/EditContext.ts @@ -0,0 +1,27 @@ +import type Locales from '@locale/Locales'; +import type Context from '@nodes/Context'; +import type Node from '@nodes/Node'; +import type Type from '@nodes/Type'; + +type BaseContext = { + /** The context of the edit */ + context: Context; + /** The type expected for whatever node is suggested */ + type: Type | undefined; + /** Locales currently in use */ + locales: Locales; +}; + +export type ReplaceContext = BaseContext & { + /** The optional node being edited */ + node: Node; +}; + +export type InsertContext = BaseContext & { + /** The parent node in which the node is being inserted */ + parent: Node; + /** The field being inserted into */ + field: string; + /** The index at which the node is being inserted, if an insert */ + index: number | undefined; +}; diff --git a/src/edit/revision/Refer.ts b/src/edit/revision/Refer.ts new file mode 100644 index 000000000..b53b2236f --- /dev/null +++ b/src/edit/revision/Refer.ts @@ -0,0 +1,51 @@ +import type Locales from '@locale/Locales'; +import type Context from '@nodes/Context'; +import type Definition from '@nodes/Definition'; +import type Node from '@nodes/Node'; +import TypeVariable from '@nodes/TypeVariable'; + +export default class Refer { + readonly creator: (name: string, operator?: string) => Node; + readonly definition: Definition; + /** True if this should be rendered with an operator name */ + readonly operator: boolean; + /** True if this is a unary operator and should be distinguished from uses of the same function as a binary operator */ + readonly unary: boolean; + + constructor( + creator: (name: string) => Node, + definition: Definition, + operator: boolean = false, + unary: boolean = false, + ) { + this.creator = creator; + this.definition = definition; + this.operator = operator; + this.unary = unary; + } + + getNode(locales: Locales) { + return this.creator( + this.operator + ? (this.definition.names.getOperatorName()?.getName() ?? + this.definition.getPreferredName(locales.getLocales())) + : this.definition.getPreferredName(locales.getLocales()), + ); + } + + getType(context: Context) { + return this.definition instanceof TypeVariable + ? undefined + : this.definition.getType(context); + } + + equals(refer: Refer) { + return ( + refer.definition === this.definition && this.unary === refer.unary + ); + } + + toString() { + return `${this.definition.getNames()[0]}`; + } +} diff --git a/src/edit/Remove.ts b/src/edit/revision/Remove.ts similarity index 93% rename from src/edit/Remove.ts rename to src/edit/revision/Remove.ts index 936c15dc9..512141d6b 100644 --- a/src/edit/Remove.ts +++ b/src/edit/revision/Remove.ts @@ -1,22 +1,20 @@ +import Caret from '@edit/caret/Caret'; import type Context from '@nodes/Context'; import type Node from '@nodes/Node'; import getPreferredSpaces from '@parser/getPreferredSpaces'; -import type { Edit } from '../components/editor/util/Commands'; -import type Locales from '../locale/Locales'; -import Caret from './Caret'; +import type { Edit } from '../../components/editor/commands/Commands'; +import type Locales from '../../locale/Locales'; import Revision from './Revision'; /** * Remove one or more nodes from sequence of nodes in a parent. */ export default class Remove extends Revision { - readonly parent: Node; readonly nodes: Node[]; constructor(context: Context, parent: Node, ...nodes: Node[]) { - super(context); + super(parent, context); - this.parent = parent; this.nodes = nodes; } @@ -28,6 +26,10 @@ export default class Remove extends Revision { return true; } + getRemoved(): Node[] { + return this.getNodes(); + } + isCompletion(): boolean { return false; } diff --git a/src/edit/Replace.ts b/src/edit/revision/Replace.ts similarity index 92% rename from src/edit/Replace.ts rename to src/edit/revision/Replace.ts index 572a5e9a7..abf9964bc 100644 --- a/src/edit/Replace.ts +++ b/src/edit/revision/Replace.ts @@ -1,12 +1,12 @@ +import Caret from '@edit/caret/Caret'; import type LocaleText from '@locale/LocaleText'; import type Context from '@nodes/Context'; import Node from '@nodes/Node'; import getPreferredSpaces from '@parser/getPreferredSpaces'; -import type { Edit } from '../components/editor/util/Commands'; -import type Locales from '../locale/Locales'; -import Markup from '../nodes/Markup'; -import Reference from '../nodes/Reference'; -import Caret from './Caret'; +import type { Edit } from '../../components/editor/commands/Commands'; +import type Locales from '../../locale/Locales'; +import Markup from '../../nodes/Markup'; +import Reference from '../../nodes/Reference'; import Refer from './Refer'; import Revision from './Revision'; @@ -20,13 +20,13 @@ export default class Replace extends Revision { context: Context, parent: Node, node: Node, - replacement: NodeType | Refer | undefined, + replacement: NodeType | Refer, /** True if this replacement completes an existing node */ description: | ((translation: LocaleText) => string) | undefined = undefined, ) { - super(context); + super(parent, context); this.parent = parent; this.node = node; @@ -42,6 +42,10 @@ export default class Replace extends Revision { return this.replacement === undefined; } + getRemoved(): Node[] { + return this.replacement === undefined ? [this.node] : []; + } + /** True if the replacement node has a reference prefixed by a reference in the original node */ isCompletion(locales: Locales): boolean { if (this.replacement === undefined) return false; diff --git a/src/edit/revision/Revision.ts b/src/edit/revision/Revision.ts new file mode 100644 index 000000000..3d8ff5080 --- /dev/null +++ b/src/edit/revision/Revision.ts @@ -0,0 +1,107 @@ +import type ConceptIndex from '@concepts/ConceptIndex'; +import type Purpose from '@concepts/Purpose'; +import type Context from '@nodes/Context'; +import type Node from '@nodes/Node'; +import Root from '@nodes/Root'; +import type Source from '@nodes/Source'; +import type Spaces from '@parser/Spaces'; +import type { Edit } from '../../components/editor/commands/Commands'; +import type Locales from '../../locale/Locales'; +import type Markup from '../../nodes/Markup'; + +export default abstract class Revision { + /** The node with a field being appended to */ + readonly parent: Node; + readonly context: Context; + + constructor(parent: Node, context: Context) { + this.parent = parent; + this.context = context; + } + + /** True if the revision will insert some named thing (e.g., a Refer) */ + abstract isReference(): boolean; + + /** True if the revision removes something */ + abstract isRemoval(): boolean; + + /** The list of nodes to be removed */ + abstract getRemoved(): Node[]; + + /** True if the revision is a completion */ + abstract isCompletion(locales: Locales): boolean; + + /** Create the edit to be processed by Editor. */ + abstract getEdit(locales: Locales): Edit | undefined; + + abstract getDescription(locales: Locales): Markup; + + /** Gets the node to be added, removed, inserted, etc. */ + abstract getNewNode(locales: Locales): Node | undefined; + + /** Gets the added or removed node, and the revised node, which incorporates the new node. May be the same node. Used for the actual edit, but also for previews. */ + abstract getEditedNode(locales: Locales): [Node, Node]; + + abstract equals(transform: Revision): boolean; + + /** + * Get the node to represent the context of the removal and the list of nodes being removed in that context. + * These are helpful for rendering a removal preview. + */ + getRemovalContext(): [Node, Node[]] { + const parent = this.parent; + const parentCopy = parent.clone(); + // Not a removal? Return the parent and no removals. + if (!this.isRemoval()) return [parentCopy, []]; + const removedNodes = this.getRemoved(); + // Not a removal? Return the parent and no removals. + if (removedNodes.length === 0) return [parentCopy, []]; + // Just one removed node? Just return the node itself, no need to account for multiple nodes being removed. + if (removedNodes.length === 1) { + const removal = removedNodes[0].clone(); + return [removal, [removal]]; + } + // Can't find the root for the parent? We need it to calculate paths, so fail by returning the parent. + const root = this.context.getRoot(parent); + if (root === undefined) return [parentCopy, []]; + // There are multiple nodes being removed, so we want to find their paths in the parent copy. + // Find the paths of the removed nodes in the parent. + const parentPath = root.getPath(parent); + const removedPaths = removedNodes.map((n) => + root.getPath(n).slice(parentPath.length), + ); + // Map them back to the copy. + const parentRoot = new Root(parentCopy); + const removedCopies = removedPaths + .map((p) => parentRoot.resolvePath(p)) + .filter((n) => n !== undefined); + return [parentCopy, removedCopies]; + } + + /** Given a concept index, find the purpose of this revision */ + getPurpose(concepts: ConceptIndex): Purpose | undefined { + const node = this.getNewNode(concepts.locales); + if (node) { + const concept = concepts.getRelevantConcept(node); + // If the node is an Evaluate, see if the function or structure it refers to has a concept in the concept index, and use it's purpose. + return concept?.getPurpose() ?? node.getPurpose(); + } else return undefined; + } + + static splitSpace(source: Source, position: number, newNode: Node): Spaces { + const tokenAfter = source.getTokenAt(position); + let newSpaces = source.spaces; + if (tokenAfter !== undefined) { + const indexAfter = source.getTokenSpacePosition(tokenAfter); + if (indexAfter === undefined) return newSpaces; + const spaceAfter = source.spaces.getSpace(tokenAfter); + const spaceOffset = position - indexAfter; + const newSpaceBefore = spaceAfter.substring(0, spaceOffset); + const newSpaceAfter = spaceAfter.substring(spaceOffset); + newSpaces = newSpaces + .withSpace(newNode, newSpaceBefore) + .withSpace(tokenAfter, newSpaceAfter); + } + return newSpaces; + } +} diff --git a/src/locale/ModerationTexts.ts b/src/locale/ModerationTexts.ts index c9d92f1b3..17b02cb0b 100644 --- a/src/locale/ModerationTexts.ts +++ b/src/locale/ModerationTexts.ts @@ -1,16 +1,16 @@ import type { FlagDescriptions } from '@db/projects/Moderation'; import type { Template } from './LocaleText'; -import type { ButtonText, DialogText } from './UITexts'; +import type { ButtonText, HeaderAndExplanationText } from './UITexts'; export type ModerationTexts = { /** What to say to warn viewers before showing content with warnings. */ - warning: DialogText; + warning: HeaderAndExplanationText; /** What to say when content is blocked */ - blocked: DialogText; + blocked: HeaderAndExplanationText; /** What to sa when content has not yet been moderated */ - unmoderated: DialogText; + unmoderated: HeaderAndExplanationText; /** Moderation view text */ - moderate: DialogText; + moderate: HeaderAndExplanationText; /** Content moderation rules that creators promise to follow. See en-US.json for ground truth language. */ flags: FlagDescriptions; /** Progress message */ diff --git a/src/locale/NodeTexts.ts b/src/locale/NodeTexts.ts index e3587bef3..78d78f4e2 100644 --- a/src/locale/NodeTexts.ts +++ b/src/locale/NodeTexts.ts @@ -76,7 +76,7 @@ type NodeTexts = { */ Name: DescriptiveNodeText; /** A list of names, e.g., `hi,hello,hey` */ - Names: NodeText; + Names: NodeText & { label: { names: string } }; /** A row in a table, e.g., `⎡1 2⎦` */ Row: NodeText & Conflicts<{ @@ -110,14 +110,14 @@ type NodeTexts = { Conflicts<{ /** When a type variable name is the same as another. $1: The duplicate name */ DuplicateTypeVariable: ConflictText; - }>; + }> & { label: { type: string } }; /** A list of type variables in a function or structure definition, e.g. `⸨T⸩` in `ƒ⸨T⸩(a: T b: T)` */ TypeVariables: NodeText; /** * Markup text used in documentation or phrase text, e.g., ` ¶Hello, I am *bold*¶ ` * Description inputs: $1 = paragraph count */ - Markup: DescriptiveNodeText; + Markup: DescriptiveNodeText & { label: { paragraphs: string } }; /** * A paragraph of text in `Markup`, e.g., `Paragraph 1` in ` ¶Paragraph 1\n\nParagraph 2¶ ` * Description inputs: $1 = number, $2 = unit @@ -159,7 +159,7 @@ type NodeTexts = { } & Conflicts<{ /** Warning about order of evaluation of binary evaluations always being reading order, not math order of operations */ OrderOfOperations: InternalConflictText; - }>; + }> & { label: { operator: string } }; /** * Naming a value, e.g., `mybinding: 5m` * Description inputs: $1 = the name that is bound @@ -167,8 +167,9 @@ type NodeTexts = { * Finish inputs: $1 = the value producd, $2: the names bound */ Bind: DescriptiveNodeText & - ExpressionText & - Conflicts<{ + ExpressionText & { + label: { value: string; names: string; type: string }; + } & Conflicts<{ /** When a bind has duplicate names. Description inputs: $1: The name that shadowed this one */ DuplicateName: { conflict: ConflictText; resolution: Template }; /** When a shared bind has a duplicate name that's shared. Description inputs: $1: The duplicate */ @@ -199,8 +200,10 @@ type NodeTexts = { */ Block: DescriptiveNodeText & ExpressionText & { - /** The placeholder label for a statement in the block */ - statement: Template; + label: { + /** The placeholder label for a statement in the block */ + statements: string; + }; } & Conflicts<{ /** When there's no ending expression */ ExpectedEndingExpression: InternalConflictText; @@ -223,12 +226,14 @@ type NodeTexts = { */ Borrow: DescriptiveNodeText & SimpleExpressionText & { - /** Placeholder label for the source name */ - source: Template; - /** Placeholder label for the bind name being borrowed */ - bind: Template; - /** Placeholder label for the version being borrowed */ - version: Template; + label: { + /** Placeholder label for the source name */ + source: string; + /** Placeholder label for the bind name being borrowed */ + bind: string; + /** Placeholder label for the version being borrowed */ + version: string; + }; } & Conflicts<{ /** When the borrowed name could not be found */ UnknownBorrow: InternalConflictText; @@ -244,7 +249,7 @@ type NodeTexts = { * A change predicate expression, true if the stream changed, causing this reevaluation, e.g., `∆ Key()` * Start inputs: $1 = stream that changed */ - Changed: NodeText & SimpleExpressionText; + Changed: NodeText & SimpleExpressionText & { label: { stream: string } }; /** * A conditional expression, e.g., `truth ? 'yes' 'no'` * Start inputs: $1 = description of condition to check @@ -256,12 +261,15 @@ type NodeTexts = { afterthen: Template; /** After the then case is done. Description inputs: jump after the "then" expression */ else: Template; - /** A placeholder label for the condition */ - condition: Template; - /** A placeholder label for then expression */ - yes: Template; - /** A placeholder label for else condition */ - no: Template; + } & { + label: { + /** A placeholder label for the condition */ + condition: Template; + /** A placeholder label for then expression */ + yes: Template; + /** A placeholder label for else condition */ + no: Template; + }; } & Conflicts<{ /** * When the condition is not boolean typed, e.g., `1 ? 'yes' 'no'` @@ -279,10 +287,12 @@ type NodeTexts = { */ Match: NodeText & ExpressionText & { - /** The label for the value to be compared against. */ - value: Template; - /** The label for the default value if none of the cases match */ - other: Template; + label: { + /** The label for the default value if none of the cases match */ + other: Template; + /** The label for the case being checked */ + case: Template; + }; /** How to describe when a case is checked */ case: Template; }; @@ -292,7 +302,13 @@ type NodeTexts = { Conflicts<{ /** When a conversion is defined somewhere it's not allowed. */ MisplacedConversion: InternalConflictText; - }>; + }> & { + label: { + input: string; + output: string; + expression: string; + }; + }; /** * A conversion expression, e.g., `1 → ''` * Start inputs: $1 = expression to convert @@ -332,10 +348,8 @@ type NodeTexts = { ExpressionText & { /** What to say after inputs are done evaluating, right before starting evaluation the function */ evaluate: Template; - /** Placeholder label for the function */ - function: Template; - /** Placeholder labelf or an unspecified input */ - input: Template; + } & { + label: { function: string; types: string; inputs: string }; } & Conflicts<{ /** * When an input given to this evaluate doesn't match the input of the function being evaluated @@ -387,7 +401,7 @@ type NodeTexts = { * Description inputs: $1: type or undefined */ ExpressionPlaceholder: DescriptiveNodeText & - SimpleExpressionText & { placeholder: Template } & Conflicts<{ + SimpleExpressionText & { label: { placeholder: string } } & Conflicts<{ Placeholder: InternalConflictText; }> & Exceptions<{ @@ -403,7 +417,13 @@ type NodeTexts = { Conflicts<{ /** When a function has no expression */ NoExpression: InternalConflictText; - }>; + }> & { + label: { + inputs: string; + output: string; + expression: string; + }; + }; /** * An internal node used by higher order functions to iterate over a list of values. * Finish inputs: $1 = resulting value @@ -461,11 +481,7 @@ type NodeTexts = { * Description inputs: $1 = item count * Finish inputs: $1 = resulting value */ - ListLiteral: DescriptiveNodeText & - ExpressionText & { - /** Placeholder label for an item in a list */ - item: Template; - }; + ListLiteral: DescriptiveNodeText & ExpressionText; /** * A way of spreading a list's values into a list literal, e.g., `[ [ 1 2 3]… 4 5]` * Description inputs: none @@ -476,8 +492,7 @@ type NodeTexts = { * Finish inputs: $1 = resulting value */ MapLiteral: DescriptiveNodeText & - ExpressionText & - Conflicts<{ + ExpressionText & { label: { values: string } } & Conflicts<{ /** * When something other than a key value pair is given. * Description inputs: $1 = expression that's not a map @@ -500,7 +515,7 @@ type NodeTexts = { * Start inputs: $1 = the stream expression being checked * Finish inputs: $1 = resulting value */ - Previous: NodeText & ExpressionText; + Previous: NodeText & ExpressionText & { label: { range: string } }; /** * A program, e.g., `1 + 1`, `hello()`, etc. * Start inputs: $1 = the stream that caused the evaluation, or nothing @@ -525,33 +540,42 @@ type NodeTexts = { StepLimitException: ExceptionText; /** When a value was expected, but not provided */ ValueException: ExceptionText; - }>; + }> & { + label: { + borrows: string; + expression: string; + }; + }; + /** * Revising a structure with a new value, e.g., `mammal.name: 5` * Description input: $1 = the name being refined * Finish inputs: $1: revised property, $2: revised value */ PropertyBind: DescriptiveNodeText & - ExpressionText & - Conflicts<{ InvalidProperty: ConflictText }>; + ExpressionText & { + label: { property: Template; value: Template }; + } & Conflicts<{ InvalidProperty: ConflictText }>; /** * Getting a structure property, e.g., `mammal.name` * Finish inputs: $1: property name, $2: value */ PropertyReference: DescriptiveNodeText & - ExpressionText & { property: Template }; + ExpressionText & { label: { property: Template } }; /** * Generating a stream of values from other streams, e.g., `a: 1 … ∆ Time() … a + 1` * Finish inputs: $1 = resulting value */ Reaction: NodeText & ExpressionText & { - /** Placeholder label for the initial value */ - initial: Template; - /** Placeholder label for the condition to check */ - condition: Template; - /** Placeholder label for the next value */ - next: Template; + label: { + /** Placeholder label for the initial value */ + initial: Template; + /** Placeholder label for the condition to check */ + condition: Template; + /** Placeholder label for the next value */ + next: Template; + }; } & Conflicts<{ /** When the condition doesn't refer to a strema */ ExpectedStream: InternalConflictText; @@ -642,7 +666,14 @@ type NodeTexts = { * Description inputs: $1 = Interface, $2 = Function */ UnimplementedInterface: InternalConflictText; - }>; + }> & { + label: { + docs: string; + inputs: string; + expression: string; + interfaces: string; + }; + }; StructureDefinitionType: DescriptiveNodeText; /** * A table literal, e.g., `⎡a•# b•#⎦⎡1 2⎦` @@ -654,13 +685,15 @@ type NodeTexts = { * A text literal, e.g., `'hi'` * Description inputs: $1 = the text of the text literal */ - TextLiteral: DescriptiveNodeText & SimpleExpressionText; + TextLiteral: DescriptiveNodeText & + SimpleExpressionText & { label: { texts: string } }; /** * One alternate translation of a text literal, e.g., the `'hola/es`' of `'hi'/en'hola'/es` * Description inputs: $1 = the text */ - Translation: DescriptiveNodeText & - Conflicts<{ + Translation: DescriptiveNodeText & { + label: { segments: string }; + } & Conflicts<{ phone: InternalConflictText; email: InternalConflictText; address: InternalConflictText; @@ -694,7 +727,8 @@ type NodeTexts = { * Description inputs: $1 = the operator * Finish inputs: $1 = resulting value */ - UnaryEvaluate: DescriptiveNodeText & ExpressionText; + UnaryEvaluate: DescriptiveNodeText & + ExpressionText & { label: { input: string } }; /** * An unparsable expression, e.g., `]a[` */ UnparsableExpression: NodeText & diff --git a/src/locale/TermTexts.ts b/src/locale/TermTexts.ts index 55d6aa200..476b50f29 100644 --- a/src/locale/TermTexts.ts +++ b/src/locale/TermTexts.ts @@ -46,6 +46,12 @@ type TermTexts = { boolean: string; /** What to call a table value */ table: string; + /** What to call a table column */ + column: string; + /** What to call a table cell */ + cell: string; + /** What to call a row value */ + row: string; /** What to call a list value */ list: string; /** What to call a map value */ @@ -54,14 +60,16 @@ type TermTexts = { text: string; /** What to call a number value */ number: string; + /** What to call a number unit */ + unit: string; + /** What to call rich text */ + markup: string; /** What to call a function value */ function: string; /** What to call a none value */ none: string; /** What to call an exception value */ exception: string; - /** What to call a row value */ - row: string; /** What to call a set value */ set: string; /** What to call a structure value */ @@ -78,6 +86,12 @@ type TermTexts = { help: string; /** What to call feedback in help/feedback links */ feedback: string; + /** What to call language tags */ + language: string; + /** What to call region tags */ + region: string; + /** What to call documentation */ + documentation: string; }; export { type TermTexts as default }; diff --git a/src/locale/UITexts.ts b/src/locale/UITexts.ts index f630f0596..c8d6ca0dd 100644 --- a/src/locale/UITexts.ts +++ b/src/locale/UITexts.ts @@ -2,8 +2,8 @@ import type { SupportedFace } from '../basis/Fonts'; import type { TileKind } from '../components/project/Tile'; import type { DocText, Template } from './LocaleText'; +import type DocumentationText from '@components/concepts/DocumentationText'; import type CheckpointsText from '@components/project/CheckpointsText'; -import { HowToCategories } from '@concepts/HowTo'; import type ErrorText from '../routes/ErrorText'; import type LandingPageText from '../routes/PageText'; import type AboutPageText from '../routes/about/PageText'; @@ -12,7 +12,7 @@ import type CharactersPageText from '../routes/characters/PageText'; import type DonatePageText from '../routes/donate/PageText'; import type GalleriesPageText from '../routes/galleries/PageText'; import type GalleryPageText from '../routes/gallery/[galleryid]/PageText'; -import type GuidePageText from '../routes/guide/PageText'; +import type { default as GuidePageText } from '../routes/guide/PageText'; import type JoinPageText from '../routes/join/PageText'; import type LearnPageText from '../routes/learn/PageText'; import type LoginPageText from '../routes/login/PageText'; @@ -37,14 +37,16 @@ export type ToggleText = { off: string; }; -export type ModeText = { +export type ModeText = { /** The tooltip and ARIA-label for the entire mode widget, describing the kind of modes it supports switching to. */ label: string; - /** The tooltip and ARIA-labels to use for each mode button describing the mode to be switched to, in the order of appearance */ - modes: Modes; + /** A list of modes */ + labels: Options; + /** A list of tips/aria labels for each option */ + tips: Options; }; -export type DialogText = { +export type HeaderAndExplanationText = { /** The header to be shown at the top of the dialog */ header: string; /** The explanation text just below the header. */ @@ -202,7 +204,7 @@ type UITexts = { /** The header for the save error */ unsaved: Template; /** The content for the translation dialog */ - translate: DialogText; + translate: HeaderAndExplanationText; }; subheader: { /** The header for the source language */ @@ -250,6 +252,8 @@ type UITexts = { show: string; /** How to describe the autocomplete back button for leaving the submenu */ back: string; + /** What to say when the menu is empty */ + empty: string; }; field: { /** The name of the source file */ @@ -370,6 +374,29 @@ type UITexts = { elide: string; /** Large deletion notification */ largeDelete: string; + /** Explanations for why something isn't editable */ + ignored: { + /** The source is not editable */ + readOnly: string; + /** No spaces in block mode unless in editable */ + blockSpace: string; + /** A node couldn't be deleted */ + noDelete: string; + /** An insertion failed */ + noInsert: string; + /** No errors allowed */ + noError: string; + /** No editor active */ + noEditor: string; + /** No clipboard */ + noClipboard: string; + /** No clipboard item */ + noClipboardItem: string; + /** No selection */ + noSelection: string; + /** No where to go */ + noMove: string; + }; }; error: { /** An invalid source name */ @@ -428,49 +455,7 @@ type UITexts = { }; }; /** The documentation browser */ - docs: { - /** The ARIA label for the palette section. */ - label: string; - /** A link to a concept in documentation */ - link: Template; - /** A link to the tutorial for a concept */ - learn: string; - /** Shown if documentation is missing for a concept */ - nodoc: string; - button: { - /** The home button in the docs tile */ - home: string; - /** The back button in the docs tile */ - back: string; - /** The toggle to expand and collapse concept groups */ - toggle: string; - }; - field: { - /** The search text field */ - search: string; - }; - /** Labels for different sections of the guide */ - modes: ModeText<[string, string]>; - header: { - /** Documentation header in structure and functions before inputs */ - inputs: string; - /** Documentation header in structure view before interfaces */ - interfaces: string; - /** Documentation header in structure before properties */ - properties: string; - /** Documentation header in structure before functions */ - functions: string; - /** Documentation header in structure before conversions */ - conversions: string; - }; - /** Everything related to how to content */ - how: { - /** The category names */ - category: Record; - /** The subheader for related how to's */ - related: string; - }; - }; + docs: DocumentationText; /** The project chat */ collaborate: { /** The ARIA label for the chat section */ @@ -666,17 +651,17 @@ type UITexts = { }; dialog: { /** The sharing dialog */ - share: DialogText & { + share: HeaderAndExplanationText & { /** The subheaders of the dialog */ subheader: { /** The gallery subheader and explanation */ - gallery: DialogText; + gallery: HeaderAndExplanationText; /** The public/private toggle subheader and explanation */ - public: DialogText; + public: HeaderAndExplanationText; /** The personal information subheader and explanation */ - pii: DialogText; + pii: HeaderAndExplanationText; /** The copy and paste dialog text */ - copy: DialogText; + copy: HeaderAndExplanationText; }; /** Text fields in the share dialog */ field: { @@ -710,7 +695,7 @@ type UITexts = { }; }; /** The settings dialog */ - settings: DialogText & { + settings: HeaderAndExplanationText & { button: { /** Show the settings dialog */ show: string; @@ -737,6 +722,8 @@ type UITexts = { dark: ModeText<[string, string, string]>; /** The writing layout direction */ writing: ModeText<[string, string, string]>; + /** The blocks on/off mode */ + blocks: ModeText<[string, string]>; /** The space_indicator on/off mode */ space: ModeText<[string, string]>; /** The line number on/off mode */ @@ -754,7 +741,7 @@ type UITexts = { }; }; /** The locale chooser dialog */ - locale: DialogText & { + locale: HeaderAndExplanationText & { /** Subheaders in the local chooser dialog. */ subheader: { /** How to label the locales that have been selected */ @@ -779,7 +766,7 @@ type UITexts = { }; }; /** The keyboard shortcut reference dialog */ - help: DialogText & { + help: HeaderAndExplanationText & { subheader: { moveCursor: string; editCode: string; @@ -788,7 +775,7 @@ type UITexts = { }; }; /** The feedback dialog */ - feedback: DialogText & { + feedback: HeaderAndExplanationText & { button: { /** Show the feedback dialog */ show: string; @@ -898,4 +885,3 @@ type UITexts = { }; export { type UITexts as default }; - diff --git a/src/locale/en-US.json b/src/locale/en-US.json index c6ec73ad3..09dbb71d2 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -31,20 +31,27 @@ "boolean": "boolean", "map": "map", "number": "number", + "unit": "unit", + "markup": "markup", "function": "function", "exception": "exception", "table": "table", + "column": "column", + "row": "row", + "cell": "cell", "none": "none", "list": "list", "stream": "stream", "structure": "structure", "index": "index", "query": "query", - "row": "row", "set": "set", "key": "key", "help": "Help", - "feedback": "feedback" + "feedback": "feedback", + "documentation": "documentation", + "language": "language", + "region": "region" }, "token": { "EvalOpen": "evaluation open", @@ -221,6 +228,7 @@ "Names": { "name": "name list", "emotion": "kind", + "label": { "names": "names" }, "doc": [ "I'm a list of @Name, useful when you want to give a value multiple names, often with different @Language.", "Names are separated by \\,\\ symbols. For example, here's @Bind giving a value multiple @Name", @@ -274,6 +282,7 @@ "TypeVariable": { "name": "type variable", "emotion": "curious", + "label": { "type": "type" }, "doc": "I am a mystery type on @FunctionDefinition or @StructureDefinition, provided by @TypeInputs when either is evaluated. @Set, @List, and @Map use me.", "conflict": { "DuplicateTypeVariable": { @@ -292,6 +301,7 @@ "name": "markup", "description": "$1 paragraphs", "emotion": "serious", + "label": { "paragraphs": "paragraphs" }, "doc": [ "I'm a list of paragraphs, using the many kinds of markup available in explanations, such as @Words, @WebLink, @ConceptLink, and @Example." ] @@ -387,6 +397,9 @@ "\\1 + 2 · 3 + 4\\", "In math, multiplication would come first, and then addition, and so the result would be \\11\\. But since I evaluate in reading order, the result is \\13\\." ], + "label": { + "operator": "operator" + }, "right": "input", "start": "Let's evaluate $1 first", "finish": "Look, I made $1!", @@ -427,6 +440,11 @@ "But we can change this to add the @Is, and now @FunctionDefinition knows that they're numbers:", "\\ƒ sum(a•# b•#) a + b\\" ], + "label": { + "value": "value", + "names": "names", + "type": "type" + }, "start": "Let's see what value we get from $1!", "finish": "Oh nice, I got $1! Let's name it $2", "conflict": { @@ -489,7 +507,9 @@ "So usually I'm just a bunch of @Bind and then an expression at the end.", "\\(\n a: 1\n b: 2\n c: 3\n d: 4\n a + b + c + d\n)\\" ], - "statement": "statement", + "label": { + "statements": "expressions" + }, "start": "First expression", "finish": "Done, I got $1", "conflict": { @@ -519,11 +539,13 @@ "name": "borrow", "description": "borrow $1[$1|missing name]", "emotion": "excited", + "label": { + "source": "source", + "bind": "name", + "version": "version" + }, "doc": "If you create a performance with multiple @Source, you can use me to borrow @Bind that are shared in those other @Source. Just use their name and I'll bring in their name and value.", "start": "Borrowing $2 from $1", - "source": "$source", - "bind": "name", - "version": "version", "conflict": { "UnknownBorrow": { "name": "unknown borrow", @@ -544,6 +566,9 @@ "Changed": { "name": "changed", "emotion": "serious", + "label": { + "stream": "stream" + }, "doc": [ "I check if a stream caused @Program to reevaluate, and make a @Boolean. Like this", "\\∆ Time()\\", @@ -555,6 +580,11 @@ "Conditional": { "name": "conditional", "emotion": "curious", + "label": { + "condition": "condition", + "yes": "yes", + "no": "no" + }, "doc": [ "I think I'm supposed to make decisions? Like this?", "\\number: -100\nnumber < 0 ? 'negative' 'positive'\\", @@ -566,9 +596,6 @@ "else": "$1[jumping over code | not jumping over code]", "afterthen": "done with yes, let's skip no?", "finish": "I guess it's $1?", - "condition": "condition", - "yes": "yes", - "no": "no", "conflict": { "ExpectedBooleanCondition": { "name": "expected boolean condition", @@ -581,6 +608,11 @@ "name": "conversion definition", "description": "$1 → $2", "emotion": "excited", + "label": { + "input": "input type", + "output": "output type", + "expression": "expression" + }, "doc": [ "Dude, I define conversions from one type to another! I go in @Block, someting like this:", "\\→ #kitty #cat . ÷ 2\n6kitty→#cat\\", @@ -649,6 +681,11 @@ "name": "evaluate", "description": "evaluate $1[$1|anonymous]", "emotion": "shy", + "label": { + "function": "function", + "types": "types", + "inputs": "input" + }, "doc": [ "Hi. I evaluate my dearest @FunctionDefinition. Like this:", "\\ƒ greeting(message•'')\ngreeting('kitty')\\", @@ -663,8 +700,6 @@ "start": "Let's evaluate the inputs first", "evaluate": "let's evaluate the function now", "finish": "I evaluated to $1", - "function": "function", - "input": "input", "conflict": { "IncompatibleInput": { "name": "incompatible input", @@ -692,8 +727,8 @@ }, "UnknownInput": { "name": "unknown input", - "primary": "I don't know of an input by this name in $1", - "secondary": "I don't have an input with the name $1" + "primary": "I don't think *$1* accepts an input with my name", + "secondary": "I don't have an input with the name *$1*" }, "InputListMustBeLast": { "name": "extra inputs aren't last", @@ -727,6 +762,7 @@ "name": "placeholder", "description": "$1[$1|placeholder]", "emotion": "scared", + "label": { "placeholder": "expression" }, "doc": [ "I'm an *expression*, but not a real one… I just take the place of one.", "I'm good if you don't know what to write yet. Like this:", @@ -737,7 +773,6 @@ "I don't like being on @Stage!" ], "start": "I don't know what to do here. Can you fill me in?", - "placeholder": "expression", "conflict": { "Placeholder": { "name": "placeholder", @@ -755,6 +790,11 @@ "name": "function", "description": "function $1", "emotion": "kind", + "label": { + "inputs": "inputs", + "output": "output", + "expression": "expression" + }, "doc": [ "Hi again! I take some inputs, then evaluate an expression using them, producing an output.", "Here's a simple example:", @@ -790,7 +830,7 @@ "finish": "I evaluated to $1" }, "Initial": { - "name": "Start", + "name": "start", "emotion": "curious", "doc": [ "I tell you whether the current evaluation of @Program is the first one, evaluating to a @Boolean. For example:", @@ -867,8 +907,7 @@ "emotion": "eager", "doc": "I'm a specific @List of values! See @List to learn more about what you can do with me.", "start": "Let's evaluate the items first", - "finish": "I made a me! $1", - "item": "item" + "finish": "I made a me! $1" }, "Spread": { "name": "spread", @@ -882,6 +921,7 @@ "name": "map literal", "description": "$1 pairing map", "emotion": "kind", + "label": { "values": "pair" }, "doc": "I'm a specific @Map between keys and values. See @Map to learn more about how I'm helpful.", "start": "Let's evaluate the keys and values first", "finish": "I connected everyone, $1", @@ -896,6 +936,10 @@ "Match": { "name": "match", "emotion": "curious", + "label": { + "case": "case", + "other": "default" + }, "doc": [ "I am the most glorious of all conditional checks! I take a value and compares it against any number of cases, and evaluates the corresponding expression that matches.", "For example, if you had a @Number and wanted to convert it to a @Text, you might do something like this:", @@ -906,9 +950,7 @@ ], "start": "Let's see what $1 is...", "case": "Let's check this condition", - "finish": "Okay, we have a final value!", - "value": "value", - "other": "default" + "finish": "Okay, we have a final value!" }, "NumberLiteral": { "name": "number literal", @@ -952,7 +994,7 @@ "doc": [ "Have you ever wanted to remember the past?", "I am the Verse's official record keeper. Give me a stream and a number to look backwards and I'll tell you what that stream's value was in history.", - "For example, here's what @Time it was five ticks ago:", + "For example, here's what @Time was five ticks ago:", "\\← 5 Time(1000ms)\\", "See how it's @None for 5 seconds, then suddenly a previous time?", "If you want the last several values, give me to arrows, and I'll interpret the number as a count:", @@ -961,7 +1003,10 @@ "I'm helpful when you want to create performances that depend on the past." ], "start": "First get $1", - "finish": "Evaluated to stream value $1" + "finish": "Evaluated to stream value $1", + "label": { + "range": "range" + } }, "Program": { "name": "program", @@ -975,6 +1020,10 @@ "If there's a problem during a performance, I'll show that problem.", "And if your performance depends on a *stream*, I'll reevaluate every time that stream changes." ], + "label": { + "borrows": "borrows", + "expression": "block" + }, "unevaluated": "the node you chose didn't evaluate", "start": "$1[$1 stream changed to $2!|It's my first evaluation]", "halt": "encountered exception, stopping", @@ -1003,6 +1052,10 @@ "name": "refine", "description": "refine $1[$1|missing name]", "emotion": "kind", + "label": { + "property": "property", + "value": "value" + }, "doc": [ "Sometimes when you make a @StructureDefinition, you want to change the tiniest thing about it, without having to make a new one with all the same values.", "For example, what if you were keeping a record of cats, but then wanted to create a copy of a cat with a different hobby? I can help you change it:", @@ -1023,18 +1076,25 @@ "name": "property", "description": "property $1[$1|missing name]", "emotion": "kind", + "label": { + "property": "property" + }, "doc": [ "When you make a @StructureDefinition, how do you get one of its inputs? I'm how", "Like if you had a structure about cities, you could get its values with me like this:", "\\•City(name•'' population•#people)\n\nportland: City('Portland' 800000people)\n\nportland.population\\" ], "start": "First let's get the value", - "finish": "Found property $1, it is $2", - "property": "property" + "finish": "Found property $1, it is $2" }, "Reaction": { "name": "reaction", "emotion": "excited", + "label": { + "initial": "initial", + "condition": "condition", + "next": "next" + }, "doc": [ "Streams are so awesome! I can make new ones based on when they change, which is super cool!", "Like, if you wanted @Time to tick, but like, to show words instead of numbers, you could do something like this:", @@ -1044,9 +1104,6 @@ ], "start": "Let's see if we should update the stream", "finish": "The new stream value is $1", - "initial": "initial", - "condition": "condition", - "next": "next", "conflict": { "ExpectedStream": { "name": "expected stream", @@ -1154,6 +1211,12 @@ "name": "structure", "description": "structure $1", "emotion": "kind", + "label": { + "docs": "documentation", + "inputs": "inputs", + "expression": "values and functions", + "interfaces": "interfaces" + }, "doc": [ "Why hello, how are you? Me? I'm great. I love to define structures that store values and function, so as long as I get to do that all day, I'm happy.", "I work like this:", @@ -1205,6 +1268,7 @@ "name": "text literal", "description": "text $1", "emotion": "serious", + "label": { "texts": "text" }, "doc": "I represent one or more specific @Translation of text. See @Text to learn more about what I can do!", "start": "Let's make text in the current locale" }, @@ -1212,6 +1276,7 @@ "name": "translation", "description": "translation $1", "emotion": "serious", + "label": { "segments": "text" }, "doc": "I represent some text, with an optional @Language tag. See @Text to learn more!", "conflict": { "phone": { @@ -1290,6 +1355,7 @@ "name": "unary evaluate", "description": "$1", "emotion": "kind", + "label": { "input": "input" }, "doc": [ "Did you know that when I'm evaluating a @FunctionDefinition with just one value, and the name of the @FunctionDefinition is just a single symbol, you can put the name before the input?", "Like this:", @@ -1416,7 +1482,7 @@ }, "MapType": { "name": "map ", - "description": "map from $1[$1|any] to $2[$2|any]", + "description": "$1[$1|any] to $2[$2|any] map", "emotion": "kind", "doc": [ "Do you know how awesome @Map is? Like really awesome. I tell @Bind what kind of map they are all the time, like this map of numbers to lists:", @@ -1434,7 +1500,7 @@ }, "NameType": { "name": "structure", - "description": "$1 type", + "description": "$1", "emotion": "curious", "doc": [ "I represent a @StructureDefinition by it's name. So like, if you had a structure like this, you could make a @Bind that stores the values it creates.", @@ -4428,7 +4494,8 @@ "menu": { "label": "autocomplete menu", "show": "show autocomplete menu", - "back": "leave submenu" + "back": "leave submenu", + "empty": "No suggestions." }, "cursor": { "priorLine": "move cursor to line before", @@ -4485,7 +4552,19 @@ "search": "search for special characters to insert", "tidy": "tidy spacing", "elide": "toggle elision", - "largeDelete": "Are you sure you want to delete this selection? You can use the undo button (↺) if you change your mind." + "largeDelete": "Are you sure you want to delete this selection? You can use the undo button (↺) if you change your mind.", + "ignored": { + "readOnly": "Source is read only", + "blockSpace": "No tabs allowed here", + "noDelete": "Can't delete this", + "noInsert": "Unable to insert here", + "noError": "This edit would create a conflict", + "noEditor": "No editor active", + "noClipboard": "No clipboard not available", + "noClipboardItem": "Nothing on the clipboard", + "noSelection": "Nothing selected", + "noMove": "Can't move any further" + } }, "error": { "invalidName": "This must be a valid Wordplay name.", @@ -4554,7 +4633,7 @@ } }, "docs": { - "label": "docuemntation browser", + "label": "documentation browser", "link": "show \"$1\" in documentation", "learn": "learn more …", "nodoc": "Who am I? What am I? What is my purpose?", @@ -4567,15 +4646,164 @@ "search": "keywords" }, "header": { - "inputs": "Inputs", - "interfaces": "Interfaces", - "properties": "Properties", - "functions": "Functions", - "conversions": "Conversions" + "inputs": { + "header": "Inputs", + "explanation": "/Required and optional values./" + }, + "interfaces": { + "header": "Interfaces", + "explanation": "/Structures this structure is based on./" + }, + "properties": { + "header": "Properties", + "explanation": "/Values this concept stores./" + }, + "functions": { + "header": "Functions", + "explanation": "/Values this concept can compute./" + }, + "conversions": { + "header": "Conversions", + "explanation": "/Ways to convert this value into values of other types./" + }, + "arrangements": { + "header": "Arrangements", + "explanation": "/Ways of arranging phrases in a group./" + }, + "forms": { + "header": "Forms", + "explanation": "/Shapes that can be placed on stage./" + }, + "appearance": { + "header": "Appearance", + "explanation": "/Ways to change how output looks./" + }, + "animation": { + "header": "Animation", + "explanation": "/Ways to animate output on stage./" + }, + "location": { + "header": "Location", + "explanation": "/Ways of determing where output is on stage./" + }, + "reactions": { + "header": "Reactions", + "explanation": "/Ways of reacting to or analyzing an input stream./" + } }, - "modes": { - "label": "browse", - "modes": ["how to", "concepts"] + "mode": { + "browse": { + "label": "section", + "labels": ["code", "how to"], + "tips": [ + "programming language concepts", + "reusable patterns for making projects" + ] + }, + "purpose": { + "label": "code", + "labels": [ + "project", + "output", + "input", + "decide", + "name", + "text", + "numbers", + "truth", + "lists", + "sets", + "tables", + "docs", + "types", + "etc." + ], + "tips": [ + "project-level concepts", + "phrases, groups, stages, layouts and more", + "time, buttons, keys, scenes, and more", + "choose values based on other values", + "name values, functions, and data structures for later use", + "true, false, and none values", + "numbers and math operations", + "plain and formatted text", + "lists and list operations", + "sets, maps, and operations", + "tables and table operations", + "data types, conversions, and type checks", + "documentation and comments", + "advanced language concepts" + ] + } + }, + "note": { + "empty": "Names you define in your project will show up here." + }, + "purposes": { + "Project": { + "header": "Project", + "explanation": "/Concepts defined in your project./" + }, + "Outputs": { + "header": "Outputs", + "explanation": "/Different types of output that can be placed on stage./" + }, + "Inputs": { + "header": "Inputs", + "explanation": "/Input streams from the world that change over time, causing a program to reevaluate./" + }, + "Decisions": { + "header": "Decisions", + "explanation": "/Ways to choose between different values based on conditions./" + }, + "Definitions": { + "header": "Definitions", + "explanation": "/Ways to name values, functions, and data structures for later use./" + }, + "Numbers": { + "header": "Numbers", + "explanation": "/Numbers, units, and math operations./" + }, + "Text": { + "header": "Text", + "explanation": "/Plain and formatted text, and ways of manipulating them./" + }, + "Truth": { + "header": "Truth", + "explanation": "/True, false, and none values and ways of using them for logic./" + }, + "Lists": { + "header": "Lists", + "explanation": "/Lists and list operations, good for storing values in a particular order./" + }, + "Maps": { + "header": "Maps", + "explanation": "/Sets, maps, and their operations, good for storing collections of values, or mappings from one value to others./" + }, + "Tables": { + "header": "Tables", + "explanation": "/Tables and table operations, good for storing structured data in rows and columns./" + }, + "Types": { + "header": "Types", + "explanation": "/Ways of declaring that named values should have a particular data type and checking that they do./" + }, + "Documentation": { + "header": "Documentation", + "explanation": "/Ways of adding comments and explanations to your code./" + }, + "Advanced": { + "header": "Advanced", + "explanation": "/Advanced language concepts and features that you probably won't need./" + }, + "Hidden": { + "header": "Hidden", + "explanation": "/Concepts that are hidden from the main documentation./" + }, + "How": { + "header": "How to", + "explanation": "/Common patterns for creating different kinds of projects./" + } }, "how": { "category": { @@ -4623,7 +4851,11 @@ "mode": { "public": { "label": "visibility", - "modes": ["private", "public"] + "labels": ["private", "public"], + "tips": [ + "only you and collaborators can see this project", + "anyone can see this project" + ] } }, "error": { @@ -4651,7 +4883,15 @@ "mode": { "layout": { "label": "layout", - "modes": [ + "labels": [ + "auto", + "row", + "stack", + "two", + "one", + "free" + ], + "tips": [ "lay out tiles automatically", "a horizontal screen layout", "a vertical screen layout", @@ -4662,7 +4902,17 @@ }, "animate": { "label": "animations", - "modes": [ + "labels": [ + "off", + "1/4", + "1/3x", + "1/2x", + "1x", + "2x", + "5x", + "10x" + ], + "tips": [ "animations off", "quarter speed", "third speed", @@ -4675,32 +4925,45 @@ }, "dark": { "label": "theme", - "modes": [ - "light colors", - "dark colors", - "use device setting" + "labels": ["light", "dark", "auto"], + "tips": [ + "lighter colors", + "darker colors", + "use the current device setting" ] }, + "blocks": { + "label": "editor", + "labels": ["text", "blocks"], + "tips": ["edit code as text", "edit code as blocks"] + }, "space": { "label": "space indicator", - "modes": [ - "show space and tab indicators explicitly", - "do not show space and tab indicators" + "labels": ["hide", "show"], + "tips": [ + "do not show space and tab indicators", + "show space and tab indicators explicitly" ] }, "lines": { "label": "line numbers", - "modes": [ - "show line numbers in text mode", - "do not show line numbers in text mode" + "labels": ["hide", "show"], + "tips": [ + "do not show line numbers in text mode", + "show line numbers in text mode" ] }, "writing": { "label": "writing layout", - "modes": [ - "horizontal, left to right", - "vertical, right to left", - "vertical, left to right" + "labels": [ + "horizontal", + "vertical rtl", + "vertical ltr" + ], + "tips": [ + "horizontal text, read left to right", + "vertical text, read right to left", + "vertical text, read left to right" ] } }, @@ -4761,8 +5024,12 @@ } }, "mode": { - "label": "feedback ", - "modes": ["defects", "idesa"] + "label": "feedback", + "labels": ["defects", "ideas"], + "tips": [ + "report something that seems broken", + "suggest new features or improvements" + ] }, "field": { "title": { @@ -5191,7 +5458,11 @@ }, "public": { "label": "visibility", - "modes": ["public", "private"] + "labels": ["public", "private"], + "tips": [ + "anyone can see this character", + "only you and collaborators can see this character" + ] }, "collaborators": "collaborators" }, @@ -5206,7 +5477,7 @@ }, "mode": { "label": "mode", - "modes": [ + "labels": [ "select", "eraser", "pixel", @@ -5214,15 +5485,34 @@ "ellipse", "path", "emoji" + ], + "tips": [ + "select shapes and pixels", + "erase shapes and pixels", + "draw pixels", + "draw rectangles", + "draw ellipses", + "draw paths", + "import an emoji as pixels" ] }, "fill": { "label": "fill", - "modes": ["none", "inerit", "choose"] + "labels": ["none", "inerit", "choose"], + "tips": [ + "no fill", + "inherit fill color from surrounding text", + "choose a fill color" + ] }, "stroke": { "label": "stroke", - "modes": ["none", "inerit", "choose"] + "labels": ["none", "inerit", "choose"], + "tips": [ + "no stroke", + "inherit stroke color from surrounding text", + "choose a stroke color" + ] }, "none": "none", "inherit": "text color", diff --git a/src/nodes/AnyType.ts b/src/nodes/AnyType.ts index 0cfc1b83c..df532090e 100644 --- a/src/nodes/AnyType.ts +++ b/src/nodes/AnyType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -14,6 +15,10 @@ export default class AnyType extends Type { return 'AnyType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar() { return []; } diff --git a/src/nodes/BasisType.ts b/src/nodes/BasisType.ts index a8bab0a02..b796771f0 100644 --- a/src/nodes/BasisType.ts +++ b/src/nodes/BasisType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type Context from './Context'; import type Definition from './Definition'; import type Node from './Node'; @@ -8,6 +9,10 @@ export default abstract class BasisType extends Type { super(); } + getPurpose(): Purpose { + return Purpose.Types; + } + /** Override the base class: instead of asking parent for scope (since there is no parent), basis type scopes are their basis structure definitions. */ getScope(context: Context): Node | undefined { return context diff --git a/src/nodes/BinaryEvaluate.ts b/src/nodes/BinaryEvaluate.ts index 567940700..f85554a85 100644 --- a/src/nodes/BinaryEvaluate.ts +++ b/src/nodes/BinaryEvaluate.ts @@ -71,7 +71,7 @@ export default class BinaryEvaluate extends Expression { return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return []; } @@ -89,24 +89,51 @@ export default class BinaryEvaluate extends Expression { name: 'left', kind: node(Expression), // The label comes from the type of left, or the default label from the translation. - label: (locales: Locales, _: Node, context: Context) => () => + label: (locales: Locales, context: Context) => () => this.left.getType(context).getLabel(locales), getType: (context) => this.left.getType(context), }, { name: 'fun', kind: node(Reference), + label: () => (l) => l.node.BinaryEvaluate.label.operator, space: true, indent: true, getDefinitions: (context: Context): Definition[] => { return this.getFunctions(context); }, + /** + * The expected function type of this binary evaluate is whether function it resolves to, but + * concretized with the actual types of the left and right inputs, as that determines what it could be replaced with. + */ + getType: (context) => { + const type = this.getFunction(context)?.getType(context); + if ( + type instanceof FunctionType && + type.inputs.length === 1 + ) { + const newType = FunctionType.make( + type.types, + [ + type.inputs[0].withType( + this.right + .getType(context) + .generalize(context), + ), + ], + type.output, + type.definition, + ); + return newType; + } + return type ?? new AnyType(); + }, }, { name: 'right', kind: node(Expression), // The name of the input from the function, or the translation default - label: (locales: Locales, _: Node, context: Context) => { + label: (locales: Locales, context: Context) => { const fun = this.getFunction(context); return fun ? (_) => locales.getName(fun.inputs[0].names) @@ -134,7 +161,7 @@ export default class BinaryEvaluate extends Expression { } getPurpose() { - return Purpose.Evaluate; + return Purpose.Advanced; } clone(replace?: Replacement) { diff --git a/src/nodes/Bind.ts b/src/nodes/Bind.ts index aef485f13..8247883c6 100644 --- a/src/nodes/Bind.ts +++ b/src/nodes/Bind.ts @@ -6,7 +6,7 @@ import { MisplacedShare } from '@conflicts/MisplacedShare'; import { MissingShareLanguages } from '@conflicts/MissingShareLanguages'; import UnexpectedEtc from '@conflicts/UnexpectedEtc'; import UnusedBind from '@conflicts/UnusedBind'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -40,7 +40,6 @@ import ListType from './ListType'; import type Name from './Name'; import Names from './Names'; import NameType from './NameType'; -import type Node from './Node'; import { any, node, none, type Grammar, type Replacement } from './Node'; import StructureDefinition from './StructureDefinition'; import Sym from './Sym'; @@ -52,7 +51,7 @@ import TypeToken from './TypeToken'; import UnknownType from './UnknownType'; export default class Bind extends Expression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly share: Token | undefined; readonly names: Names; readonly etc: Token | undefined; @@ -73,7 +72,7 @@ export default class Bind extends Expression { ) { super(); - this.docs = docs; + this.docs = docs ?? Docs.make(); this.share = share; this.names = names; this.etc = etc; @@ -112,39 +111,68 @@ export default class Bind extends Expression { ); } - static getPossibleReplacements({ node, context, type }: EditContext) { + static getPossibleReplacements({ + node, + context, + type, + locales, + }: ReplaceContext) { if (node instanceof Expression) { if (type === undefined || node.getParent(context) instanceof Block) return [ - Bind.make(undefined, Names.make(['_']), undefined, node), + Bind.make( + undefined, + Names.make([locales.get((l) => l.term.name)]), + undefined, + node, + ), ]; } } - static getPossibleAppends() { + static getPossibleInsertions({ locales, parent }: InsertContext) { return [ - Bind.make( - undefined, - Names.make(['_']), - undefined, - ExpressionPlaceholder.make(), - ), + parent instanceof StructureDefinition || + parent instanceof FunctionDefinition + ? Bind.make( + undefined, + Names.make([locales.get((l) => l.term.name)]), + TypePlaceholder.make(), + undefined, + ) + : Bind.make( + undefined, + Names.make([locales.get((l) => l.term.name)]), + undefined, + ExpressionPlaceholder.make(), + ), ]; } getGrammar(): Grammar { return [ - { name: 'docs', kind: any(node(Docs), none()) }, + { + name: 'docs', + kind: any(node(Docs), none()), + label: () => (l) => l.term.documentation, + }, { name: 'share', kind: any(node(Sym.Share), none()), getToken: () => new Token(SHARE_SYMBOL, Sym.Share), + label: undefined, + }, + { + name: 'names', + kind: node(Names), + newline: true, + label: () => (l) => l.node.Bind.label.names, }, - { name: 'names', kind: node(Names), newline: true }, { name: 'etc', kind: any(node(Sym.Etc), none()), getToken: () => new Token(ETC_SYMBOL, Sym.Etc), + label: undefined, }, { name: 'dot', @@ -152,10 +180,12 @@ export default class Bind extends Expression { node(Sym.Type), none(['type', () => TypePlaceholder.make()]), ), + label: undefined, }, { name: 'type', kind: any(node(Type), none(['dot', () => new TypeToken()])), + label: () => (l) => l.node.Bind.label.type, }, { name: 'colon', @@ -163,10 +193,12 @@ export default class Bind extends Expression { node(Sym.Bind), none(['value', () => ExpressionPlaceholder.make()]), ), + label: undefined, }, { name: 'value', kind: any( + none(), node(Expression), none(['colon', () => new BindToken()]), ), @@ -174,12 +206,12 @@ export default class Bind extends Expression { indent: true, // The bind field should be whatever type is expected. getType: (context: Context) => this.getExpectedType(context), - label: (locales: Locales, child: Node, context: Context) => { - if (child === this.value) { - const bind = - this.getCorrespondingBindDefinition(context); - return () => (bind ? locales.getName(bind.names) : '_'); - } else return () => '_'; + label: (locales: Locales, context: Context) => { + const bind = this.getCorrespondingBindDefinition(context); + return () => + bind + ? locales.getName(bind.names) + : locales.get((l) => l.node.Bind.label.value); }, }, ]; @@ -264,7 +296,7 @@ export default class Bind extends Expression { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } isEvaluationInvolved() { @@ -450,13 +482,11 @@ export default class Bind extends Expression { } computeType(context: Context): Type { + // Always compute the value's type, as it has side effects on streams. + const valueType = this.value ? this.value.getType(context) : undefined; + // What type is this binding? - let type = - this.getSpecifiedType() ?? // If it has an expression, ask the expression. - (this.value instanceof Expression - ? this.value.getType(context) - : // Otherwise, we don't know, it could be anything. - undefined); + let type = this.getSpecifiedType() ?? valueType; if (type === undefined || type instanceof UnknownType) type = this.getExpectedType(context); @@ -503,6 +533,7 @@ export default class Bind extends Expression { const evalFunc = evaluate.getFunction(context); if ( evalFunc instanceof FunctionDefinition && + funcIndex >= 0 && funcIndex < evalFunc.inputs.length ) { const bind = evalFunc.inputs[funcIndex]; diff --git a/src/nodes/Block.ts b/src/nodes/Block.ts index ab3d08309..d5593b591 100644 --- a/src/nodes/Block.ts +++ b/src/nodes/Block.ts @@ -2,7 +2,7 @@ import type Conflict from '@conflicts/Conflict'; import { ExpectedEndingExpression } from '@conflicts/ExpectedEndingExpression'; import { IgnoredExpression } from '@conflicts/IgnoredExpression'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Evaluation from '@runtime/Evaluation'; @@ -25,9 +25,11 @@ import EvalOpenToken from './EvalOpenToken'; import Expression, { ExpressionKind, type GuardContext } from './Expression'; import ExpressionPlaceholder from './ExpressionPlaceholder'; import FunctionDefinition from './FunctionDefinition'; +import Names from './Names'; import NoExpressionType from './NoExpressionType'; import type Node from './Node'; import { any, list, node, none, type Grammar, type Replacement } from './Node'; +import Reference from './Reference'; import StructureDefinition from './StructureDefinition'; import Sym from './Sym'; import type Token from './Token'; @@ -42,7 +44,7 @@ export enum BlockKind { } export default class Block extends Expression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly open: Token | undefined; readonly statements: Expression[]; readonly close: Token | undefined; @@ -61,12 +63,7 @@ export default class Block extends Expression { this.open = open; this.statements = statements; this.close = close; - this.docs = - docs === undefined - ? undefined - : docs instanceof Docs - ? docs - : new Docs(docs); + this.docs = docs === undefined ? Docs.make([]) : docs; this.kind = kind; this.computeChildren(); @@ -81,13 +78,76 @@ export default class Block extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - // Offer to replace the node parenthesized. - return node instanceof Expression ? [Block.make([node])] : []; - } + static getPossibleReplacements({ node }: ReplaceContext) { + // Offer to parenthesize the node, or add docs if there aren't any. + return node instanceof Expression + ? [ + Block.make([node]), + ...(node instanceof Block && node.docs.isEmpty() + ? [node.withDocs()] + : []), + ] + : []; + } + + static getPossibleInsertions({ + type, + context, + locales, + parent, + index, + }: InsertContext) { + if (parent instanceof StructureDefinition) + return [ + Block.make([ + FunctionDefinition.make( + undefined, + Names.make([locales.get((l) => l.node.Name.name)]), + undefined, + [], + ExpressionPlaceholder.make(), + ), + ]), + ]; - static getPossibleAppends({ type }: EditContext) { - return [Block.make([ExpressionPlaceholder.make(type)])]; + if (!(parent instanceof Block)) return []; + const definitions = [ + ...parent.getDefinitionsInScope(context), + ...(index === undefined ? [] : parent.getDefinitionsBefore(index)), + ]; + return [ + // Offer a block with an expression placeholder of the desired type. + Block.make([ExpressionPlaceholder.make(type)]), + // Offer a bind with the expected type + Bind.make( + undefined, + Names.make(['_']), + undefined, + ExpressionPlaceholder.make(type), + ), + // Offer references to anything in scope, including anything in the parent's scope + // and anything defined prior to the insertion point in this block. + ...definitions + .map((def) => + def instanceof FunctionDefinition + ? def.getEvaluateTemplate( + locales, + context, + false, + undefined, + ) + : def instanceof StructureDefinition + ? def.getEvaluateTemplate(locales, context, true) + : def instanceof Bind + ? Reference.make( + def.names + .getPreferredName(locales.getLocale()) + ?.getName() ?? '_', + ) + : undefined, + ) + .filter((n) => n !== undefined), + ]; } getEvaluationExpression(): Expression { @@ -101,16 +161,21 @@ export default class Block extends Expression { getGrammar(): Grammar { return [ - { name: 'docs', kind: any(node(Docs), none()) }, + { + name: 'docs', + kind: node(Docs), + label: () => (l) => l.term.documentation, + }, { name: 'open', kind: any(node(Sym.EvalOpen), none()), uncompletable: true, + label: undefined, }, { name: 'statements', - kind: list(true, node(Expression), node(Bind)), - label: () => (l) => l.node.Block.statement, + kind: list(this.isRoot(), node(Expression), node(Bind)), + label: () => (l) => l.node.Block.label.statements, indent: !this.isRoot(), newline: this.isRoot() || @@ -121,6 +186,7 @@ export default class Block extends Expression { { name: 'close', kind: any(node(Sym.EvalClose), none()), + label: undefined, // If it's a structure with more than one definition, insert new line newline: this.isStructure() && this.statements.length > 0, uncompletable: true, @@ -129,9 +195,10 @@ export default class Block extends Expression { } getPurpose() { - return Purpose.Evaluate; + return Purpose.Definitions; } + /** If its the root block of a program. */ isRoot() { return this.kind === BlockKind.Root; } @@ -164,6 +231,16 @@ export default class Block extends Expression { ) as this; } + withDocs(docs?: Docs) { + return new Block( + this.statements, + this.kind, + this.open, + this.close, + docs ?? Docs.make(), + ); + } + withStatement(statement: Expression) { return new Block( [...this.statements, statement], @@ -239,6 +316,10 @@ export default class Block extends Expression { // Expose any bind, function, or structures, including on the line that contains this node, to allow them to refer to themselves. // But don't expose any definitions if the node is after the definition. + return this.getDefinitionsBefore(index); + } + + getDefinitionsBefore(index: number): Definition[] { return this.statements.filter( (s, i): s is Bind | FunctionDefinition | StructureDefinition => (s instanceof Bind || diff --git a/src/nodes/BooleanLiteral.ts b/src/nodes/BooleanLiteral.ts index 501d15097..a2fc2adbd 100644 --- a/src/nodes/BooleanLiteral.ts +++ b/src/nodes/BooleanLiteral.ts @@ -1,4 +1,5 @@ -import type EditContext from '@edit/EditContext'; +import Purpose from '@concepts/Purpose'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -8,6 +9,7 @@ import type { BasisTypeName } from '../basis/BasisConstants'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import BooleanType from './BooleanType'; +import Conditional from './Conditional'; import type Context from './Context'; import Literal from './Literal'; import { node, type Grammar, type Replacement } from './Node'; @@ -33,27 +35,41 @@ export default class BooleanLiteral extends Literal { ); } - static getPossibleReplacements({ type }: EditContext) { - // Any type or a boolean? Offer the literals - return type === undefined || type instanceof BooleanType - ? [BooleanLiteral.make(true), BooleanLiteral.make(false)] + static getPossibleReplacements({ node }: ReplaceContext) { + // If the node is true, offer false, and vice versa. + return node instanceof BooleanLiteral + ? [ + node.bool() + ? BooleanLiteral.make(false) + : BooleanLiteral.make(true), + Conditional.make( + node, + BooleanLiteral.make(true), + BooleanLiteral.make(false), + ), + ] : []; } - static getPossibleAppends() { - return BooleanLiteral.make(true); + static getPossibleInsertions() { + return [BooleanLiteral.make(true), BooleanLiteral.make(false)]; } getDescriptor(): NodeDescriptor { return 'BooleanLiteral'; } + getPurpose() { + return Purpose.Truth; + } + getGrammar(): Grammar { return [ { name: 'value', kind: node(Sym.Boolean), getType: () => BooleanType.make(), + label: undefined, }, ]; } diff --git a/src/nodes/BooleanType.ts b/src/nodes/BooleanType.ts index eaf07f484..fe9a959da 100644 --- a/src/nodes/BooleanType.ts +++ b/src/nodes/BooleanType.ts @@ -29,7 +29,7 @@ export default class BooleanType extends BasisType { return [BooleanType.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [BooleanType.make()]; } @@ -38,7 +38,9 @@ export default class BooleanType extends BasisType { } getGrammar(): Grammar { - return [{ name: 'type', kind: node(Sym.BooleanType) }]; + return [ + { name: 'type', kind: node(Sym.BooleanType), label: undefined }, + ]; } clone(replace?: Replacement) { diff --git a/src/nodes/Borrow.ts b/src/nodes/Borrow.ts index 4e24e4a8d..6d30d8d44 100644 --- a/src/nodes/Borrow.ts +++ b/src/nodes/Borrow.ts @@ -82,29 +82,29 @@ export default class Borrow extends SimpleExpression { getGrammar(): Grammar { return [ - { name: 'borrow', kind: node(Sym.Borrow) }, + { name: 'borrow', kind: node(Sym.Borrow), label: undefined }, { name: 'source', kind: any(node(Reference), none()), space: true, - label: () => (l) => l.node.Borrow.source, + label: () => (l) => l.node.Borrow.label.source, }, - { name: 'dot', kind: optional(node(Sym.Access)) }, + { name: 'dot', kind: optional(node(Sym.Access)), label: undefined }, { name: 'name', kind: optional(node(Reference)), - label: () => (l) => l.node.Borrow.name, + label: () => (l) => l.node.Borrow.label.bind, }, { name: 'version', kind: optional(node(Sym.Number)), - label: () => (l) => l.node.Borrow.version, + label: () => (l) => l.node.Borrow.label.version, }, ]; } getPurpose() { - return Purpose.Source; + return Purpose.Advanced; } clone(replace?: Replacement) { diff --git a/src/nodes/Branch.ts b/src/nodes/Branch.ts index c25266bad..94bbbb806 100644 --- a/src/nodes/Branch.ts +++ b/src/nodes/Branch.ts @@ -52,12 +52,20 @@ export default class Branch extends Content { getGrammar(): Grammar { return [ - { name: 'mention', kind: node(Mention) }, - { name: 'open', kind: node(Sym.ListOpen) }, - { name: 'yes', kind: list(true, node(Words)) }, - { name: 'bar', kind: optional(node(Sym.Union)) }, - { name: 'no', kind: list(true, node(Words)) }, - { name: 'close', kind: node(Sym.ListClose) }, + { name: 'mention', kind: node(Mention), label: undefined }, + { name: 'open', kind: node(Sym.ListOpen), label: undefined }, + { + name: 'yes', + kind: list(true, node(Words)), + label: () => (l) => l.term.markup, + }, + { name: 'bar', kind: optional(node(Sym.Union)), label: undefined }, + { + name: 'no', + kind: list(true, node(Words)), + label: () => (l) => l.term.markup, + }, + { name: 'close', kind: node(Sym.ListClose), label: undefined }, ]; } computeConflicts() { @@ -76,7 +84,7 @@ export default class Branch extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.Branch; diff --git a/src/nodes/Changed.ts b/src/nodes/Changed.ts index 38561457f..a073fa557 100644 --- a/src/nodes/Changed.ts +++ b/src/nodes/Changed.ts @@ -1,5 +1,5 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -52,31 +52,28 @@ export default class Changed extends SimpleExpression { getGrammar(): Grammar { return [ - { name: 'change', kind: node(Sym.Change) }, + { name: 'change', kind: node(Sym.Change), label: undefined }, { name: 'stream', kind: node(Expression), space: true, // Must be a stream with any type getType: () => StreamType.make(new AnyType()), + label: () => (l) => l.node.Changed.label.stream, }, ]; } - static getPossibleReplacements({ node, type, context }: EditContext) { - return node instanceof Expression && type instanceof BooleanType - ? [ - Changed.make( - node.getType(context) instanceof StreamType - ? node - : ExpressionPlaceholder.make(StreamType.make()), - ), - ] + static getPossibleReplacements({ type }: ReplaceContext) { + // If a boolean is expected, suggest Changed. + return type instanceof BooleanType + ? [Changed.make(ExpressionPlaceholder.make(StreamType.make()))] : []; } - static getPossibleAppends({ type }: EditContext) { - return type === undefined || type instanceof BooleanType + static getPossibleInsertions({ type }: InsertContext) { + // If a boolean is expected, suggest Changed. + return type instanceof BooleanType ? [Changed.make(ExpressionPlaceholder.make(StreamType.make()))] : []; } @@ -89,7 +86,7 @@ export default class Changed extends SimpleExpression { } getPurpose() { - return Purpose.Decide; + return Purpose.Inputs; } getAffiliatedType(): BasisTypeName | undefined { @@ -97,7 +94,8 @@ export default class Changed extends SimpleExpression { } computeConflicts(context: Context): Conflict[] { - // This will be a value type + // The type of the stream will be the stream's value type of the stream, which doesn't help us verify the expression is a stream. + // Instead, we rely on Context.setStreamType() to be called, to cache the stream type. const valueType = this.stream.getType(context); const streamType = context.getStreamType(valueType); diff --git a/src/nodes/CompositeLiteral.ts b/src/nodes/CompositeLiteral.ts new file mode 100644 index 000000000..9681da5e6 --- /dev/null +++ b/src/nodes/CompositeLiteral.ts @@ -0,0 +1,7 @@ +import Expression from './Expression'; + +export default abstract class CompositeLiteral extends Expression { + constructor() { + super(); + } +} diff --git a/src/nodes/ConceptLink.ts b/src/nodes/ConceptLink.ts index 40502b92a..ed1c8e3b2 100644 --- a/src/nodes/ConceptLink.ts +++ b/src/nodes/ConceptLink.ts @@ -154,7 +154,9 @@ export default class ConceptLink extends Content { } getGrammar(): Field[] { - return [{ name: 'concept', kind: node(Symbol.Concept) }]; + return [ + { name: 'concept', kind: node(Symbol.Concept), label: undefined }, + ]; } clone(replace?: Replacement | undefined): this { @@ -164,7 +166,7 @@ export default class ConceptLink extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } computeConflicts(): Conflict[] { diff --git a/src/nodes/Conditional.ts b/src/nodes/Conditional.ts index 7441f4bae..9d73008c2 100644 --- a/src/nodes/Conditional.ts +++ b/src/nodes/Conditional.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import ExpectedBooleanCondition from '@conflicts/ExpectedBooleanCondition'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -15,7 +15,6 @@ import type Value from '@values/Value'; import Purpose from '../concepts/Purpose'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; -import BooleanLiteral from './BooleanLiteral'; import BooleanType from './BooleanType'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; @@ -58,9 +57,9 @@ export default class Conditional extends Expression { ); } - static getPossibleReplacements({ node, type }: EditContext) { - return node instanceof Expression && - (type === undefined || type instanceof BooleanType) + static getPossibleReplacements({ node, type }: ReplaceContext) { + // A boolean selected? Offer to wrap it in a conditional. + return node instanceof Expression && type instanceof BooleanType ? [ Conditional.make( node, @@ -71,12 +70,8 @@ export default class Conditional extends Expression { : []; } - static getPossibleAppends({ type }: EditContext) { - return Conditional.make( - BooleanLiteral.make(true), - ExpressionPlaceholder.make(type), - ExpressionPlaceholder.make(type), - ); + static getPossibleInsertions() { + return []; } isUndelimited() { @@ -92,22 +87,27 @@ export default class Conditional extends Expression { { name: 'condition', kind: node(Expression), - label: () => (l) => l.node.Conditional.condition, + label: () => (l) => l.node.Conditional.label.condition, // Must be boolean typed getType: () => BooleanType.make(), }, - { name: 'question', kind: node(Sym.Conditional), space: true }, + { + name: 'question', + kind: node(Sym.Conditional), + space: true, + label: undefined, + }, { name: 'yes', kind: node(Expression), - label: () => (l) => l.node.Conditional.yes, + label: () => (l) => l.node.Conditional.label.yes, space: true, indent: true, }, { name: 'no', kind: node(Expression), - label: () => (l) => l.node.Conditional.no, + label: () => (l) => l.node.Conditional.label.no, space: true, indent: true, }, @@ -128,7 +128,7 @@ export default class Conditional extends Expression { } getPurpose() { - return Purpose.Decide; + return Purpose.Decisions; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/Context.ts b/src/nodes/Context.ts index bc05f4101..247196b37 100644 --- a/src/nodes/Context.ts +++ b/src/nodes/Context.ts @@ -6,8 +6,9 @@ import type Node from './Node'; import type PropertyReference from './PropertyReference'; import type Reference from './Reference'; import type Source from './Source'; -import type StreamDefinition from './StreamDefinition'; +import type StreamType from './StreamType'; import type Type from './Type'; +import UnknownType from './UnknownType'; /** Passed around during type inference and conflict detection to facilitate program analysis and cycle-detection. */ export default class Context { @@ -31,7 +32,11 @@ export default class Context { readonly definitions: Map = new Map(); - readonly streamTypes: Map = new Map(); + /** + * Computed types that actually stem from streams. Used by expressions like Changed, Previous, and Reaction, + * which rely on knowing the stream type from which a value type emerged. + */ + readonly streamTypes: Map = new Map(); constructor(project: Project, source: Source) { this.project = project; @@ -62,6 +67,7 @@ export default class Context { getType(node: Expression) { let cache = this.types.get(node); if (cache === undefined) { + // If we visited the node already in this call to getType(), the type depends on itself. if (this.visited(node)) { cache = new CycleType( node, @@ -69,9 +75,19 @@ export default class Context { ); } else { this.visit(node); + // Compute the type. cache = node.computeType(this); this.unvisit(); } + // Cache the type in this context for later, unless it contains a cycle, + // in which case the type will be lazily computed elsewhere. + if ( + !cache + .getTypeSet(this) + .list() + .some((t) => t instanceof UnknownType) + ) + this.types.set(node, cache); } return cache; } @@ -98,11 +114,11 @@ export default class Context { return this.referenceUnions.set(ref, keys); } - setStreamType(type: Type, stream: StreamDefinition) { - this.streamTypes.set(type, stream); + setStreamType(type: Type, streamType: StreamType) { + this.streamTypes.set(type, streamType); } - getStreamType(type: Type): StreamDefinition | undefined { + getStreamType(type: Type): StreamType | undefined { return this.streamTypes.get(type); } } diff --git a/src/nodes/ConversionDefinition.ts b/src/nodes/ConversionDefinition.ts index 92817e01b..3e58203e4 100644 --- a/src/nodes/ConversionDefinition.ts +++ b/src/nodes/ConversionDefinition.ts @@ -31,7 +31,7 @@ import TypePlaceholder from './TypePlaceholder'; import type TypeSet from './TypeSet'; export default class ConversionDefinition extends DefinitionExpression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly arrow: Token; readonly input: Type; readonly output: Type; @@ -46,7 +46,7 @@ export default class ConversionDefinition extends DefinitionExpression { ) { super(); - this.docs = docs; + this.docs = docs ?? Docs.make(); this.arrow = arrow; this.input = input; this.output = output; @@ -74,7 +74,7 @@ export default class ConversionDefinition extends DefinitionExpression { return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [ ConversionDefinition.make( undefined, @@ -96,10 +96,24 @@ export default class ConversionDefinition extends DefinitionExpression { getGrammar(): Grammar { return [ - { name: 'docs', kind: any(node(Docs), none()) }, - { name: 'arrow', kind: node(Sym.Convert) }, - { name: 'input', kind: node(Type), space: true }, - { name: 'output', kind: node(Type), space: true }, + { + name: 'docs', + kind: any(node(Docs), none()), + label: () => (l) => l.term.documentation, + }, + { name: 'arrow', kind: node(Sym.Convert), label: undefined }, + { + name: 'input', + kind: node(Type), + space: true, + label: (locales) => () => this.input.getLabel(locales), + }, + { + name: 'output', + kind: node(Type), + space: true, + label: (locales) => () => this.output.getLabel(locales), + }, { name: 'expression', kind: node(Expression), @@ -107,12 +121,14 @@ export default class ConversionDefinition extends DefinitionExpression { indent: true, // Must match the output type getType: () => this.output, + label: () => (l) => + l.node.ConversionDefinition.label.expression, }, ]; } getPurpose() { - return Purpose.Convert; + return Purpose.Types; } clone(replace?: Replacement) { diff --git a/src/nodes/ConversionType.ts b/src/nodes/ConversionType.ts index 354e84410..26e7b09e9 100644 --- a/src/nodes/ConversionType.ts +++ b/src/nodes/ConversionType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { CONVERT_SYMBOL } from '@parser/Symbols'; @@ -8,47 +9,74 @@ import { node, type Grammar, type Replacement } from './Node'; import Sym from './Sym'; import Token from './Token'; import Type from './Type'; +import TypePlaceholder from './TypePlaceholder'; import type TypeSet from './TypeSet'; export default class ConversionType extends Type { - readonly input: Type; readonly convert: Token; + readonly input: Type; readonly output: Type; - constructor(input: Type, convert: Token, output: Type) { + constructor(convert: Token, input: Type, output: Type) { super(); - this.input = input; this.convert = convert; + this.input = input; this.output = output; this.computeChildren(); } - static make(input: Type, output: Type) { + static make(input?: Type, output?: Type) { return new ConversionType( - input, new Token(CONVERT_SYMBOL, Sym.Convert), - output, + input ?? TypePlaceholder.make(), + output ?? TypePlaceholder.make(), ); } + static getPossibleReplacements() { + return [ConversionType.make()]; + } + + static getPossibleInsertions() { + return [ConversionType.make()]; + } + getDescriptor(): NodeDescriptor { return 'ConversionType'; } + getPurpose(): Purpose { + return Purpose.Advanced; + } + getGrammar(): Grammar { return [ - { name: 'input', kind: node(Type) }, - { name: 'convert', kind: node(Sym.Convert), space: true }, - { name: 'output', kind: node(Type), space: true }, + { + name: 'convert', + kind: node(Sym.Convert), + space: true, + label: undefined, + }, + { + name: 'input', + kind: node(Type), + label: () => (l) => l.term.type, + }, + { + name: 'output', + kind: node(Type), + space: true, + label: () => (l) => l.term.type, + }, ]; } clone(replace?: Replacement) { return new ConversionType( - this.replaceChild('input', this.input, replace), this.replaceChild('convert', this.convert, replace), + this.replaceChild('input', this.input, replace), this.replaceChild('output', this.output, replace), ) as this; } diff --git a/src/nodes/Convert.ts b/src/nodes/Convert.ts index 3167abcf4..e3bc62bdf 100644 --- a/src/nodes/Convert.ts +++ b/src/nodes/Convert.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import { UnknownConversion } from '@conflicts/UnknownConversion'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -23,7 +23,6 @@ import type Context from './Context'; import ConversionDefinition from './ConversionDefinition'; import ConversionType from './ConversionType'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import { getConcreteConversionTypeVariable } from './Generics'; import Names from './Names'; import NameType from './NameType'; @@ -33,7 +32,6 @@ import { NotAType } from './NotAType'; import Sym from './Sym'; import Token from './Token'; import Type from './Type'; -import TypePlaceholder from './TypePlaceholder'; import type TypeSet from './TypeSet'; export default class Convert extends Expression { @@ -59,19 +57,15 @@ export default class Convert extends Expression { ); } - static getPossibleReplacements({ node, type }: EditContext) { - return node instanceof Expression - ? [Convert.make(node, type ?? TypePlaceholder.make())] + static getPossibleReplacements({ node, type }: ReplaceContext) { + // Have an expected type? If so, suggest conversions to that type. + return node instanceof Expression && type !== undefined + ? [Convert.make(node, type)] : []; } - static getPossibleAppends({ type }: EditContext) { - return [ - Convert.make( - ExpressionPlaceholder.make(), - type ?? TypePlaceholder.make(), - ), - ]; + static getPossibleInsertions({ type }: InsertContext) { + return []; } getDescriptor(): NodeDescriptor { @@ -80,14 +74,29 @@ export default class Convert extends Expression { getGrammar(): Grammar { return [ - { name: 'expression', kind: node(Expression) }, - { name: 'convert', kind: node(Sym.Convert), space: true }, - { name: 'type', kind: node(Type), space: true }, + { + name: 'expression', + kind: node(Expression), + label: (locales, context) => () => + this.expression.getType(context).getLabel(locales), + }, + { + name: 'convert', + kind: node(Sym.Convert), + space: true, + label: undefined, + }, + { + name: 'type', + kind: node(Type), + space: true, + label: () => (l) => l.term.type, + }, ]; } getPurpose() { - return Purpose.Convert; + return Purpose.Types; } clone(replace?: Replacement) { diff --git a/src/nodes/Delete.ts b/src/nodes/Delete.ts index 90cac5040..dae80baa8 100644 --- a/src/nodes/Delete.ts +++ b/src/nodes/Delete.ts @@ -1,5 +1,5 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -66,7 +66,12 @@ export default class Delete extends Expression { kind: node(Expression), label: () => (l) => l.term.table, }, - { name: 'del', kind: node(Sym.Delete), space: true }, + { + name: 'del', + kind: node(Sym.Delete), + space: true, + label: undefined, + }, { name: 'query', kind: node(Expression), @@ -78,7 +83,8 @@ export default class Delete extends Expression { ]; } - static getPossibleReplacements({ node, type }: EditContext) { + static getPossibleReplacements({ node, type }: ReplaceContext) { + // Offer to wrap the table expression in a delete. return node instanceof Expression && type instanceof TableType ? [ Delete.make( @@ -89,17 +95,12 @@ export default class Delete extends Expression { : []; } - static getPossibleAppends() { - return [ - Delete.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - ]; + static getPossibleInsertions() { + return []; } getPurpose() { - return Purpose.Value; + return Purpose.Tables; } clone(replace?: Replacement) { diff --git a/src/nodes/Dimension.ts b/src/nodes/Dimension.ts index 93f4da57d..e59bf9a82 100644 --- a/src/nodes/Dimension.ts +++ b/src/nodes/Dimension.ts @@ -1,4 +1,5 @@ -import type EditContext from '@edit/EditContext'; +import { getPossibleDimensions } from '@edit/menu/getPossibleUnits'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { EXPONENT_SYMBOL, PRODUCT_SYMBOL } from '@parser/Symbols'; @@ -45,17 +46,30 @@ export default class Dimension extends Node { ); } - static getPossibleReplacements({ node, type }: EditContext) { - return node instanceof Dimension && type === undefined + static getPossibleReplacements({ node, context }: ReplaceContext) { + // Offer to wrap the dimension in a power. + return node instanceof Dimension ? [ // A power of two ...(node.exponent === undefined ? [node.withPower(2)] : []), ] - : []; + : [ + getPossibleDimensions(context).map((dim) => + Dimension.make(false, dim, -1), + ), + ].flat(); } - static getPossibleAppends() { - return []; + static getPossibleInsertions({ context, index }: InsertContext) { + const dimensions = getPossibleDimensions(context); + return [ + ...dimensions.map((dim) => + Dimension.make(index !== undefined && index > 0, dim, 1), + ), + ...dimensions.map((dim) => + Dimension.make(index !== undefined && index > 0, dim, -1), + ), + ]; } getDescriptor(): NodeDescriptor { @@ -64,14 +78,24 @@ export default class Dimension extends Node { getGrammar(): Grammar { return [ - { name: 'product', kind: any(node(Sym.Operator), none()) }, - { name: 'name', kind: node(Sym.Name), uncompletable: true }, + { + name: 'product', + kind: any(node(Sym.Operator), none()), + label: undefined, + }, + { + name: 'name', + kind: node(Sym.Name), + uncompletable: true, + label: undefined, + }, { name: 'caret', kind: any( node(Sym.Operator), none(['exponent', () => new Token('1', Sym.Number)]), ), + label: undefined, }, { name: 'exponent', @@ -82,6 +106,7 @@ export default class Dimension extends Node { () => new Token(EXPONENT_SYMBOL, Sym.Operator), ]), ), + label: undefined, }, ]; } @@ -133,7 +158,7 @@ export default class Dimension extends Node { } getPurpose() { - return Purpose.Type; + return Purpose.Numbers; } getName() { diff --git a/src/nodes/Doc.ts b/src/nodes/Doc.ts index 779dd4ec6..ff2c31ec8 100644 --- a/src/nodes/Doc.ts +++ b/src/nodes/Doc.ts @@ -1,6 +1,8 @@ import type Conflict from '@conflicts/Conflict'; import { PossiblePII } from '@conflicts/PossiblePII'; +import type { InsertContext } from '@edit/revision/EditContext'; import type LanguageCode from '@locale/LanguageCode'; +import type Locales from '@locale/Locales'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { DOCS_SYMBOL } from '@parser/Symbols'; @@ -12,7 +14,7 @@ import { LanguageTagged } from './LanguageTagged'; import Markup from './Markup'; import type { Grammar, Replacement } from './Node'; import { node, optional } from './Node'; -import type Paragraph from './Paragraph'; +import Paragraph from './Paragraph'; import Sym from './Sym'; import Token from './Token'; import Words from './Words'; @@ -52,12 +54,18 @@ export default class Doc extends LanguageTagged { ); } + static getTemplate(locales: Locales) { + return Doc.make([ + new Paragraph([Words.make(locales.get((l) => l.node.Words.name))]), + ]); + } + static getPossibleReplacements() { return []; } - static getPossibleAppends() { - return [Doc.make()]; + static getPossibleInsertions({ locales }: InsertContext) { + return [Doc.getTemplate(locales)]; } getDescriptor(): NodeDescriptor { @@ -66,11 +74,23 @@ export default class Doc extends LanguageTagged { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.Doc) }, - { name: 'markup', kind: node(Markup) }, - { name: 'close', kind: node(Sym.Doc) }, - { name: 'language', kind: optional(node(Language)) }, - { name: 'separator', kind: optional(node(Sym.Separator)) }, + { name: 'open', kind: node(Sym.Doc), label: undefined }, + { + name: 'markup', + kind: node(Markup), + label: undefined, + }, + { name: 'close', kind: node(Sym.Doc), label: undefined }, + { + name: 'language', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, + { + name: 'separator', + kind: optional(node(Sym.Separator)), + label: undefined, + }, ]; } @@ -85,7 +105,7 @@ export default class Doc extends LanguageTagged { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } withLanguage(language: Language) { diff --git a/src/nodes/Docs.ts b/src/nodes/Docs.ts index aae9d2903..efa147180 100644 --- a/src/nodes/Docs.ts +++ b/src/nodes/Docs.ts @@ -1,3 +1,4 @@ +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LanguageCode from '@locale/LanguageCode'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -20,12 +21,20 @@ export default class Docs extends Node { this.computeChildren(); } - static getPossibleReplacements() { - return []; + static getTemplate(locales: Locales) { + return Docs.make([Doc.getTemplate(locales)]); + } + + static getPossibleReplacements({ locales }: ReplaceContext) { + return Docs.getTemplate(locales); + } + + static getPossibleInsertions({ locales }: InsertContext) { + return Docs.getTemplate(locales); } - static getPossibleAppends() { - return [new Docs([Doc.make()])]; + static make(docs?: Doc[]) { + return new Docs(docs ?? []); } getDescriptor(): NodeDescriptor { @@ -33,7 +42,14 @@ export default class Docs extends Node { } getGrammar(): Grammar { - return [{ name: 'docs', kind: list(false, node(Doc)), newline: true }]; + return [ + { + name: 'docs', + kind: list(true, node(Doc)), + newline: true, + label: () => (l) => l.term.documentation, + }, + ]; } clone(replace?: Replacement) { @@ -42,12 +58,16 @@ export default class Docs extends Node { ) as this; } + isEmpty() { + return this.docs.length === 0; + } + withOption(doc: Doc) { return new Docs([...this.docs, doc]); } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } computeConflicts() { @@ -77,6 +97,12 @@ export default class Docs extends Node { return getPreferred(locales, this.docs); } + getMarkup(locales: Locales) { + return this.docs + .map((doc) => doc.markup.concretize(locales, [])) + .filter((m) => m !== undefined); + } + static readonly LocalePath = (l: LocaleText) => l.node.Docs; getLocalePath() { return Docs.LocalePath; diff --git a/src/nodes/DocumentedExpression.ts b/src/nodes/DocumentedExpression.ts index b1d4e51a6..1380326e1 100644 --- a/src/nodes/DocumentedExpression.ts +++ b/src/nodes/DocumentedExpression.ts @@ -1,3 +1,4 @@ +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type Evaluator from '@runtime/Evaluator'; @@ -33,13 +34,27 @@ export default class DocumentedExpression extends SimpleExpression { getGrammar(): Grammar { return [ - { name: 'docs', kind: node(Docs) }, - { name: 'expression', kind: node(Expression) }, + { name: 'docs', kind: node(Docs), label: undefined }, + { + name: 'expression', + kind: node(Expression), + label: () => (l) => l.term.value, + }, ]; } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; + } + + static getPossibleReplacements({ node, locales }: ReplaceContext) { + return node instanceof Expression + ? new DocumentedExpression(Docs.getTemplate(locales), node) + : []; + } + + static getPossibleInsertions() { + return []; } computeConflicts() { diff --git a/src/nodes/Evaluate.ts b/src/nodes/Evaluate.ts index 02f0db4fc..5a1771d15 100644 --- a/src/nodes/Evaluate.ts +++ b/src/nodes/Evaluate.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import IncompatibleInput from '@conflicts/IncompatibleInput'; import MissingInput from '@conflicts/MissingInput'; @@ -7,7 +8,8 @@ import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; import UnexpectedInput from '@conflicts/UnexpectedInput'; import UnexpectedTypeInput from '@conflicts/UnexpectedTypeInput'; import UnknownInput from '@conflicts/UnknownInput'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; +import Refer from '@edit/revision/Refer'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Bind from '@nodes/Bind'; @@ -26,8 +28,6 @@ import StructureDefinitionValue from '@values/StructureDefinitionValue'; import UnimplementedException from '@values/UnimplementedException'; import type Value from '@values/Value'; import ValueException from '@values/ValueException'; -import Purpose from '../concepts/Purpose'; -import Refer from '../edit/Refer'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import StreamDefinitionValue from '../values/StreamDefinitionValue'; @@ -55,6 +55,7 @@ import PropertyReference from './PropertyReference'; import Reference from './Reference'; import StreamDefinition from './StreamDefinition'; import StreamDefinitionType from './StreamDefinitionType'; +import StreamType from './StreamType'; import StructureDefinition from './StructureDefinition'; import StructureDefinitionType from './StructureDefinitionType'; import StructureType from './StructureType'; @@ -111,11 +112,11 @@ export default class Evaluate extends Expression { static getPossibleEvaluations( expectedType: Type | undefined, - node: Node, + anchor: Node, replace: boolean, context: Context, ) { - const nodeBeingReplaced = replace ? node : undefined; + const nodeBeingReplaced = replace ? anchor : undefined; // Given the node the caret has selected or is after, find out // if there's an evaluate on it that we should complete. @@ -123,27 +124,29 @@ export default class Evaluate extends Expression { nodeBeingReplaced instanceof Expression ? nodeBeingReplaced.getType(context) : undefined; - const structure = - scopingType instanceof BasisType || - scopingType instanceof StructureType; + + // All the definitions outside the given node. + const functionsInScope = anchor.getDefinitionsInScope(context) ?? []; + + // All the functions inside the given node's internal scope. + const structureFunctions = nodeBeingReplaced + ? // If the scope is basis, get definitions in basis scope + scopingType instanceof BasisType + ? scopingType.getDefinitions(nodeBeingReplaced, context) + : // If the scope is a structure, get definitions in its scope + scopingType instanceof StructureType + ? scopingType.definition.getDefinitions(nodeBeingReplaced) + : // Otherwise, nothing extra + [] + : []; + // Get the definitions in the structure type we found, - // or in the surrounding scope if there isn't one. - const definitions = + // and in the surrounding scope. + const definitions = [ + ...functionsInScope, // If the anchor is selected for replacement... - nodeBeingReplaced - ? // If the scope is basis, get definitions in basis scope - scopingType instanceof BasisType - ? scopingType.getDefinitions(nodeBeingReplaced, context) - : // If the scope is a structure, get definitions in its scope - scopingType instanceof StructureType - ? scopingType.definition.getDefinitions(nodeBeingReplaced) - : // Otherwise, get definitions in scope of the anchor - node.getDefinitionsInScope(context) - : // If the node is not selected, get definitions in the anchor's scope - node.getDefinitionsInScope(context); - - // This probably doesn't belong here. The expected type is the expected type, and it should be correct. - // if (!isBeingReplaced && structure) expectedType = undefined; + ...structureFunctions, + ]; // Convert the definitions to evaluate suggestions. return definitions @@ -157,7 +160,27 @@ export default class Evaluate extends Expression { (def instanceof FunctionDefinition && (expectedType === undefined || expectedType.accepts( - def.getOutputType(context), + def.getOutputType( + context, + // If it's a binary evaluate, we pass a hypothetical evaluate so + // the output type of the function we get includes any inherited units for + // number types. + def.isBinary() + ? def.getEvaluateTemplate( + def.names.getNames()[0], + context, + true, + replace && + structureFunctions.includes( + def, + ) && + nodeBeingReplaced instanceof + Expression + ? nodeBeingReplaced + : undefined, + ) + : undefined, + ), context, ))) || (def instanceof StructureDefinition && @@ -167,37 +190,58 @@ export default class Evaluate extends Expression { def.getType(context), context, ))) || + // If its a stream and the expected type matches the stream's type, + // or it's a stream type and the stream output matches the expected type. (def instanceof StreamDefinition && (expectedType === undefined || - expectedType.accepts( - def.getType(context), - context, - ))), + expectedType.accepts(def.output, context) || + (expectedType instanceof StreamType && + expectedType.type.accepts( + def.output, + context, + )))), ) - .map( - (def) => - new Refer( - (name) => - def.getEvaluateTemplate( - name, - context, - replace && - structure && - nodeBeingReplaced instanceof Expression - ? nodeBeingReplaced - : undefined, - ), - def, - ), - ); + .map((def) => { + const type = + replace && + structureFunctions.includes(def) && + nodeBeingReplaced instanceof Expression + ? nodeBeingReplaced + : undefined; + const defaultTemplate = new Refer( + (name) => + def.getEvaluateTemplate(name, context, true, type), + def, + ); + return def instanceof FunctionDefinition && + def.isOptionalUnary() + ? [ + new Refer( + (name) => + def.getEvaluateTemplate( + name, + context, + true, + type, + true, + ), + def, + true, + true, + ), + defaultTemplate, + ] + : [defaultTemplate]; + }) + .flat(); } - static getPossibleReplacements({ node, type, context }: EditContext) { + static getPossibleReplacements({ node, type, context }: ReplaceContext) { return this.getPossibleEvaluations(type, node, true, context); } - static getPossibleAppends({ node, type, context }: EditContext) { - return this.getPossibleEvaluations(type, node, false, context); + static getPossibleInsertions({ type, parent, context }: InsertContext) { + return this.getPossibleEvaluations(type, parent, false, context); } getDescriptor(): NodeDescriptor { @@ -221,19 +265,31 @@ export default class Evaluate extends Expression { ), new AnyType(), ), - label: () => (l) => l.node.Evaluate.function, + label: () => (l) => l.node.Evaluate.label.function, + }, + { + name: 'types', + kind: any(node(TypeInputs), none()), + label: () => (l) => l.node.Evaluate.label.types, }, - { name: 'types', kind: any(node(TypeInputs), none()) }, - { name: 'open', kind: node(Sym.EvalOpen) }, + { name: 'open', kind: node(Sym.EvalOpen), label: undefined }, { name: 'inputs', kind: list(true, node(Input), node(Expression)), - label: (locales: Locales, child: Node, context: Context) => { + label: ( + locales: Locales, + context: Context, + index: number | undefined, + ) => { + if (index === undefined) + return (l) => l.node.Evaluate.label.inputs; + const child = this.inputs[index]; + // Get the function called const fun = this.getFunction(context); // Didn't find it? Default label. if (fun === undefined || !(child instanceof Expression)) - return (l) => l.node.Evaluate.input; + return (l) => l.node.Evaluate.label.inputs; // Get the mapping from inputs to binds const mapping = this.getInputMapping(context); // Find the bind to which this child was mapped and get its translation of this language. @@ -245,7 +301,7 @@ export default class Evaluate extends Expression { m.given.includes(child))), ); return bind === undefined - ? (l) => l.node.Evaluate.input + ? (l) => l.node.Evaluate.label.inputs : () => locales.getName(bind.expected.names); }, space: true, @@ -262,15 +318,20 @@ export default class Evaluate extends Expression { Math.max(0, index ?? 0), fun.inputs.length - 1, ); + if ( + insertionIndex < 0 || + insertionIndex >= fun.inputs.length + ) + return new NeverType(); return fun.inputs[insertionIndex].getType(context); }, }, - { name: 'close', kind: node(Sym.EvalClose) }, + { name: 'close', kind: node(Sym.EvalClose), label: undefined }, ]; } getPurpose() { - return Purpose.Evaluate; + return Purpose.Definitions; } clone(replace?: Replacement) { @@ -543,7 +604,11 @@ export default class Evaluate extends Expression { lastType instanceof ListType && expectedType instanceof ListType && (lastType.type === undefined || - expectedType.accepts(lastType.type, context)) + expectedType.type === undefined || + expectedType.type.accepts( + lastType.type, + context, + )) ) isVariableListInput = true; } @@ -731,7 +796,10 @@ export default class Evaluate extends Expression { ); } else if (fun instanceof StreamDefinition) { // Remember that this type came from this definition. - context.setStreamType(fun.output, fun); + context.setStreamType( + fun.output, + StreamType.make(fun.getType(context)), + ); // Return the type of this stream's output. return fun.output; } diff --git a/src/nodes/Example.ts b/src/nodes/Example.ts index db02e11e6..1d22631a4 100644 --- a/src/nodes/Example.ts +++ b/src/nodes/Example.ts @@ -1,11 +1,11 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Purpose from '../concepts/Purpose'; import Characters from '../lore/BasisCharacters'; import { CODE_SYMBOL } from '../parser/Symbols'; import Content from './Content'; +import ExpressionPlaceholder from './ExpressionPlaceholder'; import { node, type Grammar, type Replacement } from './Node'; import Program from './Program'; import Sym from './Sym'; @@ -32,12 +32,12 @@ export default class Example extends Content { ); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Content ? [Example.make(Program.make())] : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { - return [Example.make(Program.make())]; + static getPossibleInsertions() { + return [Example.make(Program.make([ExpressionPlaceholder.make()]))]; } getDescriptor(): NodeDescriptor { @@ -46,9 +46,9 @@ export default class Example extends Content { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.Code) }, - { name: 'program', kind: node(Program) }, - { name: 'close', kind: node(Sym.Code) }, + { name: 'open', kind: node(Sym.Code), label: undefined }, + { name: 'program', kind: node(Program), label: undefined }, + { name: 'close', kind: node(Sym.Code), label: undefined }, ]; } @@ -65,7 +65,7 @@ export default class Example extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.Example; diff --git a/src/nodes/ExceptionType.ts b/src/nodes/ExceptionType.ts index 49b2c601e..062ebacf8 100644 --- a/src/nodes/ExceptionType.ts +++ b/src/nodes/ExceptionType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type ExceptionValue from '@values/ExceptionValue'; @@ -20,6 +21,10 @@ export default class ExceptionType extends Type { return 'ExceptionType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar() { return []; } diff --git a/src/nodes/ExpressionPlaceholder.ts b/src/nodes/ExpressionPlaceholder.ts index 56f490665..1e9834fd3 100644 --- a/src/nodes/ExpressionPlaceholder.ts +++ b/src/nodes/ExpressionPlaceholder.ts @@ -1,8 +1,9 @@ import type Conflict from '@conflicts/Conflict'; import Placeholder from '@conflicts/Placeholder'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext } from '@edit/revision/EditContext'; import type { LocaleText } from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; +import { TYPE_SYMBOL } from '@parser/Symbols'; import type Evaluator from '@runtime/Evaluator'; import Halt from '@runtime/Halt'; import type Step from '@runtime/Step'; @@ -12,6 +13,7 @@ import Purpose from '../concepts/Purpose'; import type Locales from '../locale/Locales'; import NodeRef from '../locale/NodeRef'; import Characters from '../lore/BasisCharacters'; +import AnyType from './AnyType'; import BinaryEvaluate from './BinaryEvaluate'; import Bind from './Bind'; import type Context from './Context'; @@ -25,13 +27,13 @@ import { any, node, none, type Grammar, type Replacement } from './Node'; import PlaceholderToken from './PlaceholderToken'; import type Root from './Root'; import SimpleExpression from './SimpleExpression'; +import StreamType from './StreamType'; import Sym from './Sym'; -import type Token from './Token'; +import Token from './Token'; import Type from './Type'; import TypePlaceholder from './TypePlaceholder'; import type TypeSet from './TypeSet'; import TypeToken from './TypeToken'; -import UnimplementedType from './UnimplementedType'; export default class ExpressionPlaceholder extends SimpleExpression { readonly placeholder: Token | undefined; @@ -61,11 +63,11 @@ export default class ExpressionPlaceholder extends SimpleExpression { ); } - static getPossibleReplacements({ type }: EditContext) { - return [ExpressionPlaceholder.make(type)]; + static getPossibleReplacements() { + return []; } - static getPossibleAppends({ type }: EditContext) { + static getPossibleInsertions({ type }: InsertContext) { return [ExpressionPlaceholder.make(type)]; } @@ -78,12 +80,7 @@ export default class ExpressionPlaceholder extends SimpleExpression { { name: 'placeholder', kind: node(Sym.Placeholder), - label: ( - locales: Locales, - _: Node, - context: Context, - root: Root, - ) => { + label: (locales: Locales, context: Context, _, root: Root) => { const parent: Node | undefined = root.getParent(this); // See if the parent has a label. return ( @@ -94,7 +91,7 @@ export default class ExpressionPlaceholder extends SimpleExpression { root, ) ?? ((l: LocaleText) => - l.node.ExpressionPlaceholder.placeholder) + l.node.ExpressionPlaceholder.label.placeholder) ); }, }, @@ -104,16 +101,18 @@ export default class ExpressionPlaceholder extends SimpleExpression { node(Sym.Type), none(['type', () => TypePlaceholder.make()]), ), + label: undefined, }, { name: 'type', kind: any(node(Type), none(['dot', () => new TypeToken()])), + label: undefined, }, ]; } getPurpose() { - return Purpose.Evaluate; + return Purpose.Advanced; } clone(replace?: Replacement) { @@ -129,6 +128,11 @@ export default class ExpressionPlaceholder extends SimpleExpression { } computeType(context: Context): Type { + // If it is a stream type, set the stream type in the context, so that other expressions like Changed + // know what it is. + if (this.type instanceof StreamType) + context.setStreamType(this.type, StreamType.make(this.type.type)); + // Is the type given? Return it. if (this.type) return this.type; @@ -173,7 +177,7 @@ export default class ExpressionPlaceholder extends SimpleExpression { if (parent.output) return parent.output; } - return this.type ?? new UnimplementedType(this); + return new AnyType(); } isPlaceholder() { @@ -193,6 +197,14 @@ export default class ExpressionPlaceholder extends SimpleExpression { ]; } + withType(type: Type | undefined) { + return new ExpressionPlaceholder( + this.placeholder, + new Token(TYPE_SYMBOL, Sym.Type), + type, + ); + } + evaluate(evaluator: Evaluator, prior: Value | undefined): Value { if (prior) return prior; return new UnimplementedException(evaluator, this); diff --git a/src/nodes/FieldPosition.ts b/src/nodes/FieldPosition.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/nodes/FormattedLiteral.ts b/src/nodes/FormattedLiteral.ts index 61b27d75f..0af2fe67b 100644 --- a/src/nodes/FormattedLiteral.ts +++ b/src/nodes/FormattedLiteral.ts @@ -1,4 +1,4 @@ -import type EditContext from '@edit/EditContext'; +import type { InsertContext } from '@edit/revision/EditContext'; import type LanguageCode from '@locale/LanguageCode'; import type Locale from '@locale/Locale'; import type LocaleText from '@locale/LocaleText'; @@ -39,14 +39,18 @@ export default class FormattedLiteral extends Literal { this.computeChildren(); } - static getPossibleReplacements({ type, context }: EditContext) { + static getPossibleReplacements() { + return []; + } + + static getPossibleInsertions({ type, context }: InsertContext) { return type !== undefined && type.accepts(FormattedType.make(), context) ? [new FormattedLiteral([FormattedTranslation.make()])] : []; } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); + static make(texts: FormattedTranslation[]) { + return new FormattedLiteral(texts); } getDescriptor(): NodeDescriptor { @@ -55,7 +59,11 @@ export default class FormattedLiteral extends Literal { getGrammar(): Grammar { return [ - { name: 'texts', kind: list(false, node(FormattedTranslation)) }, + { + name: 'texts', + kind: list(false, node(FormattedTranslation)), + label: () => (l) => l.term.markup, + }, ]; } @@ -70,7 +78,7 @@ export default class FormattedLiteral extends Literal { } getPurpose() { - return Purpose.Value; + return Purpose.Text; } getOptions() { diff --git a/src/nodes/FormattedTranslation.ts b/src/nodes/FormattedTranslation.ts index 0015c56e6..7108a2c12 100644 --- a/src/nodes/FormattedTranslation.ts +++ b/src/nodes/FormattedTranslation.ts @@ -56,7 +56,7 @@ export default class FormattedTranslation extends LanguageTagged { return [FormattedTranslation.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return this.getPossibleReplacements(); } @@ -74,11 +74,19 @@ export default class FormattedTranslation extends LanguageTagged { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.Formatted) }, - { name: 'markup', kind: node(Markup) }, - { name: 'close', kind: node(Sym.Formatted) }, - { name: 'language', kind: optional(node(Language)) }, - { name: 'separator', kind: optional(node(Sym.Separator)) }, + { name: 'open', kind: node(Sym.Formatted), label: undefined }, + { name: 'markup', kind: node(Markup), label: undefined }, + { name: 'close', kind: node(Sym.Formatted), label: undefined }, + { + name: 'language', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, + { + name: 'separator', + kind: optional(node(Sym.Separator)), + label: undefined, + }, ]; } @@ -93,7 +101,7 @@ export default class FormattedTranslation extends LanguageTagged { } getPurpose() { - return Purpose.Value; + return Purpose.Text; } withLanguage(language: Language) { diff --git a/src/nodes/FormattedType.ts b/src/nodes/FormattedType.ts index ab3d459fc..d03150ea7 100644 --- a/src/nodes/FormattedType.ts +++ b/src/nodes/FormattedType.ts @@ -29,7 +29,7 @@ export default class FormattedType extends BasisType { } getGrammar(): Grammar { - return [{ name: 'tick', kind: node(Sym.Doc) }]; + return [{ name: 'tick', kind: node(Sym.Doc), label: undefined }]; } acceptsAll(types: TypeSet): boolean { diff --git a/src/nodes/FunctionDefinition.test.ts b/src/nodes/FunctionDefinition.test.ts index a426e8745..860c94076 100644 --- a/src/nodes/FunctionDefinition.test.ts +++ b/src/nodes/FunctionDefinition.test.ts @@ -24,7 +24,7 @@ test.each([ }, ); -test('Test text functions', () => { +test('Test evaluation limit', () => { expect(evaluateCode('ƒ a() a() a()')).toBeInstanceOf( EvaluationLimitException, ); diff --git a/src/nodes/FunctionDefinition.ts b/src/nodes/FunctionDefinition.ts index 4589241fb..377649098 100644 --- a/src/nodes/FunctionDefinition.ts +++ b/src/nodes/FunctionDefinition.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import NoExpression from '@conflicts/NoExpression'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { FUNCTION_SYMBOL, SHARE_SYMBOL } from '@parser/Symbols'; @@ -15,6 +15,7 @@ import Purpose from '../concepts/Purpose'; import IncompatibleType from '../conflicts/IncompatibleType'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; +import AnyType from './AnyType'; import BinaryEvaluate from './BinaryEvaluate'; import Bind from './Bind'; import type Context from './Context'; @@ -31,9 +32,9 @@ import Names from './Names'; import NameType from './NameType'; import type Node from './Node'; import { any, list, node, none, type Grammar, type Replacement } from './Node'; +import NumberType from './NumberType'; import PropertyReference from './PropertyReference'; import Reference from './Reference'; -import StructureDefinition from './StructureDefinition'; import Sym from './Sym'; import Token from './Token'; import Type from './Type'; @@ -42,11 +43,11 @@ import type TypeSet from './TypeSet'; import TypeToken from './TypeToken'; import TypeVariables from './TypeVariables'; import UnaryEvaluate from './UnaryEvaluate'; -import UnimplementedType from './UnimplementedType'; +import Unit from './Unit'; import { getEvaluationInputConflicts } from './util'; export default class FunctionDefinition extends DefinitionExpression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly share: Token | undefined; readonly fun: Token; readonly names: Names; @@ -73,7 +74,7 @@ export default class FunctionDefinition extends DefinitionExpression { ) { super(); - this.docs = docs; + this.docs = docs ?? Docs.make(); this.share = share; this.names = names; this.fun = fun; @@ -112,17 +113,18 @@ export default class FunctionDefinition extends DefinitionExpression { ); } - static getPossibleReplacements({ type, context }: EditContext) { - return type instanceof FunctionType + static getPossibleReplacements({ node, type, context }: ReplaceContext) { + return node instanceof ExpressionPlaceholder && + type instanceof FunctionType ? [type.getDefaultExpression(context)] : []; } - static getPossibleAppends() { + static getPossibleInsertions({ locales }: InsertContext) { return [ FunctionDefinition.make( undefined, - Names.make(['_']), + Names.make([locales.get((l) => l.term.name)]), undefined, [], ExpressionPlaceholder.make(), @@ -135,56 +137,116 @@ export default class FunctionDefinition extends DefinitionExpression { return 'FunctionDefinition'; } + isOptionalUnary() { + return ( + this.inputs.length === 1 && this.inputs.every((i) => i.hasDefault()) + ); + } + /** Create an expression that evaluates this function with typed placeholders for its inputs. */ getEvaluateTemplate( nameOrLocales: Locales | string, context: Context, + defaults: boolean, + /** The structure to call the function on, or the type it should be called on */ structureType: Expression | Type | undefined, - ) { - const possibleStructure = context.getRoot(this)?.getParent(this); - const structure = structureType - ? structureType - : possibleStructure instanceof StructureDefinition - ? possibleStructure - : undefined; - const reference = Reference.make( + // If it can be unary, returns a unary evaluate. + unary: boolean = false, + ): Evaluate | UnaryEvaluate | BinaryEvaluate { + const name = typeof nameOrLocales === 'string' ? nameOrLocales - : nameOrLocales.getName(this.names), - this, - ); + : nameOrLocales.getName(this.names); + const fun = + structureType instanceof Reference || + structureType instanceof PropertyReference + ? structureType + : structureType instanceof Expression + ? PropertyReference.make(structureType, Reference.make(name)) + : structureType instanceof Type + ? PropertyReference.make( + ExpressionPlaceholder.make(structureType), + Reference.make(name), + ) + : Reference.make(name); + + const structure = + structureType instanceof Expression + ? structureType + : ExpressionPlaceholder.make(structureType?.clone()); + + // The first number type input with a unit deriver, if there is one. + const unitDeriver = + this.inputs.length > 0 + ? this.inputs[0] + .getType(context) + .getPossibleTypes(context) + .find( + (t): t is NumberType => + t instanceof NumberType && + t.unit instanceof Function, + )?.unit + : undefined; + + const unitStructure = structure.getType(context); + return this.isOperator() && this.inputs.length === 0 ? new UnaryEvaluate( new Reference( new Token(this.getOperatorName() ?? '_', Sym.Operator), ), - structureType instanceof Expression - ? structureType - : ExpressionPlaceholder.make(structureType?.clone()), + structure, ) : this.isOperator() && this.inputs.length === 1 - ? new BinaryEvaluate( - structureType instanceof Expression - ? structureType - : ExpressionPlaceholder.make(structureType), - Reference.make(this.getOperatorName() ?? '_'), - ExpressionPlaceholder.make(), - ) + ? unary && this.isOptionalUnary() + ? new UnaryEvaluate( + new Reference( + new Token( + this.getOperatorName() ?? '_', + Sym.Operator, + ), + ), + structure, + ) + : new BinaryEvaluate( + structure, + new Reference( + new Token( + this.getOperatorName() ?? '_', + Sym.Operator, + ), + ), + ExpressionPlaceholder.make( + unitDeriver instanceof Function && + unitStructure instanceof NumberType && + unitStructure.unit instanceof Unit + ? NumberType.make( + unitDeriver( + unitStructure.unit, + undefined, + undefined, + ), + ) + : this.inputs[0].getType(context)?.clone(), + ), + ) : Evaluate.make( - structure - ? PropertyReference.make( - structureType instanceof Expression - ? structureType - : ExpressionPlaceholder.make(structureType), - reference, - ) - : reference, + fun, this.inputs .filter((input) => !input.hasDefault()) .map((input) => input.type - ? (input.type.getDefaultExpression(context) ?? - ExpressionPlaceholder.make(input.type)) + ? // Always generate a default for function types. Placeholders are gnarly! + defaults || input.type instanceof FunctionType + ? (input + .getType(context) + .getDefaultExpression(context) ?? + ExpressionPlaceholder.make( + input.getType(context).clone(), + )) + : ExpressionPlaceholder.make( + input.getType(context).clone(), + ) : ExpressionPlaceholder.make(), ), ); @@ -192,41 +254,54 @@ export default class FunctionDefinition extends DefinitionExpression { getGrammar(): Grammar { return [ - { name: 'docs', kind: any(node(Docs), none()) }, + { + name: 'docs', + kind: any(node(Docs), none()), + label: () => (l) => l.term.documentation, + }, { name: 'share', kind: any(node(Sym.Share), none()), getToken: () => new Token(SHARE_SYMBOL, Sym.Share), + label: undefined, + }, + { name: 'fun', kind: node(Sym.Function), label: undefined }, + { name: 'names', kind: node(Names), space: true, label: undefined }, + { + name: 'types', + kind: any(node(TypeVariables), none()), + label: undefined, }, - { name: 'fun', kind: node(Sym.Function) }, - { name: 'names', kind: node(Names), space: true }, - { name: 'types', kind: any(node(TypeVariables), none()) }, - { name: 'open', kind: node(Sym.EvalOpen) }, + { name: 'open', kind: node(Sym.EvalOpen), label: undefined }, { name: 'inputs', kind: list(true, node(Bind)), space: true, indent: true, + label: () => (l) => l.node.FunctionDefinition.label.inputs, }, - { name: 'close', kind: node(Sym.EvalClose) }, + { name: 'close', kind: node(Sym.EvalClose), label: undefined }, { name: 'dot', kind: any( node(Sym.Type), none(['output', () => TypePlaceholder.make()]), ), + label: undefined, }, { name: 'output', kind: any(node(Type), none(['dot', () => new TypeToken()])), + label: () => (l) => l.node.FunctionDefinition.label.output, }, { name: 'expression', - kind: any(node(Expression), node(Sym.Etc), none()), + kind: any(node(Expression), none()), space: true, indent: true, // Must match output type if provided getType: (context) => this.getOutputType(context), + label: () => (l) => l.node.FunctionDefinition.label.expression, }, ]; } @@ -237,7 +312,7 @@ export default class FunctionDefinition extends DefinitionExpression { } getPurpose() { - return Purpose.Evaluate; + return Purpose.Definitions; } clone(replace?: Replacement) { @@ -375,11 +450,21 @@ export default class FunctionDefinition extends DefinitionExpression { ); } - getOutputType(context: Context) { - return this.output instanceof Type - ? this.output + getOutputType( + context: Context, + caller: + | BinaryEvaluate + | UnaryEvaluate + | Evaluate + | undefined = undefined, + ): Type { + return this.output !== undefined + ? // If it's a number type, and we received a caller, pass it, so we can infer the units. + this.output instanceof NumberType && caller !== undefined + ? this.output.withOp(caller) + : this.output : this.expression === undefined - ? new UnimplementedType(this) + ? new AnyType() : this.expression.getType(context); } diff --git a/src/nodes/FunctionType.ts b/src/nodes/FunctionType.ts index 2e163c6e3..34d0f884b 100644 --- a/src/nodes/FunctionType.ts +++ b/src/nodes/FunctionType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { FUNCTION_SYMBOL } from '@parser/Symbols'; @@ -77,7 +78,7 @@ export default class FunctionType extends Type { return [FunctionType.make(undefined, [], TypePlaceholder.make())]; } - static getPossibleAppends() { + static getPossibleInsertions() { return this.getPossibleReplacements(); } @@ -85,6 +86,10 @@ export default class FunctionType extends Type { return 'FunctionType'; } + getPurpose(): Purpose { + return Purpose.Types; + } + getTemplate(context: Context): FunctionDefinition { return FunctionDefinition.make( undefined, @@ -99,17 +104,28 @@ export default class FunctionType extends Type { getGrammar(): Grammar { return [ - { name: 'fun', kind: node(Sym.Function) }, - { name: 'types', kind: optional(node(TypeVariables)), space: true }, - { name: 'open', kind: node(Sym.EvalOpen) }, + { name: 'fun', kind: node(Sym.Function), label: undefined }, + { + name: 'types', + kind: optional(node(TypeVariables)), + space: true, + label: undefined, + }, + { name: 'open', kind: node(Sym.EvalOpen), label: undefined }, { name: 'inputs', kind: list(true, node(Bind)), space: true, indent: true, + label: () => (l) => l.term.input, + }, + { name: 'close', kind: node(Sym.EvalClose), label: undefined }, + { + name: 'output', + kind: node(Type), + space: true, + label: () => (l) => l.term.type, }, - { name: 'close', kind: node(Sym.EvalClose) }, - { name: 'output', kind: node(Type), space: true }, ]; } @@ -149,16 +165,19 @@ export default class FunctionType extends Type { ) { const thisBind = this.inputs[i]; const thatBind = inputsToCheck[i]; + // Ensure the this input accepts the other input if ( thisBind.type instanceof Type && thatBind.type instanceof Type && !thisBind.type.accepts(thatBind.type, context) ) return false; + // Ensure variable-length status matches if (thisBind.isVariableLength() !== thatBind.isVariableLength()) return false; - if (thisBind.hasDefault() !== thatBind.hasDefault()) - return false; + // It doesn't matter if the binds have defaults or not. The types are the same. + // if (thisBind.hasDefault() !== thatBind.hasDefault()) + // return false; } return true; }); diff --git a/src/nodes/Initial.ts b/src/nodes/Initial.ts index 3616b7f32..0bbf5fd77 100644 --- a/src/nodes/Initial.ts +++ b/src/nodes/Initial.ts @@ -40,7 +40,7 @@ export default class Initial extends SimpleExpression { return [Initial.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [Initial.make()]; } @@ -49,7 +49,7 @@ export default class Initial extends SimpleExpression { } getGrammar(): Grammar { - return [{ name: 'diamond', kind: node(Sym.Initial) }]; + return [{ name: 'diamond', kind: node(Sym.Initial), label: undefined }]; } clone(replace?: Replacement) { @@ -59,7 +59,7 @@ export default class Initial extends SimpleExpression { } getPurpose() { - return Purpose.Decide; + return Purpose.Advanced; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/Input.ts b/src/nodes/Input.ts index 5744c8cf6..cf59db6af 100644 --- a/src/nodes/Input.ts +++ b/src/nodes/Input.ts @@ -1,7 +1,7 @@ import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; -import Refer from '@edit/Refer'; +import type { InsertContext } from '@edit/revision/EditContext'; +import Refer from '@edit/revision/Refer'; import type Locales from '@locale/Locales'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -44,17 +44,20 @@ export default class Input extends Node { ); } - static getPossibleReplacements({ node, context }: EditContext) { - const parent = node.getParent(context); - // Evaluate, and the anchor is the open or an input? Offer binds to unset properties. - if ( - parent instanceof Evaluate && - (node === parent.open || - (node instanceof Expression && parent.inputs.includes(node))) - ) { + static getPossibleReplacements() { + return []; + } + + static getPossibleInsertions({ parent, context }: InsertContext) { + // If the parent is an evaluate, offer inputs. + if (parent instanceof Evaluate) { const mapping = parent.getInputMapping(context); return mapping?.inputs - .filter((input) => input.given === undefined) + .filter( + (input) => + input.given === undefined || + input.expected.isVariableLength(), + ) .map( (input, index, inputs) => new Refer((name) => { @@ -73,14 +76,10 @@ export default class Input extends Node { } else return []; } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); - } - getGrammar(): Grammar { return [ - { name: 'name', kind: node(Sym.Name) }, - { name: 'bind', kind: node(Sym.Bind) }, + { name: 'name', kind: node(Sym.Name), label: undefined }, + { name: 'bind', kind: node(Sym.Bind), label: undefined }, { name: 'value', kind: node(Expression), @@ -97,6 +96,7 @@ export default class Input extends Node { } return new NoExpressionType(this.value); }, + label: () => (l) => l.term.value, }, ]; } @@ -110,7 +110,7 @@ export default class Input extends Node { } getPurpose() { - return Purpose.Evaluate; + return Purpose.Definitions; } getType(context: Context): Type { diff --git a/src/nodes/Insert.ts b/src/nodes/Insert.ts index 65d683378..19e0570df 100644 --- a/src/nodes/Insert.ts +++ b/src/nodes/Insert.ts @@ -1,5 +1,5 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -82,7 +82,7 @@ export default class Insert extends Expression { ]; } - static getPossibleReplacements({ node, context }: EditContext) { + static getPossibleReplacements({ node, context }: ReplaceContext) { const anchorType = node instanceof Expression ? node.getType(context) : undefined; const tableType = @@ -99,12 +99,12 @@ export default class Insert extends Expression { : []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [Insert.make(ExpressionPlaceholder.make(TableType.make()))]; } getPurpose() { - return Purpose.Value; + return Purpose.Tables; } clone(replace?: Replacement) { diff --git a/src/nodes/Is.ts b/src/nodes/Is.ts index 169d40f0d..9100931da 100644 --- a/src/nodes/Is.ts +++ b/src/nodes/Is.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import { ImpossibleType } from '@conflicts/ImpossibleType'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -42,19 +42,22 @@ export default class Is extends Expression { } static make(left: Expression, right: Type) { - return new Is(left, new Token(TYPE_SYMBOL, Sym.TypeOperator), right); + return new Is(left, new Token(TYPE_SYMBOL, Sym.Type), right); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Expression - ? [Is.make(node, TypePlaceholder.make())] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends({ type }: EditContext) { - return [ - Is.make(ExpressionPlaceholder.make(type), TypePlaceholder.make()), - ]; + static getPossibleInsertions({ type }: InsertContext) { + return type instanceof BooleanType + ? [ + Is.make( + ExpressionPlaceholder.make(type), + TypePlaceholder.make(), + ), + ] + : []; } getDescriptor(): NodeDescriptor { @@ -63,9 +66,13 @@ export default class Is extends Expression { getGrammar(): Grammar { return [ - { name: 'expression', kind: node(Expression) }, - { name: 'operator', kind: node(Sym.Type) }, - { name: 'type', kind: node(Type) }, + { + name: 'expression', + kind: node(Expression), + label: () => (l) => l.term.value, + }, + { name: 'operator', kind: node(Sym.Type), label: undefined }, + { name: 'type', kind: node(Type), label: () => (l) => l.term.type }, ]; } @@ -78,7 +85,7 @@ export default class Is extends Expression { } getPurpose(): Purpose { - return Purpose.Decide; + return Purpose.Types; } computeType() { diff --git a/src/nodes/IsLocale.ts b/src/nodes/IsLocale.ts index 586cdbfdc..4866abd5d 100644 --- a/src/nodes/IsLocale.ts +++ b/src/nodes/IsLocale.ts @@ -40,7 +40,7 @@ export default class IsLocale extends SimpleExpression { return [IsLocale.make(Language.make('en'))]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [IsLocale.make(Language.make('en'))]; } @@ -50,8 +50,12 @@ export default class IsLocale extends SimpleExpression { getGrammar(): Grammar { return [ - { name: 'globe', kind: node(Sym.Locale) }, - { name: 'locale', kind: optional(node(Language)) }, + { name: 'globe', kind: node(Sym.Locale), label: undefined }, + { + name: 'locale', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, ]; } @@ -63,7 +67,7 @@ export default class IsLocale extends SimpleExpression { } getPurpose() { - return Purpose.Decide; + return Purpose.Text; } computeConflicts() { diff --git a/src/nodes/KeyValue.ts b/src/nodes/KeyValue.ts index ea3d05cac..f5e699c34 100644 --- a/src/nodes/KeyValue.ts +++ b/src/nodes/KeyValue.ts @@ -39,7 +39,7 @@ export default class KeyValue extends Node { ]; } - static getPossibleAppends() { + static getPossibleInsertions() { return this.getPossibleReplacements(); } @@ -55,7 +55,7 @@ export default class KeyValue extends Node { label: () => (l) => l.term.key, space: true, }, - { name: 'bind', kind: node(Sym.Bind) }, + { name: 'bind', kind: node(Sym.Bind), label: undefined }, { name: 'value', kind: node(Expression), @@ -74,7 +74,7 @@ export default class KeyValue extends Node { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Maps; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/Language.ts b/src/nodes/Language.ts index 46d6c62d5..1e376318b 100644 --- a/src/nodes/Language.ts +++ b/src/nodes/Language.ts @@ -50,7 +50,7 @@ export default class Language extends Node { ); } - static getPossibleAppends() { + static getPossibleInsertions() { return Object.keys(Languages).map((language) => Language.make(language), ); @@ -62,10 +62,22 @@ export default class Language extends Node { getGrammar(): Grammar { return [ - { name: 'slash', kind: node(Sym.Language) }, - { name: 'language', kind: optional(node(Sym.Name)) }, - { name: 'dash', kind: optional(node(Sym.Region)) }, - { name: 'region', kind: optional(node(Sym.Name)) }, + { name: 'slash', kind: node(Sym.Language), label: undefined }, + { + name: 'language', + kind: optional(node(Sym.Name)), + label: undefined, + }, + { + name: 'dash', + kind: optional(node(Sym.Region)), + label: undefined, + }, + { + name: 'region', + kind: optional(node(Sym.Name)), + label: () => (l) => l.term.region, + }, ]; } @@ -79,7 +91,7 @@ export default class Language extends Node { } getPurpose() { - return Purpose.Document; + return Purpose.Text; } computeConflicts(): Conflict[] { diff --git a/src/nodes/ListAccess.ts b/src/nodes/ListAccess.ts index 80e242bbc..325bb880b 100644 --- a/src/nodes/ListAccess.ts +++ b/src/nodes/ListAccess.ts @@ -1,6 +1,5 @@ import type Conflict from '@conflicts/Conflict'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -21,7 +20,6 @@ import AnyType from './AnyType'; import Bind from './Bind'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import getGuards from './getGuards'; import ListCloseToken from './ListCloseToken'; import ListOpenToken from './ListOpenToken'; @@ -69,32 +67,12 @@ export default class ListAccess extends Expression { ); } - static getPossibleReplacements({ node, context }: EditContext) { - if (!(node instanceof Expression)) return []; - return node.getType(context).accepts(ListType.make(), context) - ? [ - ListAccess.make( - node, - ExpressionPlaceholder.make(NumberType.make()), - ), - ] - : node.getType(context).accepts(NumberType.make(), context) - ? [ - ListAccess.make( - ExpressionPlaceholder.make(ListType.make()), - node, - ), - ] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { - return [ - ListAccess.make( - ExpressionPlaceholder.make(ListType.make()), - ExpressionPlaceholder.make(NumberType.make()), - ), - ]; + static getPossibleInsertions() { + return []; } getDescriptor(): NodeDescriptor { @@ -110,7 +88,7 @@ export default class ListAccess extends Expression { // Must be a list getType: () => ListType.make(), }, - { name: 'open', kind: node(Sym.ListOpen) }, + { name: 'open', kind: node(Sym.ListOpen), label: undefined }, { name: 'index', kind: node(Expression), @@ -118,7 +96,7 @@ export default class ListAccess extends Expression { // Must be a number getType: () => NumberType.make(), }, - { name: 'close', kind: node(Sym.ListClose) }, + { name: 'close', kind: node(Sym.ListClose), label: undefined }, ]; } @@ -132,7 +110,7 @@ export default class ListAccess extends Expression { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Lists; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/ListLiteral.ts b/src/nodes/ListLiteral.ts index e40503ce2..1428a857d 100644 --- a/src/nodes/ListLiteral.ts +++ b/src/nodes/ListLiteral.ts @@ -1,6 +1,5 @@ import type Conflict from '@conflicts/Conflict'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { MAX_LINE_LENGTH } from '@parser/Spaces'; @@ -16,6 +15,7 @@ import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import TypeException from '../values/TypeException'; import AnyType from './AnyType'; +import CompositeLiteral from './CompositeLiteral'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; import ListCloseToken from './ListCloseToken'; @@ -29,7 +29,7 @@ import type Type from './Type'; import type TypeSet from './TypeSet'; import UnionType from './UnionType'; -export default class ListLiteral extends Expression { +export default class ListLiteral extends CompositeLiteral { readonly open: Token; readonly values: (Spread | Expression)[]; readonly close: Token | undefined; @@ -59,14 +59,13 @@ export default class ListLiteral extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Expression - ? [ListLiteral.make(), ListLiteral.make([node])] - : []; + static getPossibleReplacements() { + // Offer to wrap the element in a list + return node instanceof Expression ? [ListLiteral.make([node])] : []; } - static getPossibleAppends() { - return [ListLiteral.make()]; + static getPossibleInsertions() { + return [ListLiteral.make([])]; } getDescriptor(): NodeDescriptor { @@ -75,15 +74,37 @@ export default class ListLiteral extends Expression { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.ListOpen) }, + { name: 'open', kind: node(Sym.ListOpen), label: undefined }, { name: 'values', kind: list(true, node(Expression), node(Spread)), - label: () => (l) => l.node.ListLiteral.item, - // Only allow types to be inserted that are of the list's type, if provided. - getType: (context) => - this.getItemType(context)?.generalize(context) ?? - new AnyType(), + label: () => (l) => l.term.value, + // Only allow types to be inserted that are of the surrounding field's expected type. + getType: (context) => { + // What is the field of this list? + const parent = context.getRoot(this)?.getParent(this); + if (parent) { + const field = parent.getFieldOfChild(this); + if (field) { + if (field.getType) { + const fieldValue = parent.getField(field.name); + const index = Array.isArray(fieldValue) + ? fieldValue.indexOf(this) + : -1; + const listType = field.getType( + context, + index < 0 ? undefined : index, + ); + if ( + listType instanceof ListType && + listType.type !== undefined + ) + return listType.type; + } + } + } + return new AnyType(); + }, space: true, // Only add line breaks if greater than 40 characters long. newline: this.wrap(), @@ -92,8 +113,13 @@ export default class ListLiteral extends Expression { // Include an indent before all items in the list indent: true, }, - { name: 'close', kind: node(Sym.ListClose), newline: this.wrap() }, - { name: 'literal', kind: node(Sym.Literal) }, + { + name: 'close', + kind: node(Sym.ListClose), + label: undefined, + newline: this.wrap(), + }, + { name: 'literal', kind: node(Sym.Literal), label: undefined }, ]; } @@ -116,7 +142,7 @@ export default class ListLiteral extends Expression { } getPurpose() { - return Purpose.Value; + return Purpose.Lists; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/ListType.ts b/src/nodes/ListType.ts index 388b8529e..b85202b44 100644 --- a/src/nodes/ListType.ts +++ b/src/nodes/ListType.ts @@ -1,4 +1,4 @@ -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { LIST_CLOSE_SYMBOL, LIST_OPEN_SYMBOL } from '@parser/Symbols'; @@ -47,14 +47,16 @@ export default class ListType extends BasisType { ); } - static getPossibleReplacements({ node }: EditContext) { - return [ - ListType.make(), - ...(node instanceof Type ? [ListType.make(node)] : []), - ]; + static getPossibleReplacements({ node }: ReplaceContext) { + return node instanceof Type + ? [ + ListType.make(), + ...(node instanceof Type ? [ListType.make(node)] : []), + ] + : []; } - static getPossibleAppends() { + static getPossibleInsertions({}: InsertContext) { return [ListType.make()]; } @@ -64,9 +66,13 @@ export default class ListType extends BasisType { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.ListOpen) }, - { name: 'type', kind: optional(node(Type)) }, - { name: 'close', kind: node(Sym.ListClose) }, + { name: 'open', kind: node(Sym.ListOpen), label: undefined }, + { + name: 'type', + kind: optional(node(Type)), + label: () => (l) => l.term.type, + }, + { name: 'close', kind: node(Sym.ListClose), label: undefined }, ]; } diff --git a/src/nodes/Literal.ts b/src/nodes/Literal.ts index 8796ca0a0..3a31a8c82 100644 --- a/src/nodes/Literal.ts +++ b/src/nodes/Literal.ts @@ -3,7 +3,6 @@ import type Evaluator from '@runtime/Evaluator'; import StartFinish from '@runtime/StartFinish'; import type Step from '@runtime/Step'; import type Value from '@values/Value'; -import Purpose from '../concepts/Purpose'; import type Context from './Context'; import type Expression from './Expression'; import SimpleExpression from './SimpleExpression'; @@ -22,10 +21,6 @@ export default abstract class Literal extends SimpleExpression { return [new StartFinish(this)]; } - getPurpose() { - return Purpose.Value; - } - evaluate(evaluator: Evaluator, prior: Value | undefined): Value { if (prior) return prior; diff --git a/src/nodes/MapLiteral.ts b/src/nodes/MapLiteral.ts index 6fa783749..cf700a3c6 100644 --- a/src/nodes/MapLiteral.ts +++ b/src/nodes/MapLiteral.ts @@ -1,7 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import { NotAKeyValue } from '@conflicts/NotAKeyValue'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import KeyValue from '@nodes/KeyValue'; @@ -19,9 +18,9 @@ import Characters from '../lore/BasisCharacters'; import ValueException from '../values/ValueException'; import AnyType from './AnyType'; import BindToken from './BindToken'; +import CompositeLiteral from './CompositeLiteral'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import MapType from './MapType'; import { list, node, optional, type Grammar, type Replacement } from './Node'; import SetCloseToken from './SetCloseToken'; @@ -32,7 +31,7 @@ import type Type from './Type'; import type TypeSet from './TypeSet'; import UnionType from './UnionType'; -export default class MapLiteral extends Expression { +export default class MapLiteral extends CompositeLiteral { readonly open: Token; readonly values: (Expression | KeyValue)[]; readonly close: Token | undefined; @@ -66,21 +65,11 @@ export default class MapLiteral extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Expression - ? [ - MapLiteral.make(), - MapLiteral.make([ - KeyValue.make(node, ExpressionPlaceholder.make()), - ]), - MapLiteral.make([ - KeyValue.make(ExpressionPlaceholder.make(), node), - ]), - ] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [MapLiteral.make()]; } @@ -90,8 +79,8 @@ export default class MapLiteral extends Expression { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.SetOpen) }, - { name: 'bind', kind: optional(node(Sym.Bind)) }, + { name: 'open', kind: node(Sym.SetOpen), label: undefined }, + { name: 'bind', kind: optional(node(Sym.Bind)), label: undefined }, { name: 'values', kind: list(true, node(KeyValue)), @@ -99,9 +88,15 @@ export default class MapLiteral extends Expression { indent: true, initial: true, newline: this.wrap(), + label: () => (l) => l.node.MapLiteral.label.values, }, - { name: 'close', kind: node(Sym.SetClose), newline: this.wrap() }, - { name: 'literal', kind: node(Sym.Literal) }, + { + name: 'close', + kind: node(Sym.SetClose), + newline: this.wrap(), + label: undefined, + }, + { name: 'literal', kind: node(Sym.Literal), label: undefined }, ]; } @@ -125,7 +120,7 @@ export default class MapLiteral extends Expression { } getPurpose() { - return Purpose.Value; + return Purpose.Hidden; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/MapType.ts b/src/nodes/MapType.ts index 7b8107c9d..1fdabed8c 100644 --- a/src/nodes/MapType.ts +++ b/src/nodes/MapType.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -56,19 +56,16 @@ export default class MapType extends BasisType { ); } - static getPossibleReplacements({ node }: EditContext) { - return [ - MapType.make(), - ...(node instanceof Type && node - ? [ - MapType.make(node, TypePlaceholder.make()), - MapType.make(TypePlaceholder.make(), node), - ] - : []), - ]; + static getPossibleReplacements({ node }: ReplaceContext) { + return node instanceof Type + ? [ + MapType.make(node, TypePlaceholder.make()), + MapType.make(TypePlaceholder.make(), node), + ] + : []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [MapType.make()]; } @@ -78,23 +75,25 @@ export default class MapType extends BasisType { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.SetOpen) }, + { name: 'open', kind: node(Sym.SetOpen), label: undefined }, { name: 'key', kind: any( node(Type), none(['value', () => ExpressionPlaceholder.make()]), ), + label: () => (l) => l.term.type, }, - { name: 'bind', kind: node(Sym.Bind) }, + { name: 'bind', kind: node(Sym.Bind), label: undefined }, { name: 'value', kind: any( node(Type), none(['key', () => ExpressionPlaceholder.make()]), ), + label: () => (l) => l.term.type, }, - { name: 'close', kind: node(Sym.SetClose) }, + { name: 'close', kind: node(Sym.SetClose), label: undefined }, ]; } diff --git a/src/nodes/Markup.ts b/src/nodes/Markup.ts index 9c62ac361..0cca8cd96 100644 --- a/src/nodes/Markup.ts +++ b/src/nodes/Markup.ts @@ -1,3 +1,4 @@ +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { FontWeight } from '../basis/Fonts'; @@ -42,12 +43,16 @@ export default class Markup extends Content { return markup; } - static getPossibleReplacements() { - return [new Markup([new Paragraph([])])]; + static getPossibleReplacements({ locales }: ReplaceContext) { + return [ + new Paragraph([Words.make(locales.get((l) => l.node.Markup.name))]), + ]; } - static getPossibleAppends() { - return [new Markup([new Paragraph([])])]; + static getPossibleInsertions({ locales }: InsertContext) { + return [ + new Paragraph([Words.make(locales.get((l) => l.node.Markup.name))]), + ]; } getDescriptor(): NodeDescriptor { @@ -59,6 +64,7 @@ export default class Markup extends Content { { name: 'paragraphs', kind: list(true, node(Paragraph)), + label: () => (l) => l.node.Markup.label.paragraphs, newline: true, double: true, }, @@ -73,7 +79,7 @@ export default class Markup extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } computeConflicts() { diff --git a/src/nodes/Match.ts b/src/nodes/Match.ts index 9dae20355..13609f296 100644 --- a/src/nodes/Match.ts +++ b/src/nodes/Match.ts @@ -1,7 +1,7 @@ import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import IncompatibleType from '@conflicts/IncompatibleType'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -73,12 +73,21 @@ export default class Match extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - return [Match.make(node instanceof Expression ? node : undefined)]; + static getPossibleReplacements({ node }: ReplaceContext) { + // Wrap the value in a match with the value as a default + return node instanceof Expression + ? [ + Match.make( + undefined, + undefined, + node instanceof Expression ? node : undefined, + ), + ] + : []; } - static getPossibleAppends() { - return [Match.make()]; + static getPossibleInsertions() { + return []; } isUndelimited() { @@ -94,9 +103,14 @@ export default class Match extends Expression { { name: 'value', kind: node(Expression), - label: () => (l) => l.node.Match.value, + label: () => (l) => l.term.value, + }, + { + name: 'question', + kind: node(Sym.Match), + space: true, + label: undefined, }, - { name: 'question', kind: node(Sym.Match), space: true }, { name: 'cases', kind: list(true, node(KeyValue)), @@ -104,11 +118,12 @@ export default class Match extends Expression { indent: true, newline: true, initial: true, + label: () => (l) => l.node.Match.label.case, }, { name: 'other', kind: node(Expression), - label: () => (l) => l.node.Match.other, + label: () => (l) => l.node.Match.label.other, space: true, indent: true, newline: true, @@ -130,7 +145,7 @@ export default class Match extends Expression { } getPurpose() { - return Purpose.Decide; + return Purpose.Decisions; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/Mention.ts b/src/nodes/Mention.ts index fd19362d3..d03447db7 100644 --- a/src/nodes/Mention.ts +++ b/src/nodes/Mention.ts @@ -38,7 +38,7 @@ export default class Mention extends Content { return [new Mention(new Token('_', Sym.Mention))]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [new Mention(new Token('_', Sym.Mention))]; } @@ -47,7 +47,7 @@ export default class Mention extends Content { } getGrammar(): Grammar { - return [{ name: 'name', kind: node(Sym.Mention) }]; + return [{ name: 'name', kind: node(Sym.Mention), label: undefined }]; } computeConflicts() { return []; @@ -60,7 +60,7 @@ export default class Mention extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.Mention; diff --git a/src/nodes/Name.ts b/src/nodes/Name.ts index 4d58ca14a..f9dc3c1d6 100644 --- a/src/nodes/Name.ts +++ b/src/nodes/Name.ts @@ -1,9 +1,11 @@ import type Conflict from '@conflicts/Conflict'; +import type { InsertContext } from '@edit/revision/EditContext'; import type LanguageCode from '@locale/LanguageCode'; import type Locale from '@locale/Locale'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { COMMA_SYMBOL } from '@parser/Symbols'; +import { OperatorRegEx } from '@parser/Tokenizer'; import Purpose from '../concepts/Purpose'; import Emotion from '../lore/Emotion'; import ReservedSymbols from '../parser/ReservedSymbols'; @@ -45,9 +47,17 @@ export default class Name extends LanguageTagged { getGrammar(): Grammar { return [ - { name: 'name', kind: node(Sym.Name) }, - { name: 'language', kind: optional(node(Language)) }, - { name: 'separator', kind: optional(node(Sym.Separator)) }, + { name: 'name', kind: node(Sym.Name), label: undefined }, + { + name: 'language', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, + { + name: 'separator', + kind: optional(node(Sym.Separator)), + label: undefined, + }, ]; } @@ -59,8 +69,18 @@ export default class Name extends LanguageTagged { ) as this; } + /** Doesn't ever make sense to replace a Name with an empty name. */ + static getPossibleReplacements() { + return []; + } + + /** Suggest names for insertion. */ + static getPossibleInsertions({ locales }: InsertContext) { + return [Name.make(locales.get((l) => l.term.name))]; + } + simplify() { - return this; + return this.withoutLanguage(); } getCorrespondingDefinition(context: Context): Definition | undefined { @@ -78,7 +98,7 @@ export default class Name extends LanguageTagged { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } computeConflicts(): Conflict[] { @@ -130,6 +150,10 @@ export default class Name extends LanguageTagged { return this.name && this.name.startsWith(prefix); } + isOperator() { + return OperatorRegEx.test(this.name.text.getText()); + } + withoutLanguage() { return new Name(this.name, undefined, this.separator); } diff --git a/src/nodes/NameType.ts b/src/nodes/NameType.ts index 41c97e95d..0091eea57 100644 --- a/src/nodes/NameType.ts +++ b/src/nodes/NameType.ts @@ -1,7 +1,10 @@ +import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import UnexpectedTypeInput from '@conflicts/UnexpectedTypeInput'; import { UnknownName } from '@conflicts/UnknownName'; import { UnknownTypeName } from '@conflicts/UnknownTypeName'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; +import Refer from '@edit/revision/Refer'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -45,14 +48,44 @@ export default class NameType extends Type { return new NameType(new NameToken(name), undefined, definition); } + static getStructuresInScope(node: Node, context: Context) { + // Suggest all defined structures in scope + return node + .getDefinitionsInScope(context) + .filter((def) => def instanceof StructureDefinition) + .map((def) => new Refer((name) => NameType.make(name, def), def)); + } + + static getPossibleReplacements({ node, context }: ReplaceContext) { + // Suggest all defined structures in scope + return this.getStructuresInScope(node, context); + } + + static getPossibleInsertions({ parent, context }: InsertContext) { + return this.getStructuresInScope(parent, context); + } + getDescriptor(): NodeDescriptor { return 'NameType'; } + getPurpose(): Purpose { + return Purpose.Types; + } + getGrammar(): Grammar { return [ - { name: 'name', kind: node(Sym.Name), uncompletable: true }, - { name: 'types', kind: optional(node(TypeInputs)) }, + { + name: 'name', + kind: node(Sym.Name), + uncompletable: true, + label: undefined, + }, + { + name: 'types', + kind: optional(node(TypeInputs)), + label: undefined, + }, ]; } diff --git a/src/nodes/Names.ts b/src/nodes/Names.ts index fb2cbdfd5..6541eb854 100644 --- a/src/nodes/Names.ts +++ b/src/nodes/Names.ts @@ -54,7 +54,13 @@ export default class Names extends Node { } getGrammar(): Grammar { - return [{ name: 'names', kind: list(false, node(Name)) }]; + return [ + { + name: 'names', + kind: list(false, node(Name)), + label: () => (l) => l.node.Names.label.names, + }, + ]; } clone(replace?: Replacement) { @@ -63,12 +69,16 @@ export default class Names extends Node { ) as this; } + isEmpty() { + return this.names.length === 0; + } + simplify() { return new Names(this.names.map((name) => name.simplify())); } getPurpose() { - return Purpose.Bind; + return Purpose.Hidden; } computeConflicts() { @@ -122,6 +132,15 @@ export default class Names extends Node { return this.getSymbolicName() !== undefined; } + getOperatorName() { + return this.names.find((name) => name.isOperator()); + } + + /** Returns true if it has an operator name */ + hasOperatorName() { + return this.getOperatorName() !== undefined; + } + getPreferredNameString( preferred: LocaleText | LocaleText[] | Locale | Locale[], symbolic = true, @@ -212,19 +231,24 @@ export default class Names extends Node { return Names.LocalePath; } + /** Update the name with the given langauge Add or change the name of with the matching language. If there is no match, replace the , if there is one.no names have languages, then it replaces the language free name. */ withName(name: string, language: LanguageCode) { - const index = this.names.findIndex( + let matchingIndex = this.names.findIndex( (name) => name.getLanguage() === language, ); + if (matchingIndex < 0) + matchingIndex = this.names.findIndex( + (name) => name.getLanguage() === undefined, + ); const newName = Name.make(name, Language.make(language)); return new Names( - index < 0 + matchingIndex < 0 ? [...this.names, newName] : [ - ...this.names.slice(0, index), + ...this.names.slice(0, matchingIndex), newName, - ...this.names.slice(index + 1), + ...this.names.slice(matchingIndex + 1), ], ); } diff --git a/src/nodes/NeverType.ts b/src/nodes/NeverType.ts index c8ae77c2e..85ee30024 100644 --- a/src/nodes/NeverType.ts +++ b/src/nodes/NeverType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { NEVER_SYMBOL } from '@parser/Symbols'; @@ -14,6 +15,10 @@ export default class NeverType extends Type { return 'NeverType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar() { return []; } diff --git a/src/nodes/Node.test.ts b/src/nodes/Node.test.ts index 6206b2892..f02c67ddf 100644 --- a/src/nodes/Node.test.ts +++ b/src/nodes/Node.test.ts @@ -2,7 +2,7 @@ import { expect, test } from 'vitest'; import parseExpression from '../parser/parseExpression'; import { toTokens } from '../parser/toTokens'; import Bind from './Bind'; -import Docs from './Docs'; +import Doc from './Doc'; import Language from './Language'; import type Node from './Node'; import NumberLiteral from './NumberLiteral'; @@ -28,7 +28,8 @@ test.each([ ['1 + 2 + 3', NumberLiteral, 2, '4', '1 + 2 + 4'], // Replace Node with undefined ['"Hi"/en', Language, 0, undefined, '"Hi"'], - ['¶Hi¶/en(1)', Docs, 0, undefined, '(1)'], + // Remove Node from list by passing undefined + ['¶Hi¶/en(1)', Doc, 0, undefined, '(1)'], // Remove Node in list ['[ 1 2 3 ]', NumberLiteral, 0, undefined, '[ 2 3 ]'], // Replace Node in list diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index c3adaf44e..1ee01ceab 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -172,8 +172,7 @@ export default abstract class Node { nodes(include?: (node: Node) => node is Kind): Kind[] { const nodes: Node[] = []; this.traverse((node) => { - if (include === undefined || include(node) === true) - nodes.push(node); + if (include === undefined || include(node)) nodes.push(node); return true; }); return nodes as Kind[]; @@ -265,6 +264,28 @@ export default abstract class Node { return this.getFieldNamed(name)?.kind; } + getAdjacentFieldNode( + fieldName: string, + forward: boolean, + ): Node | undefined { + const grammar = forward + ? this.getGrammar() + : this.getGrammar().reverse(); + const values: Node[] = []; + for (let index = 0; index < grammar.length; index++) { + const field = grammar[index]; + // Found the node? Get the most recent node we read. + if (field.name === fieldName) return values.at(-1); + // Otherwise, add the nodes. + else { + const fieldValue = this.getField(field.name); + if (fieldValue instanceof Node) values.push(fieldValue); + else if (Array.isArray(fieldValue)) values.push(...fieldValue); + } + } + return undefined; + } + // CONFLICTS /** Given the program in which the node is situated, returns any conflicts on this node that would prevent execution. */ @@ -291,6 +312,11 @@ export default abstract class Node { return context.project.getRoot(this)?.getParent(this); } + /** Returns true if there's only one leaf in this node */ + hasOneLeaf(): boolean { + return this.leaves().length === 1; + } + // BINDINGS /** @@ -339,25 +365,27 @@ export default abstract class Node { // See if this node has any additional basis functions to include. let additional = this.getAdditionalBasisScope(context); while (scope !== undefined || additional) { + if (scope) + definitions = definitions.concat( + scope.getDefinitions(this, context), + ); // Order matters here: defintions close in the tree have precedent, so they should go first. if (additional) definitions = definitions.concat( additional.getDefinitions(this, context), ); - if (scope) - definitions = definitions.concat( - scope.getDefinitions(this, context), - ); // Before changing the scope, see if it has any additional basis scope to add. additional = scope?.getAdditionalBasisScope(context); // After getting definitions from the scope, get the scope's scope. scope = scope?.getScope(context); } + // Include any source files that aren't the main source file and aren't this source file. // Finally, implicitly include standard libraries and definitions. - definitions = definitions.concat( - context.project.getDefaultShares().all, - ); + definitions = [ + ...definitions, + ...context.project.getDefaultShares().all, + ]; // Cache the definitions for later. context.definitions.set(this, definitions); @@ -665,8 +693,15 @@ export default abstract class Node { context: Context, root: Root, ): LocaleTextAccessor | undefined { - const label = this.getFieldOfChild(child)?.label; - return label ? label(locales, child, context, root) : undefined; + const field = this.getFieldOfChild(child); + if (field === undefined) return undefined; + const label = field?.label; + if (label === undefined) return undefined; + const index = + field.kind instanceof ListOf + ? (this as any)[field.name].indexOf(child) + : undefined; + return label(locales, context, index, root); } /** Translates the node back into Wordplay text, using spaces if provided and . */ @@ -685,18 +720,9 @@ export default abstract class Node { } } -export type Field = { +type BaseField = { /** The name of the field, corresponding to a name on the Node class. Redundant with the class, but no reflection in JavaScript. */ name: string; - /** A list of possible Node class types that the field may be. Redundant with the class, but no reflection in JavaScript. */ - kind: Any | Empty | ListOf | IsA; - /** A description of the field for the UI */ - label?: ( - locales: Locales, - child: Node, - context: Context, - root: Root, - ) => LocaleTextAccessor; /** True if a preceding space is preferred the node */ space?: boolean | ((node: Node) => boolean); /** True if the field should be indented if on a new line */ @@ -717,6 +743,29 @@ export type Field = { getDefinitions?: (context: Context) => Definition[]; }; +type LabelAccessor = ( + /** The locales to use */ + locales: Locales, + /** The source context */ + context: Context, + /** The index of the child in the list, if the field is a list */ + index: number | undefined, + /** The root node */ + root: Root, +) => LocaleTextAccessor; + +export type ListField = BaseField & { + kind: ListOf; + label: LabelAccessor; +}; + +export type OtherField = BaseField & { + kind: IsA | Empty | Any; + label: LabelAccessor | undefined; +}; + +export type Field = ListField | OtherField; + /** These types help define a node's grammar at runtime, allowing for a range of rules to be specified about their structure. * This helps with edits, autocomplete, spacing rules, and more. */ @@ -734,6 +783,12 @@ export abstract class FieldKind { abstract toString(): string; } +export function enumerateSymbols(field: Field): Sym[] { + return field.kind + .enumerate() + .filter((k): k is Sym => k !== undefined && !(k instanceof Function)); +} + // A field can be of this type of node or token type. export class IsA extends FieldKind { readonly kind: Function | Sym; @@ -828,7 +883,11 @@ export class ListOf extends FieldKind { } } -type EmptyDefault = { name: string; createDefault: () => Node }; +/** + * Represents a dependency between two fields of a node, where creating one must mean creating the other. + * The createDefault() function generates the dependent field given the other field's value. + * */ +type EmptyDefault = { name: string; createDefault: (node: Node) => Node }; // A field can be undefined, and if a dependency field name is specified, only if that field is also undefined. export class Empty extends FieldKind { @@ -918,7 +977,9 @@ export class Any extends FieldKind { export function node(kind: Function | Sym) { return new IsA(kind); } -export function none(dependency?: [string, () => Node]) { + +/** An empty option, with an optional dependency on another field that generates a default when creating them. */ +export function none(dependency?: [string, (node: Node) => Node]) { return new Empty( dependency ? { name: dependency[0], createDefault: dependency[1] } @@ -938,7 +999,26 @@ export function optional(kind: IsA) { export type Grammar = Field[]; +/** Represents a replacement of an original node or string with a new field value */ export type Replacement = { original: Node | Node[] | string; replacement: FieldValue; }; + +/** Represents a node and a field on the node, and optional index into a list field. Used to represent a selection of a field for editing. */ +export type FieldPosition = { + parent: Node; + field: string; + index: number | undefined; +}; + +export function isFieldPosition(value: any): value is FieldPosition { + return ( + value !== undefined && + typeof value === 'object' && + 'parent' in value && + value.parent instanceof Node && + 'field' in value && + typeof value.field === 'string' + ); +} diff --git a/src/nodes/NoneLiteral.ts b/src/nodes/NoneLiteral.ts index 69b09b2a5..f81427b46 100644 --- a/src/nodes/NoneLiteral.ts +++ b/src/nodes/NoneLiteral.ts @@ -1,4 +1,4 @@ -import type EditContext from '@edit/EditContext'; +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { NONE_SYMBOL } from '@parser/Symbols'; @@ -29,12 +29,17 @@ export default class NoneLiteral extends Literal { return 'NoneLiteral'; } + getPurpose(): Purpose { + return Purpose.Truth; + } + getGrammar(): Grammar { return [ { name: 'none', kind: node(Sym.None), getType: () => NoneType.make(), + label: undefined, }, ]; } @@ -43,14 +48,12 @@ export default class NoneLiteral extends Literal { return new NoneLiteral(new Token(NONE_SYMBOL, Sym.None)); } - static getPossibleReplacements({ type, context }: EditContext) { - return type === undefined || type.accepts(NoneType.make(), context) - ? [NoneLiteral.make()] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); + static getPossibleInsertions() { + return [NoneLiteral.make()]; } clone(replace?: Replacement) { diff --git a/src/nodes/NoneType.ts b/src/nodes/NoneType.ts index 8f996f143..d701aebb6 100644 --- a/src/nodes/NoneType.ts +++ b/src/nodes/NoneType.ts @@ -31,7 +31,7 @@ export default class NoneType extends BasisType { return [NoneType.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [NoneType.make()]; } @@ -40,7 +40,7 @@ export default class NoneType extends BasisType { } getGrammar(): Grammar { - return [{ name: 'none', kind: node(Sym.None) }]; + return [{ name: 'none', kind: node(Sym.None), label: undefined }]; } computeConflicts() { diff --git a/src/nodes/NumberLiteral.ts b/src/nodes/NumberLiteral.ts index 559f4500f..a6132ec28 100644 --- a/src/nodes/NumberLiteral.ts +++ b/src/nodes/NumberLiteral.ts @@ -1,6 +1,8 @@ +import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import { NotANumber } from '@conflicts/NotANumber'; -import type EditContext from '@edit/EditContext'; +import { getPossibleDimensions } from '@edit/menu/getPossibleUnits'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -11,7 +13,9 @@ import type Locales from '../locale/Locales'; import { type TemplateInput } from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import type Context from './Context'; +import Dimension from './Dimension'; import Literal from './Literal'; +import type Node from './Node'; import { node, optional, type Grammar, type Replacement } from './Node'; import NumberType from './NumberType'; import Sym from './Sym'; @@ -50,7 +54,13 @@ export default class NumberLiteral extends Literal { ); } - static getPossibleReplacements({ type, context }: EditContext) { + /** Given a type and source context, */ + static getPossibleNumbers( + node: Node | undefined, + type: Type | undefined, + context: Context, + ) { + // What number types are possible? const possibleNumberTypes = type ?.getPossibleTypes(context) .filter( @@ -60,41 +70,71 @@ export default class NumberLiteral extends Literal { // If a type is provided, and it has a unit, suggest numbers with corresponding units. if (possibleNumberTypes && possibleNumberTypes.length > 0) { - return possibleNumberTypes.map((numberType) => - numberType.isLiteral() + return possibleNumberTypes.map((numberType) => { + const unit = + numberType.unit instanceof Unit + ? numberType.unit.clone() + : undefined; + return numberType.isLiteral() ? numberType.getLiteral() - : NumberLiteral.make( - 1, - numberType.unit instanceof Unit - ? numberType.unit.clone() - : undefined, - ), + : node instanceof NumberLiteral + ? node.withUnit(unit) + : NumberLiteral.make(1, unit); + }); + } + // No type provided, but there's a node? Suggest numbers with all possible units. + else if (node instanceof NumberLiteral) { + return getPossibleDimensions(context).map((dimension) => + NumberLiteral.make( + node.number.getText(), + new Unit(undefined, [Dimension.make(false, dimension, 1)]), + ), ); - } else { + } + // No type? Suggest some common numbers and hard to type numbers. + else return [ NumberLiteral.make(0, undefined, Sym.Decimal), NumberLiteral.make('π', undefined, Sym.Pi), NumberLiteral.make('∞', undefined, Sym.Infinity), ]; - } } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); + /** Replacing a node with another? Get numbers that match the expected type. */ + static getPossibleReplacements({ node, type, context }: ReplaceContext) { + return NumberLiteral.getPossibleNumbers(node, type, context); + } + + /** Inserting a number in a list? Get numbers that match the expected type. */ + static getPossibleInsertions({ type, context }: InsertContext) { + return NumberLiteral.getPossibleNumbers(undefined, type, context); } getDescriptor(): NodeDescriptor { return 'NumberLiteral'; } + getPurpose(): Purpose { + return Purpose.Numbers; + } + isPercent() { return this.number.getText().endsWith('%'); } getGrammar(): Grammar { return [ - { name: 'number', kind: node(Sym.Number), uncompletable: true }, - { name: 'unit', kind: optional(node(Unit)) }, + { + name: 'number', + kind: node(Sym.Number), + uncompletable: true, + label: undefined, + }, + { + name: 'unit', + kind: optional(node(Unit)), + label: () => (l) => l.term.unit, + }, ]; } @@ -122,6 +162,10 @@ export default class NumberLiteral extends Literal { return new NumberType(this.number, this.unit); } + withUnit(unit: Unit | undefined) { + return new NumberLiteral(this.number.clone(), unit); + } + getValue() { if (this.#numberCache) return new NumberValue( diff --git a/src/nodes/NumberType.ts b/src/nodes/NumberType.ts index 0beda2495..71ab39bf6 100644 --- a/src/nodes/NumberType.ts +++ b/src/nodes/NumberType.ts @@ -19,7 +19,7 @@ import type TypeSet from './TypeSet'; import UnaryEvaluate from './UnaryEvaluate'; import Unit from './Unit'; -type UnitDeriver = ( +export type UnitDeriver = ( left: Unit, right: Unit | undefined, constant: number | undefined, @@ -60,7 +60,7 @@ export default class NumberType extends BasisType { return [NumberType.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [NumberType.make()]; } @@ -75,8 +75,8 @@ export default class NumberType extends BasisType { getGrammar(): Grammar { return [ - { name: 'number', kind: node(Sym.NumberType) }, - { name: 'unit', kind: node(Unit) }, + { name: 'number', kind: node(Sym.NumberType), label: undefined }, + { name: 'unit', kind: node(Unit), label: undefined }, ]; } @@ -116,10 +116,10 @@ export default class NumberType extends BasisType { // See if all of the possible types are compatible. for (const possibleType of types.set) { - // Not a measurement type? Not compatible. + // Not a number type? Not compatible. if (!(possibleType instanceof NumberType)) return false; - // If it is a measurement type, get it's unit. + // If it is a number type, get it's unit. const thatUnit = possibleType.concreteUnit(context); // If this is a percent and the possible type has a unit, it's not compatible. @@ -132,7 +132,7 @@ export default class NumberType extends BasisType { ) return false; - // If the units aren't compatible, then the the types aren't compatible. + // If the units aren't compatible, then the types aren't compatible. if ( !(this.unit instanceof Function || this.unit.isUnitless()) && !thisUnit.accepts(thatUnit) diff --git a/src/nodes/Otherwise.ts b/src/nodes/Otherwise.ts index 9fcb1bd9e..852d5082b 100644 --- a/src/nodes/Otherwise.ts +++ b/src/nodes/Otherwise.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import { ImpossibleType } from '@conflicts/ImpossibleType'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { COALESCE_SYMBOL } from '@parser/Symbols'; @@ -41,7 +41,7 @@ export default class Otherwise extends SimpleExpression { this.computeChildren(); } - static getPossibleReplacements({ node }: EditContext) { + static getPossibleReplacements({ node }: ReplaceContext) { return node instanceof Expression ? [ Otherwise.make(node, ExpressionPlaceholder.make()), @@ -50,13 +50,8 @@ export default class Otherwise extends SimpleExpression { : []; } - static getPossibleAppends({ type }: EditContext) { - return [ - Otherwise.make( - ExpressionPlaceholder.make(type), - ExpressionPlaceholder.make(type), - ), - ]; + static getPossibleInsertions() { + return []; } static make(left: Expression, right: Expression) { @@ -73,9 +68,23 @@ export default class Otherwise extends SimpleExpression { getGrammar(): Grammar { return [ - { name: 'left', kind: node(Expression) }, - { name: 'question', kind: node(Sym.Otherwise), space: true }, - { name: 'right', kind: node(Expression), space: true }, + { + name: 'left', + kind: node(Expression), + label: () => (l) => l.term.value, + }, + { + name: 'question', + kind: node(Sym.Otherwise), + space: true, + label: undefined, + }, + { + name: 'right', + kind: node(Expression), + space: true, + label: () => (l) => l.term.value, + }, ]; } @@ -88,7 +97,7 @@ export default class Otherwise extends SimpleExpression { } getPurpose() { - return Purpose.Decide; + return Purpose.Decisions; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/Paragraph.ts b/src/nodes/Paragraph.ts index 42cddaee9..1eb0a953a 100644 --- a/src/nodes/Paragraph.ts +++ b/src/nodes/Paragraph.ts @@ -1,4 +1,5 @@ import type Conflict from '@conflicts/Conflict'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Node, { list, node } from '@nodes/Node'; @@ -40,12 +41,12 @@ export default class Paragraph extends Content { this.segments = segments; } - static getPossibleReplacements() { - return [new Paragraph([])]; + static getPossibleReplacements({ locales }: ReplaceContext) { + return [new Paragraph([Words.make(locales.get((l) => l.token.Words))])]; } - static getPossibleAppends() { - return [new Paragraph([])]; + static getPossibleInsertions({ locales }: InsertContext) { + return [new Paragraph([Words.make(locales.get((l) => l.token.Words))])]; } getDescriptor(): NodeDescriptor { @@ -67,6 +68,7 @@ export default class Paragraph extends Content { node(Mention), node(Branch), ), + label: () => (l) => l.term.markup, }, ]; } @@ -89,8 +91,14 @@ export default class Paragraph extends Content { return new Paragraph(segments); } + withSegmentInsertedAt(index: number, segment: Segment) { + const newSegments = [...this.segments]; + newSegments.splice(index, 0, segment); + return this.withSegments(newSegments); + } + getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.Paragraph; diff --git a/src/nodes/Previous.ts b/src/nodes/Previous.ts index bf09decfe..528338ae5 100644 --- a/src/nodes/Previous.ts +++ b/src/nodes/Previous.ts @@ -1,5 +1,5 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -19,7 +19,6 @@ import Characters from '../lore/BasisCharacters'; import AnyType from './AnyType'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import ListType from './ListType'; import { node, optional, type Grammar, type Replacement } from './Node'; import NoneType from './NoneType'; @@ -63,25 +62,14 @@ export default class Previous extends Expression { ); } - static getPossibleReplacements({ node, context }: EditContext) { - return node instanceof Expression && - node.getType(context).accepts(StreamType.make(), context) - ? [ - Previous.make( - node, - ExpressionPlaceholder.make(NumberType.make()), - ), - ] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { - return [ - Previous.make( - ExpressionPlaceholder.make(StreamType.make()), - ExpressionPlaceholder.make(NumberType.make()), - ), - ]; + static getPossibleInsertions({ parent, field }: InsertContext) { + return parent instanceof Previous && field === 'range' + ? [new Token(PREVIOUS_SYMBOL, Sym.Previous)] + : []; } getDescriptor(): NodeDescriptor { @@ -90,8 +78,12 @@ export default class Previous extends Expression { getGrammar(): Grammar { return [ - { name: 'previous', kind: node(Sym.Previous) }, - { name: 'range', kind: optional(node(Sym.Previous)) }, + { name: 'previous', kind: node(Sym.Previous), label: undefined }, + { + name: 'range', + kind: optional(node(Sym.Previous)), + label: (l) => (l) => l.node.Previous.label.range, + }, { name: 'number', kind: node(Expression), @@ -112,7 +104,7 @@ export default class Previous extends Expression { } getPurpose() { - return Purpose.Input; + return Purpose.Inputs; } clone(replace?: Replacement) { diff --git a/src/nodes/Program.ts b/src/nodes/Program.ts index bec4002e2..e9c545bb0 100644 --- a/src/nodes/Program.ts +++ b/src/nodes/Program.ts @@ -18,7 +18,6 @@ import Borrow from './Borrow'; import type Context from './Context'; import type Definition from './Definition'; import Dimension from './Dimension'; -import Docs from './Docs'; import Expression, { ExpressionKind } from './Expression'; import Language from './Language'; import type Node from './Node'; @@ -31,20 +30,13 @@ import type TypeSet from './TypeSet'; import Unit from './Unit'; export default class Program extends Expression { - readonly docs: Docs | undefined; readonly borrows: Borrow[]; readonly expression: Block; readonly end: Token | undefined; - constructor( - docs: Docs | undefined, - borrows: Borrow[], - expression: Block, - end: Token | undefined, - ) { + constructor(borrows: Borrow[], expression: Block, end: Token | undefined) { super(); - this.docs = docs; this.borrows = borrows.slice(); this.expression = expression; this.end = end; @@ -54,7 +46,6 @@ export default class Program extends Expression { static make(expressions: Expression[] = []) { return new Program( - undefined, [], new Block(expressions, BlockKind.Root), new Token('', Sym.End), @@ -67,20 +58,26 @@ export default class Program extends Expression { getGrammar(): Grammar { return [ - { name: 'docs', kind: optional(node(Docs)) }, - { name: 'borrows', kind: list(true, node(Borrow)) }, - { name: 'expression', kind: node(Block) }, - { name: 'end', kind: optional(node(Sym.End)) }, + { + name: 'borrows', + kind: list(true, node(Borrow)), + label: () => (l) => l.node.Program.label.borrows, + }, + { + name: 'expression', + kind: node(Block), + label: () => (l) => l.node.Program.label.expression, + }, + { name: 'end', kind: optional(node(Sym.End)), label: undefined }, ]; } getPurpose() { - return Purpose.Source; + return Purpose.Advanced; } clone(replace?: Replacement) { return new Program( - this.replaceChild('docs', this.docs, replace), this.replaceChild('borrows', this.borrows, replace), this.replaceChild('expression', this.expression, replace), this.replaceChild('end', this.end, replace), @@ -88,7 +85,7 @@ export default class Program extends Expression { } isEmpty() { - return this.leaves().length === 1; + return this.hasOneLeaf(); } isEvaluationInvolved() { diff --git a/src/nodes/PropertyBind.ts b/src/nodes/PropertyBind.ts index bcbe36dd3..a2cc075e2 100644 --- a/src/nodes/PropertyBind.ts +++ b/src/nodes/PropertyBind.ts @@ -1,6 +1,5 @@ import type Conflict from '@conflicts/Conflict'; import InvalidProperty from '@conflicts/InvalidProperty'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Evaluation from '@runtime/Evaluation'; @@ -23,10 +22,8 @@ import BindToken from './BindToken'; import type Context from './Context'; import { buildBindings } from './Evaluate'; import Expression from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import { node, type Grammar, type Replacement } from './Node'; import PropertyReference from './PropertyReference'; -import Reference from './Reference'; import StructureDefinitionType from './StructureDefinitionType'; import Sym from './Sym'; import type Token from './Token'; @@ -52,22 +49,12 @@ export default class PropertyBind extends Expression { return new PropertyBind(reference, new BindToken(), value); } - static getPossibleReplacements({ node, type }: EditContext) { - return node instanceof PropertyReference - ? [PropertyBind.make(node, ExpressionPlaceholder.make(type))] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends({ type }: EditContext) { - return [ - PropertyBind.make( - PropertyReference.make( - ExpressionPlaceholder.make(), - Reference.make('_'), - ), - ExpressionPlaceholder.make(type), - ), - ]; + static getPossibleInsertions() { + return []; } getDescriptor(): NodeDescriptor { @@ -76,9 +63,17 @@ export default class PropertyBind extends Expression { getGrammar(): Grammar { return [ - { name: 'reference', kind: node(PropertyReference) }, - { name: 'bind', kind: node(Sym.Bind) }, - { name: 'value', kind: node(Expression) }, + { + name: 'reference', + kind: node(PropertyReference), + label: () => (l) => l.node.PropertyBind.label.property, + }, + { name: 'bind', kind: node(Sym.Bind), label: undefined }, + { + name: 'value', + kind: node(Expression), + label: () => (l) => l.node.PropertyBind.label.value, + }, ]; } @@ -91,7 +86,7 @@ export default class PropertyBind extends Expression { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/PropertyReference.ts b/src/nodes/PropertyReference.ts index 303b9501d..e3fdd0a45 100644 --- a/src/nodes/PropertyReference.ts +++ b/src/nodes/PropertyReference.ts @@ -1,5 +1,6 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; +import Refer from '@edit/revision/Refer'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -12,7 +13,6 @@ import NameException from '@values/NameException'; import type Value from '@values/Value'; import Purpose from '../concepts/Purpose'; import { UnknownName } from '../conflicts/UnknownName'; -import Refer from '../edit/Refer'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import UnimplementedException from '../values/UnimplementedException'; @@ -21,7 +21,6 @@ import Bind from './Bind'; import type Context from './Context'; import type Definition from './Definition'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import FunctionDefinition from './FunctionDefinition'; import getGuards from './getGuards'; import NameType from './NameType'; @@ -63,15 +62,10 @@ export default class PropertyReference extends Expression { static getPossibleReferences( type: Type | undefined, - node: Node, - replace: boolean, + node: Node | undefined, context: Context, ) { - if (!replace) - return [ - PropertyReference.make(ExpressionPlaceholder.make(), undefined), - ]; - else if (node instanceof PropertyReference) { + if (node instanceof PropertyReference) { const selectionType = node.structure.getType(context); const definition = selectionType instanceof StructureType @@ -120,6 +114,7 @@ export default class PropertyReference extends Expression { def.getEvaluateTemplate( name, context, + true, node.structure, ), def, @@ -134,12 +129,12 @@ export default class PropertyReference extends Expression { return []; } - static getPossibleReplacements({ type, node, context }: EditContext) { - return this.getPossibleReferences(type, node, true, context); + static getPossibleReplacements({ type, node, context }: ReplaceContext) { + return this.getPossibleReferences(type, node, context); } - static getPossibleAppends({ type, node, context }: EditContext) { - return this.getPossibleReferences(type, node, false, context); + static getPossibleInsertions({ type, parent, context }: InsertContext) { + return this.getPossibleReferences(type, parent, context); } getDescriptor(): NodeDescriptor { @@ -148,13 +143,19 @@ export default class PropertyReference extends Expression { getGrammar(): Grammar { return [ - { name: 'structure', kind: node(Expression) }, - { name: 'dot', kind: node(Sym.Access) }, + { + name: 'structure', + kind: node(Expression), + label: (locales, context) => { + return () => this.getSubjectType(context).getLabel(locales); + }, + }, + { name: 'dot', kind: node(Sym.Access), label: undefined }, { name: 'name', kind: node(Reference), // The label is - label: () => (l) => l.node.PropertyReference.property, + label: () => (l) => l.node.PropertyReference.label.property, // The valid definitions of the name are based on the referenced structure type, prefix filtered by whatever name is already provided. getDefinitions: (context: Context) => { let defs = this.getDefinitions(this, context); @@ -183,7 +184,7 @@ export default class PropertyReference extends Expression { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/Reaction.ts b/src/nodes/Reaction.ts index 8f0249905..32b07ecf1 100644 --- a/src/nodes/Reaction.ts +++ b/src/nodes/Reaction.ts @@ -1,5 +1,5 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Check from '@runtime/Check'; @@ -69,8 +69,9 @@ export default class Reaction extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Expression + static getPossibleReplacements({ node, type }: ReplaceContext) { + // Wrap the stream in a reaction if replacing an expression with a stream type. + return node instanceof Expression && type instanceof StreamType ? [ Reaction.make( node, @@ -83,12 +84,8 @@ export default class Reaction extends Expression { : []; } - static getPossibleAppends() { - return Reaction.make( - ExpressionPlaceholder.make(), - Changed.make(ExpressionPlaceholder.make(StreamType.make())), - ExpressionPlaceholder.make(), - ); + static getPossibleInsertions() { + return []; } isUndelimited() { @@ -104,14 +101,19 @@ export default class Reaction extends Expression { { name: 'initial', kind: node(Expression), - label: () => (l) => l.node.Reaction.initial, + label: () => (l) => l.node.Reaction.label.initial, + }, + { + name: 'dots', + kind: node(Sym.Stream), + space: true, + label: undefined, }, - { name: 'dots', kind: node(Sym.Stream), space: true }, { name: 'condition', kind: node(Expression), space: true, - label: () => (l) => l.node.Reaction.condition, + label: () => (l) => l.node.Reaction.label.condition, getType: () => BooleanType.make(), }, { @@ -119,11 +121,12 @@ export default class Reaction extends Expression { kind: node(Sym.Stream), space: true, indent: true, + label: undefined, }, { name: 'next', kind: node(Expression), - label: () => (l) => l.node.Reaction.next, + label: () => (l) => l.node.Reaction.label.next, space: true, indent: true, }, @@ -145,7 +148,7 @@ export default class Reaction extends Expression { } getPurpose() { - return Purpose.Input; + return Purpose.Inputs; } getAffiliatedType(): BasisTypeName | undefined { @@ -160,11 +163,13 @@ export default class Reaction extends Expression { if (!(conditionType instanceof BooleanType)) conflicts.push(new ExpectedBooleanCondition(this, conditionType)); + // At least one dependency of the condition must be a stream. if ( !Array.from(this.condition.getAllDependencies(context)).some( (node) => context.getStreamType(node.getType(context)) !== undefined, - ) + ) && + context.getStreamType(this.condition.getType(context)) === undefined ) conflicts.push(new ExpectedStream(this)); @@ -172,18 +177,21 @@ export default class Reaction extends Expression { } computeType(context: Context): Type { + // Get the union of all of the types in the initial and next expressions. const type = UnionType.getPossibleUnion(context, [ this.initial.getType(context), this.next.getType(context), ]).generalize(context); - // If the type includes an unknown type because of a cycle or some other unknown type, remove the unknown, since the rest of the type defines the possible values. - const types = type.getTypeSet(context).list(); - const cycle = types.findIndex((type) => type instanceof UnknownType); - if (cycle >= 0) { - types.splice(cycle, 1); - return UnionType.getPossibleUnion(context, types); - } else return type; + // If the type includes an unknown type because of a cycle or some other unknown type, + // remove them, since the rest of the type defines the possible values. + return UnionType.getPossibleUnion( + context, + type + .getTypeSet(context) + .list() + .filter((t) => !(t instanceof UnknownType)), + ); } getDependencies(): Expression[] { diff --git a/src/nodes/Reference.ts b/src/nodes/Reference.ts index 40c498995..097e44fde 100644 --- a/src/nodes/Reference.ts +++ b/src/nodes/Reference.ts @@ -2,7 +2,8 @@ import type Conflict from '@conflicts/Conflict'; import ReferenceCycle from '@conflicts/ReferenceCycle'; import { UnexpectedTypeVariable } from '@conflicts/UnexpectedTypeVariable'; import { UnknownName } from '@conflicts/UnknownName'; -import type EditContext from '@edit/EditContext'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; +import Refer from '@edit/revision/Refer'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -12,10 +13,11 @@ import type Step from '@runtime/Step'; import NameException from '@values/NameException'; import type Value from '@values/Value'; import Purpose from '../concepts/Purpose'; -import Refer from '../edit/Refer'; import type Locales from '../locale/Locales'; import { type TemplateInput } from '../locale/Locales'; +import BinaryEvaluate from './BinaryEvaluate'; import Bind from './Bind'; +import Borrow from './Borrow'; import type Context from './Context'; import type Definition from './Definition'; import Expression, { type GuardContext } from './Expression'; @@ -25,15 +27,19 @@ import getGuards from './getGuards'; import NameToken from './NameToken'; import type Node from './Node'; import { ListOf, node, type Grammar, type Replacement } from './Node'; +import PropertyReference from './PropertyReference'; import Reaction from './Reaction'; import SimpleExpression from './SimpleExpression'; +import Source from './Source'; import StreamDefinition from './StreamDefinition'; +import StreamType from './StreamType'; import StructureDefinition from './StructureDefinition'; import Sym from './Sym'; -import type Token from './Token'; +import Token from './Token'; import Type from './Type'; import type TypeSet from './TypeSet'; import TypeVariable from './TypeVariable'; +import UnaryEvaluate from './UnaryEvaluate'; import UnionType from './UnionType'; import UnknownNameType from './UnknownNameType'; @@ -70,6 +76,9 @@ export default class Reference extends SimpleExpression { /** The context of the edit */ context: Context, ): Refer[] { + // Find the parent. + const parent = reference.getParent(context); + // A matching function to see if a definition matches const match = (def: Definition, prefix: string, name: string) => def.getNames().find((n) => n.startsWith(prefix)) ?? name; @@ -83,22 +92,39 @@ export default class Reference extends SimpleExpression { // If the anchor is being replaced but isn't a reference, suggest nothing. // Otherwise, suggest references in the anchor node's scope that complete the prefix. return ( - reference - // Find all the definitions in scope. - .getDefinitionsInScope(context) - // Only accept definitions that have a matching name. + [ + // Find all the definitions in scope. If the anchor happens to be a property reference and we're completing, + // only find definitions in it's scope. + ...(reference instanceof PropertyReference && complete + ? reference.getDefinitions(reference, context) + : reference.getDefinitionsInScope(context)), + // Find all the sources in scope if the context is a borrow. + ...(reference instanceof Borrow + ? context.project + .getSupplements() + .filter((s) => s !== context.source) + : []), + ] + // If there's a prefix we're completing, include .filter( (def) => prefix === undefined || - def.getNames().some((name) => - // Hello - name.startsWith(prefix), - ), + def.getNames().some((name) => name.startsWith(prefix)), ) // Translate the definitions into References, or to the definitions. .map((definition) => { - // Bind of acceptible type? Make a reference. + // Is the function an operator? That affects how we name it. + const isOperator = + definition instanceof FunctionDefinition && + definition.isOperator(); + // Is the type of the definition coming from a stream? We might generate a reference to the stream itself. + const streamType = !(definition instanceof TypeVariable) + ? context.getStreamType(definition.getType(context)) + : undefined; if ( + // A source? + definition instanceof Source || + // Bind of acceptible type? Make a reference. (definition instanceof Bind && (type === undefined || type.accepts( @@ -106,20 +132,37 @@ export default class Reference extends SimpleExpression { .getType(context) .generalize(context), context, - ))) || // A function type that matches the function? + ))) || + // If this definition replaced the current one and it's concrete types, would it be of an acceptable type? (type instanceof FunctionType && definition instanceof FunctionDefinition && - type.accepts(definition.getType(context), context)) - ) + definition + .getType(context) + .accepts(type, context) && + // Only accept definitions with symbolic names if a binary evaluate. + ((!(parent instanceof BinaryEvaluate) && + !(parent instanceof UnaryEvaluate)) || + definition.names.hasOperatorName())) || + // If the type is a StreamType and the definition is a stream with a matching type, suggest + (type instanceof StreamType && + streamType !== undefined && + type.accepts(streamType, context)) + ) { return new Refer( (name) => - Reference.make( - prefix - ? match(definition, prefix, name) - : name, + new Reference( + new Token( + // Completing? Pass the prefix to find a matching name. + prefix + ? match(definition, prefix, name) + : name, + isOperator ? Sym.Operator : Sym.Name, + ), ), definition, + isOperator, ); + } // If the anchor is in list field, and the anchor is not being replaced, offer (Binary/Unary)Evaluate in scope. else if ( complete && @@ -143,6 +186,7 @@ export default class Reference extends SimpleExpression { ? match(definition, prefix, name) : name, context, + true, undefined, ), definition, @@ -165,6 +209,7 @@ export default class Reference extends SimpleExpression { ? match(definition, prefix, name) : name, context, + true, ), definition, ); @@ -176,12 +221,12 @@ export default class Reference extends SimpleExpression { ); } - static getPossibleReplacements({ type, node, context }: EditContext) { - return this.getPossibleReferences(type, node, true, context); + static getPossibleReplacements({ type, node, context }: ReplaceContext) { + return this.getPossibleReferences(type, node, false, context); } - static getPossibleAppends({ type, node, context }: EditContext) { - return this.getPossibleReferences(type, node, true, context); + static getPossibleInsertions({ type, parent, context }: InsertContext) { + return this.getPossibleReferences(type, parent, false, context); } getDescriptor(): NodeDescriptor { @@ -205,7 +250,7 @@ export default class Reference extends SimpleExpression { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } clone(replace?: Replacement) { @@ -298,7 +343,6 @@ export default class Reference extends SimpleExpression { if (definition === undefined || definition instanceof TypeVariable) return new UnknownNameType(this, this.name, undefined); - // What is the type of the definition? const type = definition.getType(context); // Otherwise, do some type guard analyis on the definition. diff --git a/src/nodes/Root.ts b/src/nodes/Root.ts index 5d98d43ff..1582b5895 100644 --- a/src/nodes/Root.ts +++ b/src/nodes/Root.ts @@ -1,5 +1,6 @@ import Expression from './Expression'; import type Node from './Node'; +import type { Field } from './Node'; export type Path = { type: string; index: number }[]; type Cache = { @@ -189,4 +190,9 @@ export default class Root { // in which case the path doesn't resolve. this.resolvePath(path.slice(1), child); } + + /** Get the field of a node */ + getFieldOfChild(child: Node): Field | undefined { + return this.getParent(child)?.getFieldOfChild(child); + } } diff --git a/src/nodes/Row.ts b/src/nodes/Row.ts index bd60e2831..e8a1d2d78 100644 --- a/src/nodes/Row.ts +++ b/src/nodes/Row.ts @@ -59,13 +59,15 @@ export default class Row extends Node { node(Sym.Delete), node(Sym.Update), ), + label: undefined, }, { name: 'cells', kind: list(true, node(Input), node(Expression)), space: true, + label: () => (l) => l.term.cell, }, - { name: 'close', kind: node(Sym.TableClose) }, + { name: 'close', kind: node(Sym.TableClose), label: undefined }, ]; } @@ -78,7 +80,7 @@ export default class Row extends Node { } getPurpose() { - return Purpose.Bind; + return Purpose.Tables; } getDependencies() { diff --git a/src/nodes/Select.ts b/src/nodes/Select.ts index 064ebb94e..689714e55 100644 --- a/src/nodes/Select.ts +++ b/src/nodes/Select.ts @@ -1,7 +1,7 @@ import type Conflict from '@conflicts/Conflict'; import ExpectedSelectName from '@conflicts/ExpectedSelectName'; import UnknownColumn from '@conflicts/UnknownColumn'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -99,12 +99,8 @@ export default class Select extends Expression { ]; } - static getPossibleReplacements({ node, context }: EditContext) { - const anchorType = - node instanceof Expression ? node.getType(context) : undefined; - const tableType = - anchorType instanceof TableType ? anchorType : undefined; - return node instanceof Expression && tableType + static getPossibleReplacements({ node, type }: ReplaceContext) { + return node instanceof Expression && type instanceof TableType ? [ Select.make( node, @@ -114,17 +110,12 @@ export default class Select extends Expression { : []; } - static getPossibleAppends() { - return [ - Select.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - ]; + static getPossibleInsertions() { + return []; } getPurpose() { - return Purpose.Value; + return Purpose.Tables; } clone(replace?: Replacement) { diff --git a/src/nodes/SetLiteral.ts b/src/nodes/SetLiteral.ts index 76be48b75..3e4afb600 100644 --- a/src/nodes/SetLiteral.ts +++ b/src/nodes/SetLiteral.ts @@ -1,4 +1,3 @@ -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { MAX_LINE_LENGTH } from '@parser/Spaces'; @@ -16,6 +15,7 @@ import UnclosedDelimiter from '../conflicts/UnclosedDelimiter'; import type Locales from '../locale/Locales'; import Characters from '../lore/BasisCharacters'; import AnyType from './AnyType'; +import CompositeLiteral from './CompositeLiteral'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; import { list, node, type Grammar, type Replacement } from './Node'; @@ -27,7 +27,7 @@ import type Type from './Type'; import type TypeSet from './TypeSet'; import UnionType from './UnionType'; -export default class SetLiteral extends Expression { +export default class SetLiteral extends CompositeLiteral { readonly open: Token; readonly values: Expression[]; readonly close: Token | undefined; @@ -57,13 +57,11 @@ export default class SetLiteral extends Expression { ); } - static getPossibleReplacements({ node }: EditContext) { - return node instanceof Expression - ? [SetLiteral.make(), SetLiteral.make([node])] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [SetLiteral.make()]; } @@ -73,10 +71,11 @@ export default class SetLiteral extends Expression { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.SetOpen) }, + { name: 'open', kind: node(Sym.SetOpen), label: undefined }, { name: 'values', kind: list(true, node(Expression)), + label: () => (l) => l.term.value, // Only allow types to be inserted that are of the list's type, if provided. getType: (context) => this.getItemType(context) ?? new AnyType(), @@ -85,8 +84,13 @@ export default class SetLiteral extends Expression { initial: true, indent: true, }, - { name: 'close', kind: node(Sym.SetClose), newline: this.wrap() }, - { name: 'literal', kind: node(Sym.Literal) }, + { + name: 'close', + kind: node(Sym.SetClose), + newline: this.wrap(), + label: undefined, + }, + { name: 'literal', kind: node(Sym.Literal), label: undefined }, ]; } @@ -109,7 +113,7 @@ export default class SetLiteral extends Expression { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Hidden; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/SetOrMapAccess.ts b/src/nodes/SetOrMapAccess.ts index 672f3b5be..130e7b828 100644 --- a/src/nodes/SetOrMapAccess.ts +++ b/src/nodes/SetOrMapAccess.ts @@ -1,7 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import { IncompatibleKey } from '@conflicts/IncompatibleKey'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -22,7 +21,6 @@ import Bind from './Bind'; import BooleanType from './BooleanType'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import getGuards from './getGuards'; import MapType from './MapType'; import { node, type Grammar, type Replacement } from './Node'; @@ -70,23 +68,12 @@ export default class SetOrMapAccess extends Expression { ); } - static getPossibleReplacements({ node, context }: EditContext) { - if (!(node instanceof Expression)) return []; - return node.getType(context).accepts(SetType.make(), context) || - node.getType(context).accepts(MapType.make(), context) - ? [SetOrMapAccess.make(node, ExpressionPlaceholder.make())] - : []; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { - return [ - SetOrMapAccess.make( - ExpressionPlaceholder.make( - UnionType.make(SetType.make(), MapType.make()), - ), - ExpressionPlaceholder.make(), - ), - ]; + static getPossibleInsertions() { + return []; } getDescriptor(): NodeDescriptor { @@ -102,13 +89,13 @@ export default class SetOrMapAccess extends Expression { // Must be a number getType: () => UnionType.make(SetType.make(), MapType.make()), }, - { name: 'open', kind: node(Sym.SetOpen) }, + { name: 'open', kind: node(Sym.SetOpen), label: undefined }, { name: 'key', kind: node(Expression), label: () => (l) => l.term.key, }, - { name: 'close', kind: node(Sym.SetClose) }, + { name: 'close', kind: node(Sym.SetClose), label: undefined }, ]; } @@ -122,7 +109,7 @@ export default class SetOrMapAccess extends Expression { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Maps; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/SetType.ts b/src/nodes/SetType.ts index d455fc284..c3ff8138d 100644 --- a/src/nodes/SetType.ts +++ b/src/nodes/SetType.ts @@ -1,6 +1,6 @@ import type Conflict from '@conflicts/Conflict'; import UnclosedDelimiter from '@conflicts/UnclosedDelimiter'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -37,15 +37,15 @@ export default class SetType extends BasisType { return new SetType(new SetOpenToken(), key, new SetCloseToken()); } - static getPossibleReplacements({ node }: EditContext) { + static getPossibleReplacements({ node }: ReplaceContext) { return [ SetType.make(), ...(node instanceof Type ? [SetType.make(node)] : []), ]; } - static getPossibleAppends() { - return SetType.make(); + static getPossibleInsertions() { + return [SetType.make()]; } getDescriptor(): NodeDescriptor { @@ -54,9 +54,13 @@ export default class SetType extends BasisType { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.SetOpen) }, - { name: 'key', kind: optional(node(Type)) }, - { name: 'close', kind: node(Sym.SetClose) }, + { name: 'open', kind: node(Sym.SetOpen), label: undefined }, + { + name: 'key', + kind: optional(node(Type)), + label: () => (l) => l.term.type, + }, + { name: 'close', kind: node(Sym.SetClose), label: undefined }, ]; } diff --git a/src/nodes/Source.ts b/src/nodes/Source.ts index dca0e85a2..404e2b6ff 100644 --- a/src/nodes/Source.ts +++ b/src/nodes/Source.ts @@ -110,6 +110,10 @@ export default class Source extends Expression { return new Source(Names.make([mainName]), ''); } + getParentNode(node: Node): Node | undefined { + return this.root.getParent(node); + } + /** Used by Evaluator to get the steps for the evaluation of this source file. */ getEvaluationSteps(evaluator: Evaluator, context: Context): Step[] { return this.expression.compile(evaluator, context); @@ -124,6 +128,7 @@ export default class Source extends Expression { { name: 'expression', kind: node(Program), + label: undefined, space: false, indent: false, }, @@ -463,6 +468,7 @@ export default class Source extends Expression { ? index - this.spaces.getSpace(token).length : undefined; } + getTokenLastPosition(token: Token) { const index = this.getTokenTextPosition(token); return index !== undefined ? index + token.getTextLength() : undefined; @@ -494,6 +500,21 @@ export default class Source extends Expression { return position; } + /** Get the last position of the end of the given line */ + getEndOfLine(line: number): number | undefined { + let currentLine = 0; + for (let index = 0; index < this.code.getLength(); index++) { + const char = this.code.at(index); + if (char === '\n') { + if (currentLine === line) return index; + currentLine++; + } + } + // If we reached the end of the code, and we're on the target line, return the end of the code. + if (currentLine === line) return this.code.getLength(); + return undefined; + } + /** Given a node in this source, return the line the node is on */ getLine(position: number | Node): number | undefined { if (position instanceof Node) { @@ -516,6 +537,39 @@ export default class Source extends Expression { } } + /** Given a node and field name, return the position of the field in the source. */ + getFieldPosition(parent: Node, field: string): number | undefined { + // Get position of the parent by iterating through its children and finding the first + // field set. Then, iterate through the fields to find the field before the target field, + // we so we can find the last position of the last token in the field before. That is the position. + const grammar = parent.getGrammar(); + const targetField = grammar.find((f) => f.name === field); + if (targetField === undefined) return undefined; // Field not found in grammar + const targetFieldIndex = grammar.indexOf(targetField); + if (targetFieldIndex === -1) return undefined; // Field not found in grammar + // If the target field is first, return the position of the first token in the parent. + if (targetFieldIndex === 0) { + const firstToken = parent.leaves()[0]; + return firstToken + ? this.getTokenTextPosition(firstToken) + : this.getNodeFirstPosition(parent); + } else { + for (let i = targetFieldIndex + 1; i < grammar.length; i++) { + const siblingOrList = parent.getField(grammar[i].name); + const sibling = Array.isArray(siblingOrList) + ? siblingOrList[0] + : siblingOrList; + if (sibling) { + const firstSiblingToken = sibling.leaves()[0]; + return firstSiblingToken + ? this.getTokenTextPosition(firstSiblingToken) + : undefined; + } + } + } + return undefined; + } + scanLines( checker: ( line: number, @@ -815,7 +869,7 @@ export default class Source extends Expression { return this.getTokenTextPosition(firstToken); const tokenBefore = this.getTokenBeforeNode(node); return tokenBefore === undefined - ? undefined + ? 0 : this.getTokenLastPosition(tokenBefore); } @@ -825,10 +879,21 @@ export default class Source extends Expression { return this.getTokenLastPosition(lastToken); const tokenAfter = this.getTokenAfterNode(node); return tokenAfter === undefined - ? undefined + ? this.code.getLength() : this.getTokenTextPosition(tokenAfter); } + getRange(node: Node): [number, number] | undefined { + const tokens = node.nodes((t): t is Token => t instanceof Token); + const first = tokens[0]; + const last = tokens[tokens.length - 1]; + const firstIndex = this.getTokenTextPosition(first); + const lastIndex = this.getTokenLastPosition(last); + return firstIndex === undefined || lastIndex === undefined + ? undefined + : [firstIndex, lastIndex]; + } + getFirstToken(node: Node): Token | undefined { return node.nodes().filter((n): n is Token => n instanceof Token)[0]; } @@ -939,6 +1004,6 @@ export default class Source extends Expression { } getPurpose() { - return Purpose.Source; + return Purpose.Advanced; } } diff --git a/src/nodes/Spread.ts b/src/nodes/Spread.ts index e04942e92..08e24d2b3 100644 --- a/src/nodes/Spread.ts +++ b/src/nodes/Spread.ts @@ -1,4 +1,4 @@ -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -10,7 +10,6 @@ import { BIND_SYMBOL } from '../parser/Symbols'; import AnyType from './AnyType'; import type Context from './Context'; import Expression from './Expression'; -import ExpressionPlaceholder from './ExpressionPlaceholder'; import ListType from './ListType'; import type { Grammar, Replacement } from './Node'; import Node, { node, optional } from './Node'; @@ -35,15 +34,15 @@ export default class Spread extends Node { return new Spread(new Token(BIND_SYMBOL, Sym.Bind), list); } - static getPossibleReplacements({ node, context }: EditContext) { + static getPossibleReplacements({ node, context }: ReplaceContext) { return node instanceof Expression && node.getType(context).accepts(ListType.make(), context) ? [Spread.make(node)] : []; } - static getPossibleAppends() { - return [Spread.make(ExpressionPlaceholder.make())]; + static getPossibleInsertions() { + return []; } getDescriptor(): NodeDescriptor { @@ -52,7 +51,7 @@ export default class Spread extends Node { getGrammar(): Grammar { return [ - { name: 'dots', kind: node(Sym.Bind) }, + { name: 'dots', kind: node(Sym.Bind), label: undefined }, { name: 'list', kind: optional(node(Expression)), @@ -70,7 +69,7 @@ export default class Spread extends Node { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Lists; } getAffiliatedType(): BasisTypeName | undefined { diff --git a/src/nodes/StreamDefinition.ts b/src/nodes/StreamDefinition.ts index c4e055d24..d506f1ac3 100644 --- a/src/nodes/StreamDefinition.ts +++ b/src/nodes/StreamDefinition.ts @@ -42,7 +42,7 @@ import TypeToken from './TypeToken'; import { getEvaluationInputConflicts } from './util'; export default class StreamDefinition extends DefinitionExpression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly dots: Token; readonly names: Names; readonly open: Token | undefined; @@ -65,7 +65,7 @@ export default class StreamDefinition extends DefinitionExpression { ) { super(); - this.docs = docs; + this.docs = docs ?? Docs.make(); this.names = names; this.dots = dots; this.open = open; @@ -110,33 +110,40 @@ export default class StreamDefinition extends DefinitionExpression { getGrammar(): Grammar { return [ - { name: 'docs', kind: optional(node(Docs)) }, - { name: 'dots', kind: node(Sym.Stream) }, - { name: 'names', kind: node(Names) }, - { name: 'open', kind: node(Sym.EvalOpen) }, + { + name: 'docs', + kind: optional(node(Docs)), + label: () => (l) => l.term.documentation, + }, + { name: 'dots', kind: node(Sym.Stream), label: undefined }, + { name: 'names', kind: node(Names), label: undefined }, + { name: 'open', kind: node(Sym.EvalOpen), label: undefined }, { name: 'inputs', kind: list(true, node(Bind)), space: true, indent: true, + label: () => (l) => l.term.input, }, - { name: 'close', kind: node(Sym.EvalClose) }, + { name: 'close', kind: node(Sym.EvalClose), label: undefined }, { name: 'dot', kind: any( node(Sym.Type), none(['output', () => TypePlaceholder.make()]), ), + label: undefined, }, { name: 'output', kind: any(node(Type), none(['dot', () => new TypeToken()])), + label: () => (l) => l.term.type, }, ]; } getPurpose() { - return Purpose.Input; + return Purpose.Hidden; } clone(replace?: Replacement) { @@ -153,7 +160,11 @@ export default class StreamDefinition extends DefinitionExpression { ) as this; } - getEvaluateTemplate(nameOrLocales: string | Locales) { + getEvaluateTemplate( + nameOrLocales: string | Locales, + context: Context, + defaults: boolean, + ): Evaluate { return Evaluate.make( Reference.make( typeof nameOrLocales === 'string' @@ -166,7 +177,10 @@ export default class StreamDefinition extends DefinitionExpression { this.inputs .filter((input) => !input.hasDefault()) .map((input) => - ExpressionPlaceholder.make(input.type?.clone()), + defaults && input.type !== undefined + ? (input.type.getDefaultExpression(context) ?? + ExpressionPlaceholder.make(input.type.clone())) + : ExpressionPlaceholder.make(input.type?.clone()), ), ); } diff --git a/src/nodes/StreamDefinitionType.ts b/src/nodes/StreamDefinitionType.ts index b9a480433..6002f0a74 100644 --- a/src/nodes/StreamDefinitionType.ts +++ b/src/nodes/StreamDefinitionType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -21,6 +22,10 @@ export default class StreamDefinitionType extends Type { return 'StreamDefinitionType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar() { return []; } diff --git a/src/nodes/StreamType.ts b/src/nodes/StreamType.ts index 26dabfec3..8d08ba2a0 100644 --- a/src/nodes/StreamType.ts +++ b/src/nodes/StreamType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { STREAM_SYMBOL } from '@parser/Symbols'; @@ -37,10 +38,14 @@ export default class StreamType extends Type { return 'StreamType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar(): Grammar { return [ - { name: 'stream', kind: node(Sym.Stream) }, - { name: 'type', kind: node(Type) }, + { name: 'stream', kind: node(Sym.Stream), label: undefined }, + { name: 'type', kind: node(Type), label: () => (l) => l.term.type }, ]; } diff --git a/src/nodes/StructureDefinition.ts b/src/nodes/StructureDefinition.ts index ecbdc17c6..3425adffc 100644 --- a/src/nodes/StructureDefinition.ts +++ b/src/nodes/StructureDefinition.ts @@ -44,15 +44,15 @@ import TypeVariables from './TypeVariables'; import { getEvaluationInputConflicts } from './util'; export default class StructureDefinition extends DefinitionExpression { - readonly docs: Docs | undefined; + readonly docs: Docs; readonly share: Token | undefined; readonly type: Token; readonly names: Names; readonly interfaces: Reference[]; readonly types: TypeVariables | undefined; - readonly open: Token | undefined; + readonly open: Token; readonly inputs: Bind[]; - readonly close: Token | undefined; + readonly close: Token; readonly expression: Block | undefined; // PERF: Cache definitions to avoid having to recreate the list. @@ -65,14 +65,14 @@ export default class StructureDefinition extends DefinitionExpression { names: Names, interfaces: Reference[], types: TypeVariables | undefined, - open: Token | undefined, + open: Token, inputs: Bind[], - close: Token | undefined, + close: Token, block?: Block, ) { super(); - this.docs = docs; + this.docs = docs ?? Docs.make(); this.share = share; this.type = type; this.names = names; @@ -113,7 +113,7 @@ export default class StructureDefinition extends DefinitionExpression { return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [ StructureDefinition.make( undefined, @@ -140,33 +140,46 @@ export default class StructureDefinition extends DefinitionExpression { getGrammar(): Grammar { return [ - { name: 'docs', kind: optional(node(Docs)) }, + { + name: 'docs', + kind: optional(node(Docs)), + label: () => (l) => l.node.StructureDefinition.label.docs, + }, { name: 'share', kind: optional(node(Sym.Share)), getToken: () => new Token(SHARE_SYMBOL, Sym.Share), + label: undefined, }, - { name: 'type', kind: node(Sym.Type) }, - { name: 'names', kind: node(Names) }, + { name: 'type', kind: node(Sym.Type), label: undefined }, + { name: 'names', kind: node(Names), label: undefined }, { name: 'interfaces', kind: list(true, node(Reference)), space: true, + label: () => (l) => l.node.StructureDefinition.label.interfaces, + }, + { + name: 'types', + kind: optional(node(TypeVariables)), + space: true, + label: undefined, }, - { name: 'types', kind: optional(node(TypeVariables)), space: true }, - { name: 'open', kind: node(Sym.EvalOpen) }, + { name: 'open', kind: node(Sym.EvalOpen), label: undefined }, { name: 'inputs', kind: list(true, node(Bind)), space: true, indent: true, + label: () => (l) => l.node.StructureDefinition.label.inputs, }, - { name: 'close', kind: node(Sym.EvalClose) }, + { name: 'close', kind: node(Sym.EvalClose), label: undefined }, { name: 'expression', kind: optional(node(Block)), space: true, indent: !(this.expression instanceof Block), + label: () => (l) => l.node.StructureDefinition.label.expression, }, ]; } @@ -187,7 +200,7 @@ export default class StructureDefinition extends DefinitionExpression { } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } getNames() { @@ -210,7 +223,11 @@ export default class StructureDefinition extends DefinitionExpression { return true; } - getEvaluateTemplate(nameOrLocales: Locales | string, context: Context) { + getEvaluateTemplate( + nameOrLocales: Locales | string, + context: Context, + defaults: boolean, + ): Evaluate | ExpressionPlaceholder { // In case for some reason an input of this refers to this. if (context.visited(this)) return ExpressionPlaceholder.make(); context.visit(this); @@ -225,8 +242,10 @@ export default class StructureDefinition extends DefinitionExpression { .filter((input) => !input.hasDefault()) .map((input) => input.type - ? (input.type.getDefaultExpression(context) ?? - ExpressionPlaceholder.make(input.type)) + ? defaults + ? (input.type.getDefaultExpression(context) ?? + ExpressionPlaceholder.make(input.type.clone())) + : ExpressionPlaceholder.make(input.type.clone()) : ExpressionPlaceholder.make(), ), ); diff --git a/src/nodes/StructureDefinitionType.ts b/src/nodes/StructureDefinitionType.ts index 4b05e2782..d4c9850c4 100644 --- a/src/nodes/StructureDefinitionType.ts +++ b/src/nodes/StructureDefinitionType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -22,6 +23,10 @@ export default class StructureDefinitionType extends Type { return 'StructureDefinitionType'; } + getPurpose(): Purpose { + return Purpose.Types; + } + getGrammar() { return []; } diff --git a/src/nodes/StructureType.ts b/src/nodes/StructureType.ts index 5bc07db30..5ef934937 100644 --- a/src/nodes/StructureType.ts +++ b/src/nodes/StructureType.ts @@ -184,6 +184,7 @@ export default class StructureType extends BasisType { return def.getEvaluateTemplate( context.getBasis().locales, context, + true, this, ); } diff --git a/src/nodes/Sym.ts b/src/nodes/Sym.ts index dc6a46467..44dce1671 100644 --- a/src/nodes/Sym.ts +++ b/src/nodes/Sym.ts @@ -20,11 +20,11 @@ enum Sym { FormattedType = '`…`', Words = 'words', Link = '@', - Italic = '//', - Underline = '__', - Light = '~~', - Bold = '**', - Extra = '^^', + Italic = '/', + Underline = '_', + Light = '~', + Bold = '*', + Extra = '^', Concept = '@concept', URL = 'http...', Mention = '$', @@ -32,7 +32,6 @@ enum Sym { None = 'ø', Type = '•', Literal = '!', - TypeOperator = '•op', TypeOpen = '⸨', TypeClose = '⸩', Separator = ',', @@ -42,7 +41,7 @@ enum Sym { NumberType = '•#', JapaneseNumeral = '#jpn', RomanNumeral = '#rom', - Pi = '#pi', + Pi = 'π', Infinity = '∞', TableOpen = '⎡', TableClose = '⎦', diff --git a/src/nodes/TableLiteral.ts b/src/nodes/TableLiteral.ts index bb9a49404..7959c534d 100644 --- a/src/nodes/TableLiteral.ts +++ b/src/nodes/TableLiteral.ts @@ -1,8 +1,6 @@ import type Conflict from '@conflicts/Conflict'; -import type EditContext from '@edit/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; -import { PLACEHOLDER_SYMBOL } from '@parser/Symbols'; import type Evaluator from '@runtime/Evaluator'; import Finish from '@runtime/Finish'; import Start from '@runtime/Start'; @@ -21,6 +19,7 @@ import StructureValue from '../values/StructureValue'; import Bind from './Bind'; import BooleanLiteral from './BooleanLiteral'; import BooleanType from './BooleanType'; +import CompositeLiteral from './CompositeLiteral'; import type Context from './Context'; import Expression, { type GuardContext } from './Expression'; import Input from './Input'; @@ -40,7 +39,7 @@ import type Type from './Type'; import type TypeSet from './TypeSet'; import UnionType from './UnionType'; -export default class TableLiteral extends Expression { +export default class TableLiteral extends CompositeLiteral { readonly type: TableType; readonly rows: Row[]; @@ -165,28 +164,12 @@ export default class TableLiteral extends Expression { return new TableLiteral(type, rows); } - static getPossibleReplacements({ node }: EditContext) { - return [ - TableLiteral.make( - TableType.make(), - node instanceof Expression ? [Row.make([node])] : undefined, - ), - ]; + static getPossibleReplacements() { + return []; } - static getPossibleAppends() { - return [ - TableLiteral.make( - TableType.make([ - Bind.make( - undefined, - Names.make([PLACEHOLDER_SYMBOL]), - NumberType.make(), - ), - ]), - [Row.make([NumberLiteral.make(1)])], - ), - ]; + static getPossibleInsertions() { + return [TableLiteral.make()]; } getDescriptor(): NodeDescriptor { @@ -200,12 +183,17 @@ export default class TableLiteral extends Expression { kind: node(TableType), label: () => (l) => l.term.table, }, - { name: 'rows', kind: list(true, node(Row)), newline: true }, + { + name: 'rows', + kind: list(true, node(Row)), + newline: true, + label: () => (l) => l.term.row, + }, ]; } getPurpose() { - return Purpose.Value; + return Purpose.Hidden; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/TableType.ts b/src/nodes/TableType.ts index d91ce625f..1c95bce05 100644 --- a/src/nodes/TableType.ts +++ b/src/nodes/TableType.ts @@ -53,7 +53,7 @@ export default class TableType extends BasisType { return [TableType.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [TableType.make()]; } @@ -63,9 +63,14 @@ export default class TableType extends BasisType { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.TableOpen) }, - { name: 'columns', kind: list(true, node(Bind)), space: true }, - { name: 'close', kind: node(Sym.TableClose) }, + { name: 'open', kind: node(Sym.TableOpen), label: undefined }, + { + name: 'columns', + kind: list(true, node(Bind)), + label: () => (l) => l.term.column, + space: true, + }, + { name: 'close', kind: node(Sym.TableClose), label: undefined }, ]; } diff --git a/src/nodes/TextLiteral.ts b/src/nodes/TextLiteral.ts index 5e9d3e129..9b65b6ed9 100644 --- a/src/nodes/TextLiteral.ts +++ b/src/nodes/TextLiteral.ts @@ -1,4 +1,5 @@ -import type EditContext from '@edit/EditContext'; +import Purpose from '@concepts/Purpose'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LanguageCode from '@locale/LanguageCode'; import type Locale from '@locale/Locale'; import type LocaleText from '@locale/LocaleText'; @@ -47,7 +48,7 @@ export default class TextLiteral extends Literal { return new TextLiteral([Translation.make(text ?? '', language)]); } - static getPossibleReplacements({ type, context }: EditContext) { + static getPossibleText(type: Type | undefined, context: Context) { // Is the type one or more literal text types? Suggest those. Otherwise just suggest an empty text literal. const types = type ? type @@ -59,16 +60,30 @@ export default class TextLiteral extends Literal { : [TextLiteral.make()]; } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); + static getPossibleReplacements({ type, context }: ReplaceContext) { + return this.getPossibleText(type, context); + } + + static getPossibleInsertions({ type, context }: InsertContext) { + return this.getPossibleText(type, context); } getDescriptor(): NodeDescriptor { return 'TextLiteral'; } + getPurpose() { + return Purpose.Text; + } + getGrammar(): Grammar { - return [{ name: 'texts', kind: list(false, node(Translation)) }]; + return [ + { + name: 'texts', + kind: list(false, node(Translation)), + label: () => (l) => l.node.TextLiteral.label.texts, + }, + ]; } clone(replace?: Replacement): this { diff --git a/src/nodes/TextType.ts b/src/nodes/TextType.ts index 5a4ac3285..6880ed05d 100644 --- a/src/nodes/TextType.ts +++ b/src/nodes/TextType.ts @@ -50,7 +50,7 @@ export default class TextType extends BasisType { return [TextType.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [TextType.make()]; } @@ -60,10 +60,14 @@ export default class TextType extends BasisType { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.Text) }, - { name: 'text', kind: node(Sym.Words) }, - { name: 'close', kind: node(Sym.Text) }, - { name: 'language', kind: optional(node(Language)) }, + { name: 'open', kind: node(Sym.Text), label: undefined }, + { name: 'text', kind: node(Sym.Words), label: undefined }, + { name: 'close', kind: node(Sym.Text), label: undefined }, + { + name: 'language', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, ]; } diff --git a/src/nodes/This.ts b/src/nodes/This.ts index 530ce6103..9fb48ac09 100644 --- a/src/nodes/This.ts +++ b/src/nodes/This.ts @@ -8,7 +8,7 @@ import type Type from './Type'; import type TypeSet from './TypeSet'; import { MisplacedThis } from '@conflicts/MisplacedThis'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import StartFinish from '@runtime/StartFinish'; @@ -45,7 +45,7 @@ export default class This extends SimpleExpression { return new This(new Token(PROPERTY_SYMBOL, Sym.Access)); } - static getPossibleReplacements({ node, context }: EditContext) { + static getPossibleReplacements({ node, context }: ReplaceContext) { return context .getRoot(node) ?.getAncestors(node) @@ -59,8 +59,8 @@ export default class This extends SimpleExpression { : []; } - static getPossibleAppends(context: EditContext) { - return this.getPossibleReplacements(context); + static getPossibleInsertions() { + return []; } getDescriptor(): NodeDescriptor { @@ -68,11 +68,11 @@ export default class This extends SimpleExpression { } getGrammar(): Grammar { - return [{ name: 'dis', kind: node(Sym.This) }]; + return [{ name: 'dis', kind: node(Sym.This), label: undefined }]; } getPurpose() { - return Purpose.Bind; + return Purpose.Definitions; } clone(replace?: Replacement) { diff --git a/src/nodes/Token.ts b/src/nodes/Token.ts index 15994c58c..01854af99 100644 --- a/src/nodes/Token.ts +++ b/src/nodes/Token.ts @@ -67,7 +67,8 @@ export default class Token extends Node { } getPurpose() { - return Purpose.Value; + // Purpose depends on the token type. + return Purpose.Hidden; } // TOKEN TYPES diff --git a/src/nodes/Translation.ts b/src/nodes/Translation.ts index e2846c74f..41fc25354 100644 --- a/src/nodes/Translation.ts +++ b/src/nodes/Translation.ts @@ -43,35 +43,50 @@ export default class Translation extends LanguageTagged { this.close = close; this.separator = separator; - /** Unescape the text string */ - this.computeChildren(); } static make(text?: string, language?: Language) { return new Translation( new Token("'", Sym.Text), - [new Token(text ?? '', Sym.Words)], + text && text.length > 0 ? [new Token(text, Sym.Words)] : [], new Token("'", Sym.Text), language, undefined, ); } + static getPossibleReplacements() { + return []; + } + + static getPossibleInsertions() { + return [this.make('')]; + } + getDescriptor(): NodeDescriptor { return 'Translation'; } getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.Text) }, + { name: 'open', kind: node(Sym.Text), label: undefined }, { name: 'segments', kind: list(true, node(Sym.Words), node(Example)), + label: () => (l) => l.node.Translation.label.segments, + }, + { name: 'close', kind: node(Sym.Text), label: undefined }, + { + name: 'language', + kind: optional(node(Language)), + label: () => (l) => l.term.language, + }, + { + name: 'separator', + kind: optional(node(Sym.Separator)), + label: undefined, }, - { name: 'close', kind: node(Sym.Text) }, - { name: 'language', kind: optional(node(Language)) }, - { name: 'separator', kind: optional(node(Sym.Separator)) }, ]; } @@ -86,7 +101,7 @@ export default class Translation extends LanguageTagged { } getPurpose(): Purpose { - return Purpose.Value; + return Purpose.Text; } static ConceptRegExPattern = new RegExp(ConceptRegExPattern, 'ug'); diff --git a/src/nodes/Type.ts b/src/nodes/Type.ts index c3b1bb18d..ca26b7de1 100644 --- a/src/nodes/Type.ts +++ b/src/nodes/Type.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { BasisTypeName } from '../basis/BasisConstants'; -import Purpose from '../concepts/Purpose'; import type Context from './Context'; import type ConversionDefinition from './ConversionDefinition'; import type Expression from './Expression'; @@ -13,10 +12,6 @@ export default abstract class Type extends Node { super(); } - getPurpose() { - return Purpose.Type; - } - /** * True if the given type can be bound to this type, in the given program context. * Gets all possible types of the given type, then asks the subclass to check them all. diff --git a/src/nodes/TypeInputs.ts b/src/nodes/TypeInputs.ts index faa841d1c..c3fe3ea4e 100644 --- a/src/nodes/TypeInputs.ts +++ b/src/nodes/TypeInputs.ts @@ -36,7 +36,7 @@ export default class TypeInputs extends Node { return [TypeInputs.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [TypeInputs.make()]; } @@ -45,14 +45,22 @@ export default class TypeInputs extends Node { } getPurpose() { - return Purpose.Type; + return Purpose.Advanced; } getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.TypeOpen) }, - { name: 'types', kind: list(true, node(Type)) }, - { name: 'close', kind: optional(node(Sym.TypeClose)) }, + { name: 'open', kind: node(Sym.TypeOpen), label: undefined }, + { + name: 'types', + kind: list(true, node(Type)), + label: () => (l) => l.term.type, + }, + { + name: 'close', + kind: optional(node(Sym.TypeClose)), + label: undefined, + }, ]; } diff --git a/src/nodes/TypePlaceholder.ts b/src/nodes/TypePlaceholder.ts index c8c465daa..d9840b8a1 100644 --- a/src/nodes/TypePlaceholder.ts +++ b/src/nodes/TypePlaceholder.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import Placeholder from '@conflicts/Placeholder'; import type LocaleText from '@locale/LocaleText'; @@ -29,7 +30,7 @@ export default class TypePlaceholder extends Type { return [TypePlaceholder.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [TypePlaceholder.make()]; } @@ -37,6 +38,10 @@ export default class TypePlaceholder extends Type { return 'TypePlaceholder'; } + getPurpose() { + return Purpose.Advanced; + } + getGrammar(): Grammar { return [ { diff --git a/src/nodes/TypeVariable.ts b/src/nodes/TypeVariable.ts index 4add255ba..36f9af7a9 100644 --- a/src/nodes/TypeVariable.ts +++ b/src/nodes/TypeVariable.ts @@ -47,17 +47,19 @@ export default class TypeVariable extends Node { getGrammar(): Grammar { return [ - { name: 'names', kind: node(Names) }, + { name: 'names', kind: node(Names), label: undefined }, { name: 'dot', kind: any( node(Sym.Type), none(['type', () => TypePlaceholder.make()]), ), + label: undefined, }, { name: 'type', kind: any(node(Type), none(['dot', () => new TypeToken()])), + label: () => (l) => l.node.TypeVariable.label.type, }, ]; } @@ -71,7 +73,7 @@ export default class TypeVariable extends Node { } getPurpose() { - return Purpose.Type; + return Purpose.Advanced; } simplify() { diff --git a/src/nodes/TypeVariables.ts b/src/nodes/TypeVariables.ts index 92b2d4ffd..e329777e7 100644 --- a/src/nodes/TypeVariables.ts +++ b/src/nodes/TypeVariables.ts @@ -39,7 +39,7 @@ export default class TypeVariables extends Node { return [TypeVariables.make()]; } - static getPossibleAppends() { + static getPossibleInsertions() { return [TypeVariables.make()]; } @@ -49,9 +49,9 @@ export default class TypeVariables extends Node { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.TypeOpen) }, - { name: 'variables', kind: node(Names) }, - { name: 'close', kind: node(Sym.TypeClose) }, + { name: 'open', kind: node(Sym.TypeOpen), label: undefined }, + { name: 'variables', kind: node(Names), label: undefined }, + { name: 'close', kind: node(Sym.TypeClose), label: undefined }, ]; } @@ -72,7 +72,7 @@ export default class TypeVariables extends Node { } getPurpose() { - return Purpose.Type; + return Purpose.Advanced; } computeConflicts() { diff --git a/src/nodes/UnaryEvaluate.ts b/src/nodes/UnaryEvaluate.ts index 6c9de5bb2..5e0da9136 100644 --- a/src/nodes/UnaryEvaluate.ts +++ b/src/nodes/UnaryEvaluate.ts @@ -49,7 +49,7 @@ export default class UnaryEvaluate extends Expression { return []; } - static getPossibleAppends() { + static getPossibleInsertions() { return []; } @@ -65,13 +65,25 @@ export default class UnaryEvaluate extends Expression { getDefinitions: (context: Context): Definition[] => { return this.getFunctions(context); }, + label: undefined, + /** + * The expected function type of this binary evaluate is whether function it resolves to, but + * concretized with the actual types of the left and right inputs, as that determines what it could be replaced with. + */ + getType: (context) => + this.getFunction(context)?.getType(context) ?? + new AnyType(), + }, + { + name: 'input', + kind: node(Expression), + label: () => (l) => l.node.UnaryEvaluate.label.input, }, - { name: 'input', kind: node(Expression) }, ]; } getPurpose() { - return Purpose.Evaluate; + return Purpose.Advanced; } clone(replace?: Replacement) { diff --git a/src/nodes/UnimplementedType.ts b/src/nodes/UnimplementedType.ts deleted file mode 100644 index a293bb8e1..000000000 --- a/src/nodes/UnimplementedType.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type Locales from '../locale/Locales'; -import type Expression from './Expression'; -import UnknownType from './UnknownType'; - -export default class UnimplementedType extends UnknownType { - constructor(expression: Expression) { - super(expression, undefined); - } - - getReason(locales: Locales) { - return locales.concretize((l) => l.node.NotImplementedType.name); - } -} diff --git a/src/nodes/UnionType.ts b/src/nodes/UnionType.ts index a2316d79d..f7a204629 100644 --- a/src/nodes/UnionType.ts +++ b/src/nodes/UnionType.ts @@ -1,4 +1,5 @@ -import type EditContext from '@edit/EditContext'; +import Purpose from '@concepts/Purpose'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { OR_SYMBOL } from '@parser/Symbols'; @@ -39,13 +40,13 @@ export default class UnionType extends Type { return new UnionType(left, new Token(OR_SYMBOL, Sym.Union), right); } - static getPossibleReplacements({ node }: EditContext) { + static getPossibleReplacements({ node }: ReplaceContext) { return node instanceof Type ? [UnionType.make(node, TypePlaceholder.make())] : []; } - static getPossibleAppends() { + static getPossibleInsertions() { return [UnionType.make(TypePlaceholder.make(), TypePlaceholder.make())]; } @@ -57,11 +58,23 @@ export default class UnionType extends Type { return 'UnionType'; } + getPurpose(): Purpose { + return Purpose.Types; + } + getGrammar(): Grammar { return [ - { name: 'left', kind: node(Type) }, - { name: 'or', kind: node(Sym.Union) }, - { name: 'right', kind: node(Type) }, + { name: 'left', kind: node(Type), label: () => (l) => l.term.type }, + { + name: 'or', + kind: node(Sym.Union), + label: undefined, + }, + { + name: 'right', + kind: node(Type), + label: () => (l) => l.term.type, + }, ]; } diff --git a/src/nodes/Unit.ts b/src/nodes/Unit.ts index c8fdf6ce2..66c70bafb 100644 --- a/src/nodes/Unit.ts +++ b/src/nodes/Unit.ts @@ -1,4 +1,6 @@ -import type EditContext from '@edit/EditContext'; +import Purpose from '@concepts/Purpose'; +import { getPossibleDimensions } from '@edit/menu/getPossibleUnits'; +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import { @@ -8,29 +10,37 @@ import { } from '@parser/Symbols'; import NumberValue from '@values/NumberValue'; import type { BasisTypeName } from '../basis/BasisConstants'; -import { getPossibleDimensions } from '../edit/getPossibleUnits'; import type Locales from '../locale/Locales'; import Emotion from '../lore/Emotion'; import Dimension from './Dimension'; import LanguageToken from './LanguageToken'; -import { list, node, optional, type Grammar, type Replacement } from './Node'; +import Node, { + list, + node, + optional, + type Grammar, + type Replacement, +} from './Node'; import Sym from './Sym'; import Token from './Token'; -import Type from './Type'; import type TypeSet from './TypeSet'; -export default class Unit extends Type { +export default class Unit extends Node { /** In case this was parsed, we keep the original tokens around. */ readonly numerator: Dimension[]; readonly slash: Token | undefined; readonly denominator: Dimension[]; - /** We store units internally as a map from unit names to a positive or negative non-zero exponent. */ - readonly exponents: Map; + /** + * We store units internally as a map from unit names to a positive or negative non-zero exponent. + * We only instantiate this map once, in the constructor, and it is immutable after that. + * It remains undefined if unitless, to save on memory. + */ + readonly exponents: Map | undefined; constructor( exponents: undefined | Map = undefined, - numerator?: Dimension[] | undefined, + numerator?: Dimension[], slash?: Token, denominator?: Dimension[], ) { @@ -47,36 +57,42 @@ export default class Unit extends Type { : slash; this.denominator = denominator ?? []; - this.exponents = new Map(); - - for (const dim of this.numerator) { - const name = dim.getName(); - if (name !== undefined) { - const exp = - dim.exponent === undefined - ? 1 - : NumberValue.fromToken(dim.exponent)[0].toNumber(); - const current = this.exponents.get(name); - this.exponents.set(name, (current ?? 0) + exp); + if (this.numerator.length === 0 && this.denominator.length === 0) + this.exponents = undefined; + else { + this.exponents = new Map(); + + for (const dim of this.numerator) { + const name = dim.getName(); + if (name !== undefined) { + const exp = + dim.exponent === undefined + ? 1 + : NumberValue.fromToken( + dim.exponent, + )[0].toNumber(); + const current = this.exponents.get(name); + this.exponents.set(name, (current ?? 0) + exp); + } } - } - for (const dim of this.denominator) { - const name = dim.getName(); - if (name !== undefined) { - const exp = - dim.exponent === undefined - ? -1 - : -NumberValue.fromToken( - dim.exponent, - )[0].toNumber(); - const current = this.exponents.get(name); - this.exponents.set(name, (current ?? 0) + exp); + for (const dim of this.denominator) { + const name = dim.getName(); + if (name !== undefined) { + const exp = + dim.exponent === undefined + ? -1 + : -NumberValue.fromToken( + dim.exponent, + )[0].toNumber(); + const current = this.exponents.get(name); + this.exponents.set(name, (current ?? 0) + exp); + } } - } - // Eliminate any 0 exponent units. - for (const [unit, exp] of this.exponents) - if (exp === 0) this.exponents.delete(unit); + // Eliminate any 0 exponent units. + for (const [unit, exp] of this.exponents) + if (exp === 0) this.exponents.delete(unit); + } } else { // Start as empty. this.numerator = []; @@ -123,7 +139,7 @@ export default class Unit extends Type { this.computeChildren(); } - static getPossibleReplacements({ node, context }: EditContext) { + static getPossibleReplacements({ node, context }: ReplaceContext) { // What dimensions are possible? const dimensions = getPossibleDimensions(context); @@ -143,7 +159,7 @@ export default class Unit extends Type { : []; } - static getPossibleAppends({ context }: EditContext) { + static getPossibleInsertions({ context }: InsertContext) { return getPossibleDimensions(context).map((dim) => Unit.create([dim])); } @@ -153,11 +169,27 @@ export default class Unit extends Type { return 'Unit'; } + getPurpose(): Purpose { + return Purpose.Numbers; + } + getGrammar(): Grammar { return [ - { name: 'numerator', kind: list(true, node(Dimension)) }, - { name: 'slash', kind: optional(node(Sym.Language)) }, - { name: 'denominator', kind: list(true, node(Dimension)) }, + { + name: 'numerator', + kind: list(true, node(Dimension)), + label: () => (l) => l.term.unit, + }, + { + name: 'slash', + kind: optional(node(Sym.Language)), + label: undefined, + }, + { + name: 'denominator', + kind: list(true, node(Dimension)), + label: () => (l) => l.term.unit, + }, ]; } @@ -185,18 +217,20 @@ export default class Unit extends Type { return exponents; } - static reuse(numerator: string[], denominator: string[] = []) { + static reuse(numerator: string[] = [], denominator: string[] = []) { return Unit.get(Unit.map(numerator, denominator)); } - static create(numerator: string[], denominator: string[] = []) { + static create(numerator: string[] = [], denominator: string[] = []) { return new Unit(Unit.map(numerator, denominator)); } /** A unit pool, since they recur so frequently. We map the exponents to a unique string */ static Pool = new Map(); - static get(exponents: Map) { + static get(exponents: Map | undefined) { + if (exponents === undefined || exponents.size === 0) return Unit.Empty; + // Convert the exponents to a canonical string let hash = ''; for (const key of Array.from(exponents.keys()).sort()) @@ -212,18 +246,27 @@ export default class Unit extends Type { return newUnit; } + isEmpty() { + return this.numerator.length === 0 && this.denominator.length === 0; + } + isUnitless() { - return this.exponents.size === 0; + return this.size() === 0; + } + + size() { + return this.exponents?.size ?? 0; } isEqualTo(unit: Unit) { - return ( - unit instanceof Unit && - this.exponents.size === unit.exponents.size && - Array.from(this.exponents.keys()).every( - (key) => this.exponents.get(key) === unit.exponents.get(key), - ) - ); + if (!(unit instanceof Unit)) return false; + if (this.size() !== unit.size()) return false; + if (this.exponents !== undefined && unit.exponents !== undefined) + for (const key of this.exponents.keys()) { + if (this.exponents.get(key) !== unit.exponents.get(key)) + return false; + } + return true; } computeConflicts() { @@ -261,7 +304,7 @@ export default class Unit extends Type { ]); } - accepts(unit: Type): boolean { + accepts(unit: Unit): boolean { // Every key in this exists in the given unit and they have the same exponents. return ( // Is this a unit? @@ -285,13 +328,10 @@ export default class Unit extends Type { } toString(depth?: number) { - const units = Array.from(this.exponents.keys()).sort(); - const numerator = units.filter( - (unit) => (this.exponents.get(unit) ?? 0) > 0, - ); - const denominator = units.filter( - (unit) => (this.exponents.get(unit) ?? 0) < 0, - ); + const exp = this.exponents === undefined ? new Map() : this.exponents; + const units = Array.from(exp.keys()).sort(); + const numerator = units.filter((unit) => (exp.get(unit) ?? 0) > 0); + const denominator = units.filter((unit) => (exp.get(unit) ?? 0) < 0); return ( (depth === undefined ? '' : '\t'.repeat(depth)) + @@ -299,10 +339,8 @@ export default class Unit extends Type { .map( (unit) => `${unit}${ - (this.exponents.get(unit) ?? 0) > 1 - ? `${EXPONENT_SYMBOL}${this.exponents.get( - unit, - )}` + (exp.get(unit) ?? 0) > 1 + ? `${EXPONENT_SYMBOL}${exp.get(unit)}` : '' }`, ) @@ -312,9 +350,9 @@ export default class Unit extends Type { .map( (unit) => `${unit}${ - (this.exponents.get(unit) ?? 0) < -1 + (exp.get(unit) ?? 0) < -1 ? `${EXPONENT_SYMBOL}${Math.abs( - this.exponents.get(unit) ?? 0, + exp.get(unit) ?? 0, )}` : '' }`, @@ -324,6 +362,8 @@ export default class Unit extends Type { } root(root: number) { + if (this.exponents === undefined) return this; + const newExponents = new Map(); // Subtract one from every unit's exponent, and if it would be zero, set it to -1. @@ -340,7 +380,7 @@ export default class Unit extends Type { const newExponents = new Map(this.exponents); // Add the given units' exponents to the existing exponents - for (const [unit, exponent] of operand.exponents) { + for (const [unit, exponent] of operand.exponents ?? new Map()) { const currentExponent = newExponents.get(unit); newExponents.set(unit, (currentExponent ?? 0) + exponent); } @@ -352,7 +392,7 @@ export default class Unit extends Type { const newExponents = new Map(this.exponents); // Subtract the given units' exponents from the existing exponents - for (const [unit, exponent] of operand.exponents) { + for (const [unit, exponent] of operand.exponents ?? new Map()) { const currentExponent = newExponents.get(unit); newExponents.set(unit, (currentExponent ?? 0) - exponent); } @@ -365,7 +405,7 @@ export default class Unit extends Type { const newExponents = new Map(this.exponents); // Multiply the units by the power. - for (const [unit, exp] of this.exponents) { + for (const [unit, exp] of this.exponents ?? new Map()) { newExponents.set(unit, exp * exponent); } @@ -383,7 +423,7 @@ export default class Unit extends Type { getDescriptionInputs(locales: Locales) { return [ - this.exponents.size === 0 + this.isUnitless() ? locales.get((l) => l.basis.Number.name[0]) : this.toWordplay(), ]; diff --git a/src/nodes/UnknownType.ts b/src/nodes/UnknownType.ts index d17b90315..f5a5793da 100644 --- a/src/nodes/UnknownType.ts +++ b/src/nodes/UnknownType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -29,6 +30,10 @@ export default abstract class UnknownType< return 'UnknownType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar(): Grammar { return []; } diff --git a/src/nodes/UnparsableExpression.ts b/src/nodes/UnparsableExpression.ts index 7ede88f76..0ca961fab 100644 --- a/src/nodes/UnparsableExpression.ts +++ b/src/nodes/UnparsableExpression.ts @@ -32,11 +32,17 @@ export default class UnparsableExpression extends SimpleExpression { } getGrammar(): Grammar { - return [{ name: 'unparsables', kind: list(true, node(Node)) }]; + return [ + { + name: 'unparsables', + kind: list(true, node(Node)), + label: undefined, + }, + ]; } getPurpose() { - return Purpose.Source; + return Purpose.Hidden; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/UnparsableType.ts b/src/nodes/UnparsableType.ts index b9bf6afed..6f23aaf2f 100644 --- a/src/nodes/UnparsableType.ts +++ b/src/nodes/UnparsableType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type Conflict from '@conflicts/Conflict'; import { UnparsableConflict } from '@conflicts/UnparsableConflict'; import type LocaleText from '@locale/LocaleText'; @@ -22,6 +23,10 @@ export default class UnparsableType extends Type { return 'UnparsableType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + acceptsAll(): boolean { return false; } @@ -31,7 +36,13 @@ export default class UnparsableType extends Type { } getGrammar(): Grammar { - return [{ name: 'unparsables', kind: list(true, node(Node)) }]; + return [ + { + name: 'unparsables', + kind: list(true, node(Node)), + label: undefined, + }, + ]; } computeConflicts(context: Context): Conflict[] { diff --git a/src/nodes/Update.ts b/src/nodes/Update.ts index 1d6f68f6f..0f03c9054 100644 --- a/src/nodes/Update.ts +++ b/src/nodes/Update.ts @@ -2,7 +2,7 @@ import type Conflict from '@conflicts/Conflict'; import ExpectedColumnBind from '@conflicts/ExpectedColumnBind'; import IncompatibleCellType from '@conflicts/IncompatibleCellType'; import UnknownColumn from '@conflicts/UnknownColumn'; -import type EditContext from '@edit/EditContext'; +import type { ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import NodeRef from '@locale/NodeRef'; import type { NodeDescriptor } from '@locale/NodeTexts'; @@ -98,12 +98,8 @@ export default class Update extends Expression { ]; } - static getPossibleReplacements({ node, context }: EditContext) { - const anchorType = - node instanceof Expression ? node.getType(context) : undefined; - const tableType = - anchorType instanceof TableType ? anchorType : undefined; - return node instanceof Expression && tableType + static getPossibleReplacements({ node, context, type }: ReplaceContext) { + return node instanceof Expression && type instanceof TableType ? [ Update.make( node, @@ -113,13 +109,8 @@ export default class Update extends Expression { : []; } - static getPossibleAppends() { - return [ - Update.make( - ExpressionPlaceholder.make(TableType.make()), - ExpressionPlaceholder.make(BooleanType.make()), - ), - ]; + static getPossibleInsertions() { + return []; } clone(replace?: Replacement) { @@ -131,7 +122,7 @@ export default class Update extends Expression { } getPurpose() { - return Purpose.Value; + return Purpose.Tables; } getScopeOfChild(child: Node, context: Context): Node | undefined { diff --git a/src/nodes/VariableType.ts b/src/nodes/VariableType.ts index ef84b78df..0fe835d33 100644 --- a/src/nodes/VariableType.ts +++ b/src/nodes/VariableType.ts @@ -1,3 +1,4 @@ +import Purpose from '@concepts/Purpose'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import type { BasisTypeName } from '../basis/BasisConstants'; @@ -19,6 +20,10 @@ export default class VariableType extends Type { return 'VariableType'; } + getPurpose(): Purpose { + return Purpose.Hidden; + } + getGrammar(): Grammar { return []; } diff --git a/src/nodes/WebLink.ts b/src/nodes/WebLink.ts index 964197622..4242f9092 100644 --- a/src/nodes/WebLink.ts +++ b/src/nodes/WebLink.ts @@ -1,3 +1,4 @@ +import type { InsertContext, ReplaceContext } from '@edit/revision/EditContext'; import type LocaleText from '@locale/LocaleText'; import type { NodeDescriptor } from '@locale/NodeTexts'; import Purpose from '../concepts/Purpose'; @@ -46,12 +47,22 @@ export default class WebLink extends Content { ); } - static getPossibleReplacements() { - return [WebLink.make('...', 'https://')]; + static getPossibleReplacements({ locales }: ReplaceContext) { + return [ + WebLink.make( + locales.get((l) => l.node.WebLink.name), + 'https://', + ), + ]; } - static getPossibleAppends() { - return [WebLink.make('...', 'https://')]; + static getPossibleInsertions({ locales }: InsertContext) { + return [ + WebLink.make( + locales.get((l) => l.node.WebLink.name), + 'https://', + ), + ]; } getDescriptor(): NodeDescriptor { @@ -60,11 +71,15 @@ export default class WebLink extends Content { getGrammar(): Grammar { return [ - { name: 'open', kind: node(Sym.TagOpen) }, - { name: 'description', kind: node(Sym.Words) }, - { name: 'at', kind: node(Sym.Link) }, - { name: 'url', kind: node(Sym.URL) }, - { name: 'close', kind: node(Sym.TagClose) }, + { name: 'open', kind: node(Sym.TagOpen), label: undefined }, + { + name: 'description', + kind: node(Sym.Words), + label: () => (l) => l.term.markup, + }, + { name: 'at', kind: node(Sym.Link), label: undefined }, + { name: 'url', kind: node(Sym.URL), label: () => (l) => 'url' }, + { name: 'close', kind: node(Sym.TagClose), label: undefined }, ]; } @@ -83,7 +98,7 @@ export default class WebLink extends Content { } getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.WebLink; diff --git a/src/nodes/Words.ts b/src/nodes/Words.ts index 29c7ead1a..aa140ba7f 100644 --- a/src/nodes/Words.ts +++ b/src/nodes/Words.ts @@ -48,8 +48,12 @@ export default class Words extends Content { this.close = close; } - static make() { - return new Words(undefined, [new Token('…', Sym.Words)], undefined); + static make(text?: string) { + return new Words( + undefined, + [new Token(text ?? '…', Sym.Words)], + undefined, + ); } getDescriptor(): NodeDescriptor { @@ -66,8 +70,9 @@ export default class Words extends Content { node(Sym.Light), node(Sym.Bold), node(Sym.Extra), - none(['close', () => new Token(Sym.Italic, Sym.Italic)]), + none(['close', (close) => close.clone()]), ), + label: undefined, }, { name: 'segments', @@ -81,6 +86,7 @@ export default class Words extends Content { node(Mention), node(Branch), ), + label: () => (l) => l.term.markup, }, { name: 'close', @@ -90,8 +96,9 @@ export default class Words extends Content { node(Sym.Light), node(Sym.Bold), node(Sym.Extra), - none(['open', () => new Token(Sym.Italic, Sym.Italic)]), + none(['open', (open) => open.clone()]), ), + label: undefined, }, ]; } @@ -103,7 +110,15 @@ export default class Words extends Content { clone(replace?: Replacement | undefined): this { return new Words( this.replaceChild('open', this.open, replace), - this.replaceChild('segments', this.getNodeSegments(), replace), + this.replaceChild( + 'segments', + // We have to branch here because otherwise, we don't pass the original list to replaceChild(), which + // breaks replacements that target the list. + this.segments.every((n) => n instanceof Node) + ? this.segments + : this.getNodeSegments(), + replace, + ), this.replaceChild('close', this.close, replace), ) as this; } @@ -116,8 +131,14 @@ export default class Words extends Content { return new Words(this.open, segments, this.close); } + withSegmentInsertedAt(index: number, segment: Segment) { + const newSegments = [...this.segments]; + newSegments.splice(index, 0, segment); + return this.withSegments(newSegments); + } + getPurpose() { - return Purpose.Document; + return Purpose.Documentation; } static readonly LocalePath = (l: LocaleText) => l.node.Words; diff --git a/src/parser/Parser.test.ts b/src/parser/Parser.test.ts index 0b6a8d673..0c5fffe17 100644 --- a/src/parser/Parser.test.ts +++ b/src/parser/Parser.test.ts @@ -7,6 +7,7 @@ import BooleanType from '@nodes/BooleanType'; import Borrow from '@nodes/Borrow'; import Conditional from '@nodes/Conditional'; import ConversionDefinition from '@nodes/ConversionDefinition'; +import ConversionType from '@nodes/ConversionType'; import Convert from '@nodes/Convert'; import Doc from '@nodes/Doc'; import DocumentedExpression from '@nodes/DocumentedExpression'; @@ -52,6 +53,7 @@ import UnparsableExpression from '@nodes/UnparsableExpression'; import UnparsableType from '@nodes/UnparsableType'; import Update from '@nodes/Update'; import WebLink from '@nodes/WebLink'; +import Words from '@nodes/Words'; import { expect, test } from 'vitest'; import Delete from '../nodes/Delete'; import Docs from '../nodes/Docs'; @@ -276,6 +278,7 @@ test.each([ ['a•…#', Bind, 'type', StreamType], ['a•Cat|#', Bind, 'type', UnionType], ['a•`…`', Bind, 'type', FormattedType], + ['a•→# ""', Bind, 'type', ConversionType], ['a•/', Bind, 'type', UnparsableType], ])( '%s -> %o', @@ -349,7 +352,7 @@ test('plain docs', () => { const doc = parseDoc(toTokens('¶this is what I am.¶')); expect(doc).toBeInstanceOf(Doc); expect(doc.markup.paragraphs[0]).toBeInstanceOf(Paragraph); - expect(doc.markup.paragraphs[0].segments[0]).toBeInstanceOf(Token); + expect(doc.markup.paragraphs[0].segments[0]).toBeInstanceOf(Words); expect(doc.markup.paragraphs[0].segments.length).toBe(1); }); @@ -369,10 +372,12 @@ test('linked docs', () => { ); expect(doc).toBeInstanceOf(Doc); expect(doc.markup.paragraphs[0]).toBeInstanceOf(Paragraph); - expect(doc.markup.paragraphs[0].segments[1]).toBeInstanceOf(WebLink); - expect( - (doc.markup.paragraphs[0].segments[1] as WebLink).url?.getText(), - ).toBe('https://wikipedia.org'); + const words = doc.markup.paragraphs[0].segments[0]; + expect(words).toBeInstanceOf(Words); + expect((words as Words).segments[1]).toBeInstanceOf(WebLink); + expect(((words as Words).segments[1] as WebLink).url?.getText()).toBe( + 'https://wikipedia.org', + ); }); test('docs in docs', () => { @@ -381,10 +386,11 @@ test('docs in docs', () => { ); expect(doc).toBeInstanceOf(Doc); expect(doc.markup.paragraphs[0]).toBeInstanceOf(Paragraph); - expect(doc.markup.paragraphs[0].segments[0]).toBeInstanceOf(Token); - expect(doc.markup.paragraphs[0].segments[1]).toBeInstanceOf(Example); - expect(doc.markup.paragraphs[0].segments[2]).toBeInstanceOf(Token); - expect(doc.markup.paragraphs[0].segments.length).toBe(3); + const words = doc.markup.paragraphs[0].segments[0]; + expect(words).toBeInstanceOf(Words); + expect((words as Words).segments[1]).toBeInstanceOf(Example); + expect((words as Words).segments[2]).toBeInstanceOf(Token); + expect((words as Words).segments.length).toBe(3); }); test('unparsables in docs', () => { @@ -395,10 +401,11 @@ test('unparsables in docs', () => { ); expect(doc).toBeInstanceOf(Doc); expect(doc.markup.paragraphs[0]).toBeInstanceOf(Paragraph); - expect(doc.markup.paragraphs[0].segments[0]).toBeInstanceOf(Token); - expect(doc.markup.paragraphs[0].segments[1]).toBeInstanceOf(Example); - expect(doc.markup.paragraphs[0].segments[2]).toBeInstanceOf(Token); - expect(doc.markup.paragraphs[0].segments.length).toBe(3); + const words = doc.markup.paragraphs[0].segments[0]; + expect(words).toBeInstanceOf(Words); + expect((words as Words).segments[1]).toBeInstanceOf(Example); + expect((words as Words).segments[2]).toBeInstanceOf(Token); + expect((words as Words).segments.length).toBe(3); }); test('unparsables in blocks', () => { diff --git a/src/parser/Spaces.ts b/src/parser/Spaces.ts index e4f34977e..2a65aa45b 100644 --- a/src/parser/Spaces.ts +++ b/src/parser/Spaces.ts @@ -9,9 +9,11 @@ export const SPACE_HTML = '·'; export const TAB_HTML = ' '.repeat(TAB_WIDTH - TAB_SYMBOL.length) + TAB_SYMBOL; export const SPACE_TEXT = '\xa0'; +export const EXPLICIT_SPACE_TEXT = '·'; export const TAB_TEXT = SPACE_TEXT.repeat(TAB_WIDTH); export const EXPLICIT_TAB_TEXT = SPACE_TEXT.repeat(TAB_WIDTH - TAB_SYMBOL.length) + TAB_SYMBOL; +export const EXPLICIT_NEWLINE_TEXT = '↵'; export const MAX_LINE_LENGTH = 40; diff --git a/src/parser/Symbols.ts b/src/parser/Symbols.ts index ca07b4447..ab2b137d0 100644 --- a/src/parser/Symbols.ts +++ b/src/parser/Symbols.ts @@ -135,3 +135,8 @@ export const IDEA_SYMBOL = '💡'; export const DEFECT_SYMBOL = '🪲'; export const SEARCH_SYMBOL = '🔍'; + +export const BLOCK_EDITING_SYMBOL = withMonoEmoji('🖱️'); +export const TEXT_EDITING_SYMBOL = withMonoEmoji('⌨️'); + +export const DROP_DOWN_SYMBOL = '▾'; diff --git a/src/parser/Tokenizer.ts b/src/parser/Tokenizer.ts index 211fdc272..f0abc5230 100644 --- a/src/parser/Tokenizer.ts +++ b/src/parser/Tokenizer.ts @@ -89,10 +89,12 @@ const TEXT_SEPARATORS = '\'‘’"“”„«»‹›「」『』'; const OPERATORS = `${NOT_SYMBOL}\\-\\^${SUM_SYMBOL}\\${DIFFERENCE_SYMBOL}×${PRODUCT_SYMBOL}÷%<≤=≠≥>&|~?\\u2200-\\u22FF\\u2A00-\\u2AFF\\u2190-\\u21FF\\u27F0-\\u27FF\\u2900-\\u297F`; export const OperatorRegEx = new RegExp(`^[${OPERATORS}]`, 'u'); -export const URLRegEx = new RegExp( +export const StrictURLRegEx = new RegExp( /^(https?)?:\/\/(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_+.~#?&//=]*)/, 'u', ); +/** We use this permissive one because we still want to tokenize URL link things, even if they aren't strict. */ +export const PermissiveURLRegEx = StrictURLRegEx; //new RegExp(/^(https?)?:\/\/[^>]*/, 'u'); export const MarkupSymbols = [ CODE_SYMBOL, @@ -254,7 +256,7 @@ const CodeTokenPatterns: TokenPattern[] = [ { pattern: CONVERT_SYMBOL2, types: [Sym.Convert] }, { pattern: CONVERT_SYMBOL3, types: [Sym.Convert] }, { pattern: NONE_SYMBOL, types: [Sym.None, Sym.None] }, - { pattern: TYPE_SYMBOL, types: [Sym.Type, Sym.TypeOperator] }, + { pattern: TYPE_SYMBOL, types: [Sym.Type] }, { pattern: /^!#/, types: [Sym.Number] }, { pattern: new RegExp(`^[${LITERAL_SYMBOL}${LITERAL_SYMBOL_FULL}]`, 'u'), @@ -662,7 +664,7 @@ function getNextToken( inMarkup = true; // Check URLs first, since the word regex will match URLs. - const urlMatch = source.match(URLRegEx); + const urlMatch = source.match(PermissiveURLRegEx); if (urlMatch !== null) return new Token(urlMatch[0], Sym.URL); const wordsMatch = source.match(WordsRegEx); diff --git a/src/parser/parseExpression.ts b/src/parser/parseExpression.ts index 98e8fece7..cf6f81215 100644 --- a/src/parser/parseExpression.ts +++ b/src/parser/parseExpression.ts @@ -105,11 +105,7 @@ export function parseBlock( const root = kind === BlockKind.Root; // Grab any documentation if this isn't a root. - const docs = root - ? undefined - : tokens.nextIs(Sym.Doc) - ? parseDocs(tokens) - : undefined; + const docs = tokens.nextIs(Sym.Doc) ? parseDocs(tokens) : undefined; const open = root ? undefined @@ -207,11 +203,11 @@ export function parseBinaryEvaluate(tokens: Tokens): Expression { // If the next is a unary operator, then it has to have no preceding space to be parsed as a binary evaluate. (!tokens.nextIsUnary() || tokens.nextLacksPrecedingSpace()) && (tokens.nextIs(Sym.Operator) || - (tokens.nextIs(Sym.TypeOperator) && + (tokens.nextIs(Sym.Type) && !tokens.nextHasPrecedingLineBreak())), () => - (left = tokens.nextIs(Sym.TypeOperator) - ? new Is(left, tokens.read(Sym.TypeOperator), parseType(tokens)) + (left = tokens.nextIs(Sym.Type) + ? new Is(left, tokens.read(Sym.Type), parseType(tokens)) : new BinaryEvaluate( left, parseReference(tokens), @@ -408,7 +404,7 @@ export function parseNumber(tokens: Tokens): NumberLiteral { tokens.nextLacksPrecedingSpace() ? parseUnit(tokens) : undefined; - return new NumberLiteral(number, unit ?? Unit.Empty); + return new NumberLiteral(number, unit ?? Unit.create()); } export function parseUnit(tokens: Tokens): Unit | undefined { @@ -767,13 +763,27 @@ export function parseFunction(tokens: Tokens): FunctionDefinition { ); } -export function parseStructure(tokens: Tokens): StructureDefinition { +export function parseStructure( + tokens: Tokens, +): StructureDefinition | UnparsableExpression { const docs = tokens.nextIs(Sym.Doc) ? parseDocs(tokens) : undefined; const share = tokens.nextIs(Sym.Share) ? tokens.read(Sym.Share) : undefined; const type = tokens.read(Sym.Type); const names = parseNames(tokens); + // So we can rewind to the start. + const firstToken = [ + ...(docs ? docs.leaves() : []), + ...(share ? [share] : []), + type, + ][0]; + + if (names.isEmpty()) { + tokens.unreadTo(firstToken); + return parseUnparsable(tokens); + } + const interfaces: Reference[] = []; tokens.whileDo( () => tokens.nextIs(Sym.Name), @@ -783,9 +793,13 @@ export function parseStructure(tokens: Tokens): StructureDefinition { const types = tokens.nextIs(Sym.TypeOpen) ? parseTypeVariables(tokens) : undefined; - const open = tokens.nextIs(Sym.EvalOpen) - ? tokens.read(Sym.EvalOpen) - : undefined; + + if (!tokens.nextIs(Sym.EvalOpen)) { + tokens.unreadTo(firstToken); + return parseUnparsable(tokens); + } + + const open = tokens.read(Sym.EvalOpen); // Don't allow reactions on structure input binds tokens.pushReactionAllowed(false); @@ -799,9 +813,12 @@ export function parseStructure(tokens: Tokens): StructureDefinition { // Restore tokens.popReactionAllowed(); - const close = tokens.nextIs(Sym.EvalClose) - ? tokens.read(Sym.EvalClose) - : undefined; + if (!tokens.nextIs(Sym.EvalClose)) { + tokens.unreadTo(firstToken); + return parseUnparsable(tokens); + } + + const close = tokens.read(Sym.EvalClose); const block = nextAreOptionalDocsThen(tokens, [Sym.EvalOpen]) ? parseBlock(tokens, BlockKind.Structure) : undefined; @@ -965,7 +982,7 @@ function parsePropertyReference(left: Expression, tokens: Tokens): Expression { return left; } -function parseUnparsable(tokens: Tokens): Expression { +function parseUnparsable(tokens: Tokens): UnparsableExpression { return new UnparsableExpression(tokens.readLine()); } diff --git a/src/parser/parseMarkup.ts b/src/parser/parseMarkup.ts index ab4228b4c..734666993 100644 --- a/src/parser/parseMarkup.ts +++ b/src/parser/parseMarkup.ts @@ -43,17 +43,15 @@ export function parseParagraph(tokens: Tokens): Paragraph { } function parseSegment(tokens: Tokens) { - return tokens.nextIs(Sym.Words) - ? tokens.read(Sym.Words) - : tokens.nextIs(Sym.TagOpen) - ? parseWebLink(tokens) - : tokens.nextIs(Sym.Concept) - ? parseConceptLink(tokens) - : tokens.nextIs(Sym.Code) - ? parseExample(tokens) - : tokens.nextIs(Sym.Mention) - ? parseMention(tokens) - : parseWords(tokens); + return tokens.nextIs(Sym.TagOpen) + ? parseWebLink(tokens) + : tokens.nextIs(Sym.Concept) + ? parseConceptLink(tokens) + : tokens.nextIs(Sym.Code) + ? parseExample(tokens) + : tokens.nextIs(Sym.Mention) + ? parseMention(tokens) + : parseWords(tokens); } function parseWebLink(tokens: Tokens): WebLink { diff --git a/src/parser/parseProgram.ts b/src/parser/parseProgram.ts index 1228203a4..78cfaa367 100644 --- a/src/parser/parseProgram.ts +++ b/src/parser/parseProgram.ts @@ -3,7 +3,7 @@ import Borrow from '@nodes/Borrow'; import Program from '@nodes/Program'; import Sym from '@nodes/Sym'; import type Tokens from './Tokens'; -import { parseBlock, parseDocs, parseReference } from './parseExpression'; +import { parseBlock, parseReference } from './parseExpression'; import { toTokens } from './toTokens'; export function toProgram(code: string): Program { @@ -11,9 +11,6 @@ export function toProgram(code: string): Program { } export default function parseProgram(tokens: Tokens, doc = false): Program { - // If a borrow is next or there's no whitespace, parse a docs. - const docs = tokens.nextIs(Sym.Doc) ? parseDocs(tokens) : undefined; - const borrows: Borrow[] = []; tokens.whileDo( () => tokens.hasNext() && tokens.nextIs(Sym.Borrow), @@ -25,7 +22,7 @@ export default function parseProgram(tokens: Tokens, doc = false): Program { // If the next token is the end, we're done! const end = tokens.nextIsEnd() ? tokens.read(Sym.End) : undefined; - return new Program(docs, borrows, block, end); + return new Program(borrows, block, end); } export function parseBorrow(tokens: Tokens): Borrow { diff --git a/src/parser/parseType.ts b/src/parser/parseType.ts index ad4aeba1a..ba518548c 100644 --- a/src/parser/parseType.ts +++ b/src/parser/parseType.ts @@ -53,10 +53,9 @@ export default function parseType(tokens: Tokens, isExpression = false): Type { : // We use the doc symbol because it looks like an empty formatted tokens.nextIs(Sym.FormattedType) ? parseFormattedType(tokens) - : new UnparsableType(tokens.readLine()); - - if (!isExpression && tokens.nextIs(Sym.Convert)) - left = parseConversionType(left, tokens); + : tokens.nextIs(Sym.Convert) + ? parseConversionType(tokens) + : new UnparsableType(tokens.readLine()); tokens.whileDo( () => tokens.nextIs(Sym.Union) && tokens.nextLacksPrecedingSpace(), @@ -191,11 +190,12 @@ function parseFunctionType(tokens: Tokens): FunctionType { return new FunctionType(fun, typeVars, open, inputs, close, output); } -function parseConversionType(left: Type, tokens: Tokens): ConversionType { +function parseConversionType(tokens: Tokens): ConversionType { const convert = tokens.read(Sym.Convert); + const from = parseType(tokens); const to = parseType(tokens); - return new ConversionType(left, convert, to); + return new ConversionType(convert, from, to); } function parseFormattedType(tokens: Tokens): FormattedType { diff --git a/src/routes/character/[id]/+page.svelte b/src/routes/character/[id]/+page.svelte index cc5669007..cdc518ae8 100644 --- a/src/routes/character/[id]/+page.svelte +++ b/src/routes/character/[id]/+page.svelte @@ -1562,8 +1562,8 @@ )}

{locales.get(accessor).label}

l.ui.page.character.field.inherit), ...(none @@ -1676,8 +1676,8 @@ /> l.ui.page.character.field.mode} - modes={['👆', '⌫', '■', '🔲', '⚪️', '╱', '🙂']} + modes={(l) => l.ui.page.character.field.mode} + icons={['👆', '⌫', '■', '🔲', '⚪️', '╱', '🙂']} choice={mode} select={(choice: number) => { mode = choice as DrawingMode; @@ -1711,7 +1711,7 @@ l.ui.page.character.shape.emoji} /> {:else} l.ui.page.character.field.mode.modes[0]} + path={(l) => l.ui.page.character.field.mode.labels[0]} />… {/if} @@ -2263,21 +2263,15 @@ tip: (l) => l.ui.page.character.share.button.tip, icon: isPublic ? GLOBE1_SYMBOL : '🤫', label: isPublic - ? (l) => l.ui.page.character.share.public.modes[0] - : (l) => l.ui.page.character.share.public.modes[1], + ? (l) => l.ui.page.character.share.public.labels[0] + : (l) => l.ui.page.character.share.public.labels[1], }} > l.ui.page.character.share.public} + modes={(l) => l.ui.page.character.share.public} choice={isPublic ? 0 : 1} select={(mode) => (isPublic = mode === 0)} - modes={[ - `${GLOBE1_SYMBOL} ${$locales.get( - (l) => - l.ui.page.character.share.public.modes[0], - )}`, - `🤫 ${$locales.get((l) => l.ui.page.character.share.public.modes[1])}`, - ]} + icons={[GLOBE1_SYMBOL, '🤫']} /> l.ui.page.character.share.collaborators} diff --git a/src/routes/character/[id]/PageText.ts b/src/routes/character/[id]/PageText.ts index 9f2cb660a..12ade3fef 100644 --- a/src/routes/character/[id]/PageText.ts +++ b/src/routes/character/[id]/PageText.ts @@ -1,8 +1,8 @@ import type { Template } from '@locale/LocaleText'; import type { ButtonText, - DialogText, FieldText, + HeaderAndExplanationText, ModeText, } from '@locale/UITexts'; @@ -30,7 +30,7 @@ type PageText = { emoji: string; }; share: { - dialog: DialogText; + dialog: HeaderAndExplanationText; button: ButtonText; delete: ButtonText; public: ModeText; diff --git a/src/routes/gallery/[galleryid]/PageText.ts b/src/routes/gallery/[galleryid]/PageText.ts index 09a463739..ec9e4fb7f 100644 --- a/src/routes/gallery/[galleryid]/PageText.ts +++ b/src/routes/gallery/[galleryid]/PageText.ts @@ -1,4 +1,8 @@ -import type { ConfirmText, DialogText, FieldText } from '@locale/UITexts'; +import type { + ConfirmText, + FieldText, + HeaderAndExplanationText, +} from '@locale/UITexts'; type PageText = { /** What to call a gallery by default, before it's given a name */ @@ -8,13 +12,13 @@ type PageText = { /** Headers on the page */ subheader: { /** Associtaed classes header */ - classes: DialogText; + classes: HeaderAndExplanationText; /** The list of curators */ - curators: DialogText; + curators: HeaderAndExplanationText; /** The list of curators */ - creators: DialogText; + creators: HeaderAndExplanationText; /** Delete header */ - delete: DialogText; + delete: HeaderAndExplanationText; }; /** Confirm buttons on the gallery page */ confirm: { diff --git a/src/routes/guide/Guide.svelte b/src/routes/guide/Guide.svelte index 38e36271a..849a22a76 100644 --- a/src/routes/guide/Guide.svelte +++ b/src/routes/guide/Guide.svelte @@ -130,7 +130,7 @@ l.ui.page.guide.description} />
- +