From 311117dce9798343f5bd83b0055c56b9d2533e3f Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Fri, 5 Apr 2024 17:25:51 -0700 Subject: [PATCH 1/8] form editor, disabled download button, nested grid which may have room for improvement --- .../sequencing/SequenceEditor.svelte | 82 ++++--- src/components/sequencing/SequenceForm.svelte | 3 +- .../form/add-missing-args-button.svelte | 11 + .../sequencing/form/arg-editor.svelte | 88 ++++++++ .../sequencing/form/arg-title.svelte | 19 ++ .../sequencing/form/enum-editor.svelte | 37 ++++ .../form/extra-argument-editor.svelte | 12 ++ .../sequencing/form/num-editor.svelte | 61 ++++++ .../sequencing/form/selected-command.svelte | 202 ++++++++++++++++++ .../sequencing/form/string-editor.svelte | 38 ++++ src/components/sequencing/form/utils.ts | 95 ++++++++ src/stores/sequencing.ts | 2 + .../sequencer-grammar-constants.ts | 2 + .../new-sequence-editor/tree-utils.ts | 8 + 14 files changed, 628 insertions(+), 32 deletions(-) create mode 100644 src/components/sequencing/form/add-missing-args-button.svelte create mode 100644 src/components/sequencing/form/arg-editor.svelte create mode 100644 src/components/sequencing/form/arg-title.svelte create mode 100644 src/components/sequencing/form/enum-editor.svelte create mode 100644 src/components/sequencing/form/extra-argument-editor.svelte create mode 100644 src/components/sequencing/form/num-editor.svelte create mode 100644 src/components/sequencing/form/selected-command.svelte create mode 100644 src/components/sequencing/form/string-editor.svelte create mode 100644 src/components/sequencing/form/utils.ts diff --git a/src/components/sequencing/SequenceEditor.svelte b/src/components/sequencing/SequenceEditor.svelte index 575c097383..e6945266af 100644 --- a/src/components/sequencing/SequenceEditor.svelte +++ b/src/components/sequencing/SequenceEditor.svelte @@ -6,12 +6,13 @@ import { lintGutter } from '@codemirror/lint'; import { Compartment, EditorState } from '@codemirror/state'; import type { ViewUpdate } from '@codemirror/view'; + import type { SyntaxNode } from '@lezer/common'; import type { CommandDictionary } from '@nasa-jpl/aerie-ampcs'; import { EditorView, basicSetup } from 'codemirror'; import { seq } from 'codemirror-lang-sequence'; import { debounce } from 'lodash-es'; import { createEventDispatcher, onMount } from 'svelte'; - import { commandDictionaries, userSequencesRows } from '../../stores/sequencing'; + import { commandDictionaries, userSequenceEditorColumns, userSequencesRows } from '../../stores/sequencing'; import type { User } from '../../types/app'; import effects from '../../utilities/effects'; import { seqJsonLinter } from '../../utilities/new-sequence-editor/seq-json-linter'; @@ -23,6 +24,7 @@ import CssGridGutter from '../ui/CssGridGutter.svelte'; import Panel from '../ui/Panel.svelte'; import SectionTitle from '../ui/SectionTitle.svelte'; + import SelectedCommand from './form/selected-command.svelte'; export let readOnly: boolean = false; export let sequenceCommandDictionaryId: number | null = null; @@ -48,6 +50,7 @@ let editorSeqJsonView: EditorView; let editorSequenceDiv: HTMLDivElement; let editorSequenceView: EditorView; + let selectedNode: SyntaxNode | null; $: { if (editorSequenceView) { @@ -122,8 +125,15 @@ editorSeqJsonView.dispatch({ changes: { from: 0, insert: seqJsonStr, to: editorSeqJsonView.state.doc.length } }); dispatch('sequence', sequence); + + const updatedSelectionNode = tree.resolveInner(viewUpdate.state.selection.asSingle().main.from, -1); + // minimize triggering selected command view + if (selectedNode !== updatedSelectionNode) { + selectedNode = updatedSelectionNode; + } } + // eslint-disable-next-line @typescript-eslint/no-unused-vars function downloadSeqJson() { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([sequenceDefinition], { type: 'application/json' })); @@ -132,34 +142,44 @@ } - - - - {title} - -
- -
-
- - -
- - - - - - - - Seq JSON (Read-only) - -
- -
-
- - -
- - + + + + + {title} + +
+ +
+
+ + +
+ + + + + + + + Seq JSON (Read-only) + + + + + +
+ + + + + + + {#if !!commandDictionary && !!selectedNode} + + {:else} +
Selected Command
+ {/if} diff --git a/src/components/sequencing/SequenceForm.svelte b/src/components/sequencing/SequenceForm.svelte index d172a46292..446c836a38 100644 --- a/src/components/sequencing/SequenceForm.svelte +++ b/src/components/sequencing/SequenceForm.svelte @@ -80,6 +80,7 @@ loadAdaptation(adaptation); }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars async function getUserSequenceFromSeqJson() { const file: File = seqJsonFiles[0]; const text = await file.text(); @@ -256,7 +257,7 @@ }} /> - + +
+ + diff --git a/src/components/sequencing/form/selected-command.svelte b/src/components/sequencing/form/selected-command.svelte new file mode 100644 index 0000000000..e3f5dacc1e --- /dev/null +++ b/src/components/sequencing/form/selected-command.svelte @@ -0,0 +1,202 @@ + + + + +
+ {#if !!commandNode} +
Selected Command
+ {#if !!commandDef} + {#if !!timeTagNode} +
Time Tag: {timeTagNode.text.trim()}
+ {/if} +
{commandDef.stem}
+
+
+ {#each editorArgInfoArray as argInfo} + + {/each} + {#if missingArgDefArray.length} + { + if (commandNode) { + addDefaultArgs(commandNode, missingArgDefArray); + } + }} + /> + {/if} +
+ {/if} + {/if} +
+ + diff --git a/src/components/sequencing/form/string-editor.svelte b/src/components/sequencing/form/string-editor.svelte new file mode 100644 index 0000000000..7a8485cf0b --- /dev/null +++ b/src/components/sequencing/form/string-editor.svelte @@ -0,0 +1,38 @@ + + + + +
+ +
+ + diff --git a/src/components/sequencing/form/utils.ts b/src/components/sequencing/form/utils.ts new file mode 100644 index 0000000000..49caf590dd --- /dev/null +++ b/src/components/sequencing/form/utils.ts @@ -0,0 +1,95 @@ + +import type { + FswCommandArgument, + FswCommandArgumentEnum, + FswCommandArgumentFixedString, + FswCommandArgumentFloat, + FswCommandArgumentInteger, + FswCommandArgumentNumeric, + FswCommandArgumentRepeat, + FswCommandArgumentUnsigned, + FswCommandArgumentVarString, +} from '@nasa-jpl/aerie-ampcs'; +import type { SyntaxNode } from '@lezer/common'; + +export function isFswCommandArgumentEnum(arg: FswCommandArgument): arg is FswCommandArgumentEnum { + return arg.arg_type === 'enum'; +} + +export function isFswCommandArgumentInteger(arg: FswCommandArgument): arg is FswCommandArgumentInteger { + return arg.arg_type === 'integer'; +} + +export function isFswCommandArgumentFloat(arg: FswCommandArgument): arg is FswCommandArgumentFloat { + return arg.arg_type === 'float'; +} + +export function isFswCommandArgumentNumeric(arg: FswCommandArgument): arg is FswCommandArgumentNumeric { + return arg.arg_type === 'numeric'; +} + +export function isFswCommandArgumentUnsigned(arg: FswCommandArgument): arg is FswCommandArgumentUnsigned { + return arg.arg_type === 'unsigned'; +} + +export function isFswCommandArgumentRepeat(arg: FswCommandArgument): arg is FswCommandArgumentRepeat { + return arg.arg_type === 'repeat'; +} + +export function isFswCommandArgumentVarString(arg: FswCommandArgument): arg is FswCommandArgumentVarString { + return arg.arg_type === 'var_string'; +} + +export function isFswCommandArgumentFixedString(arg: FswCommandArgument): arg is FswCommandArgumentFixedString { + return arg.arg_type === 'fixed_string'; +} + +export function isNumberArg(arg: FswCommandArgument): arg is NumberArg { + return ( + isFswCommandArgumentFloat(arg) || + isFswCommandArgumentInteger(arg) || + isFswCommandArgumentNumeric(arg) || + isFswCommandArgumentUnsigned(arg) + ); +} + +export function isStringArg(arg: FswCommandArgument): arg is StringArg { + return isFswCommandArgumentVarString(arg) || isFswCommandArgumentFixedString(arg); +} + +export type StringArg = FswCommandArgumentVarString | FswCommandArgumentFixedString; + +export type NumberArg = + | FswCommandArgumentFloat + | FswCommandArgumentInteger + | FswCommandArgumentNumeric + | FswCommandArgumentUnsigned; + +export type ArgTextDef = { + node?: SyntaxNode; + text?: string; + argDef?: FswCommandArgument; + children?: ArgTextDef[]; + parentArgDef?: FswCommandArgumentRepeat; +}; + +export function getMissingArgDefs(argInfoArray: ArgTextDef[]) { + return argInfoArray + .filter((argInfo): argInfo is { argDef: FswCommandArgument } => !argInfo.node && !!argInfo.argDef) + .map(argInfo => argInfo.argDef); +} + +export function isQuoted(s: string) { + return s.startsWith('"') && s.endsWith('"'); +} + +export function unquoteUnescape(s: string) { + if (isQuoted(s)) { + return s.slice(1, -1).replaceAll('\\"', '"'); + } + return s; +} + +export function quoteEscape(s: string) { + return `"${s.replaceAll('"', '\\"')}"`; +} diff --git a/src/stores/sequencing.ts b/src/stores/sequencing.ts index 7e87fb7897..9b962131e6 100644 --- a/src/stores/sequencing.ts +++ b/src/stores/sequencing.ts @@ -16,3 +16,5 @@ export const userSequencesColumns: Writable = writable('1.5fr 3px 1fr'); export const userSequenceFormColumns: Writable = writable('1fr 3px 2fr'); export const userSequencesRows: Writable = writable('1fr 3px 1fr'); + +export const userSequenceEditorColumns: Writable = writable('3fr 3px 1fr'); diff --git a/src/utilities/new-sequence-editor/sequencer-grammar-constants.ts b/src/utilities/new-sequence-editor/sequencer-grammar-constants.ts index b420079c30..77eff640d7 100644 --- a/src/utilities/new-sequence-editor/sequencer-grammar-constants.ts +++ b/src/utilities/new-sequence-editor/sequencer-grammar-constants.ts @@ -1 +1,3 @@ +export const TOKEN_COMMAND = 'Command'; export const TOKEN_REPEAT_ARG = 'RepeatArg'; +export const TOKEN_ERROR = '⚠'; diff --git a/src/utilities/new-sequence-editor/tree-utils.ts b/src/utilities/new-sequence-editor/tree-utils.ts index 2fab3c398c..0e9864763f 100644 --- a/src/utilities/new-sequence-editor/tree-utils.ts +++ b/src/utilities/new-sequence-editor/tree-utils.ts @@ -45,3 +45,11 @@ export function getFromAndTo(nodes: (SyntaxNode | null)[]): { from: number; to: { from: Number.MAX_VALUE, to: Number.MIN_VALUE }, ); } + +export function getAncestorNode(node: SyntaxNode | null, name: string) { + let commandNode: SyntaxNode | null = node; + while (commandNode && commandNode.name !== name) { + commandNode = commandNode.parent; + } + return commandNode; +} From b78223b1e4b7c4df98025e4a6be2b2698e666352 Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Sat, 6 Apr 2024 10:46:56 -0700 Subject: [PATCH 2/8] filter codemirror edit dispatching --- .../sequencing/form/selected-command.svelte | 33 ++++++++++++------- .../sequencing/form/string-editor.svelte | 20 +++++------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/components/sequencing/form/selected-command.svelte b/src/components/sequencing/form/selected-command.svelte index e3f5dacc1e..f5e685a836 100644 --- a/src/components/sequencing/form/selected-command.svelte +++ b/src/components/sequencing/form/selected-command.svelte @@ -21,6 +21,8 @@ export let commandDictionary: CommandDictionary; export let node: SyntaxNode | null; + const ID_COMMAND_DETAIL_PANE = 'ID_COMMAND_DETAIL_PANE'; + $: commandNode = getAncestorNode(node, TOKEN_COMMAND); $: commandDef = getCommandDef(commandDictionary, editorSequenceView.state, commandNode); $: argInfoArray = getArgumentInfo(commandNode?.getChild('Args') ?? null, commandDef?.arguments); @@ -110,18 +112,30 @@ } function setInEditor(token: SyntaxNode, val: string) { - if (editorSequenceView) { + // checking that we are not in the code mirror editor + // this breaks cycle of form edits triggering document updates and vice versa + if (editorSequenceView && hasAncestorWithId(document.activeElement, ID_COMMAND_DETAIL_PANE)) { const currentVal = editorSequenceView.state.sliceDoc(token.node.from, token.node.to); if (currentVal !== val) { - const transaction = editorSequenceView.state.update({ - changes: { from: token.node.from, insert: val, to: token.node.to }, - userEvent: 'formView', - }); - editorSequenceView.dispatch(transaction); + editorSequenceView.dispatch( + editorSequenceView.state.update({ + changes: { from: token.node.from, insert: val, to: token.node.to }, + userEvent: 'formView', + }), + ); } } } + function hasAncestorWithId(element: Element | null, id: string) { + if (element === null) { + return false; + } else if (element.id === id) { + return true; + } + return hasAncestorWithId(element.parentElement, id); + } + function addDefaultArgs(commandNode: SyntaxNode, argDefs: FswCommandArgument[]) { let insertPosition: undefined | number = undefined; if (editorSequenceView) { @@ -158,16 +172,13 @@ // provide a more restrictive editor to keep argument valid. Otherwise fall back on a string editor. // TODO - // display repeat args - // button to add repeat set - // add descriptive text // better handling of unclosed strings // {'integer_arg', 'float_arg', 'unsigned_arg', 'enum_arg', 'var_string_arg', 'repeat_arg'} // {'unsigned_arg', 'enum_arg', 'var_string_arg', 'float_arg'} -
+
{#if !!commandNode}
Selected Command
{#if !!commandDef} @@ -178,7 +189,7 @@
{#each editorArgInfoArray as argInfo} - + {/each} {#if missingArgDefArray.length}
- +