From 452f1f0b70f75ef7247d93fe94df5524741d1b42 Mon Sep 17 00:00:00 2001 From: Ryan Goetz <70245883+goetzrrGit@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:29:07 -1000 Subject: [PATCH] Library Sequences from Workspace (#1539) * Pass along the workspaceID to the SequenceEditor * Create a Sequence library Type and updated the Adaptation object * Added workspaces sequences as Library Sequences. * Using the workspace_id all sequences in a workspace are library sequences. * Pass this list of library sequences along to the different parts of codemirror etc. linter, autocomplete, SelectedCommand Panel. * Hook up the linter with library sequences. * The linter will typechecking the library sequences parameters/arguments * Added auto completion to Library Sequences * Update unit test to use arrow functions --- src/components/expansion/ExpansionRuns.svelte | 1 + .../sequencing/SequenceEditor.svelte | 30 +- src/components/sequencing/SequenceForm.svelte | 1 + src/components/sequencing/Sequences.svelte | 1 + src/types/sequencing.ts | 11 +- .../codemirror/codemirror-utils.test.ts | 26 +- src/utilities/codemirror/codemirror-utils.ts | 43 +++ src/utilities/codemirror/seq-n-tree-utils.ts | 6 +- .../sequence-editor/extension-points.ts | 5 +- .../sequence-editor/sequence-completion.ts | 44 ++- .../sequence-editor/sequence-linter.ts | 324 ++++++++++++++---- .../sequence-editor/to-seq-json.test.ts | 60 +++- src/utilities/sequence-editor/to-seq-json.ts | 2 +- 13 files changed, 463 insertions(+), 91 deletions(-) diff --git a/src/components/expansion/ExpansionRuns.svelte b/src/components/expansion/ExpansionRuns.svelte index 21352ddff9..611088b5d9 100644 --- a/src/components/expansion/ExpansionRuns.svelte +++ b/src/components/expansion/ExpansionRuns.svelte @@ -190,6 +190,7 @@ sequenceOutput={selectedSequence ? JSON.stringify(selectedSequence.expanded_sequence, null, 2) : undefined} readOnly={true} title="Sequence - Definition Editor (Read-only)" + workspaceId={null} {user} /> diff --git a/src/components/sequencing/SequenceEditor.svelte b/src/components/sequencing/SequenceEditor.svelte index 851ce48e74..08e81a7cf4 100644 --- a/src/components/sequencing/SequenceEditor.svelte +++ b/src/components/sequencing/SequenceEditor.svelte @@ -32,10 +32,11 @@ parcelToParameterDictionaries, userSequenceEditorColumns, userSequenceEditorColumnsWithFormBuilder, + userSequences, } from '../../stores/sequencing'; import type { User } from '../../types/app'; - import type { IOutputFormat, Parcel } from '../../types/sequencing'; - import { setupLanguageSupport } from '../../utilities/codemirror'; + import type { IOutputFormat, LibrarySequence, Parcel } from '../../types/sequencing'; + import { SeqLanguage, setupLanguageSupport } from '../../utilities/codemirror'; import type { CommandInfoMapper } from '../../utilities/codemirror/commandInfoMapper'; import { seqNHighlightBlock, seqqNBlockHighlighter } from '../../utilities/codemirror/seq-n-highlighter'; import { SeqNCommandInfoMapper } from '../../utilities/codemirror/seq-n-tree-utils'; @@ -56,6 +57,7 @@ import { inputLinter, outputLinter } from '../../utilities/sequence-editor/extension-points'; import { seqNFormat } from '../../utilities/sequence-editor/sequence-autoindent'; import { sequenceTooltip } from '../../utilities/sequence-editor/sequence-tooltip'; + import { parseVariables } from '../../utilities/sequence-editor/to-seq-json'; import { showFailureToast, showSuccessToast } from '../../utilities/toast'; import { tooltip } from '../../utilities/tooltip'; import Menu from '../menus/Menu.svelte'; @@ -74,6 +76,7 @@ export let sequenceOutput: string = ''; export let title: string = 'Sequence - Definition Editor'; export let user: User | null; + export let workspaceId: number | null; const dispatch = createEventDispatcher<{ sequence: { input: string; output: string }; @@ -94,6 +97,7 @@ let commandDictionary: CommandDictionary | null; let disableCopyAndExport: boolean = true; let parameterDictionaries: ParameterDictionary[] = []; + let librarySequences: LibrarySequence[] = []; let commandFormBuilderGrid: string; let editorOutputDiv: HTMLDivElement; let editorOutputView: EditorView; @@ -164,6 +168,18 @@ } }); + librarySequences = $userSequences + .filter(sequence => sequence.workspace_id === workspaceId && sequence.name !== sequenceName) + .map(sequence => { + const tree = SeqLanguage.parser.parse(sequence.definition); + return { + name: sequence.name, + parameters: parseVariables(tree.topNode, sequence.definition, 'ParameterDeclaration') ?? [], + tree, + workspace_id: sequence.workspace_id, + }; + }); + if (unparsedCommandDictionary) { if (sequenceName && isInVmlMode) { getParsedCommandDictionary(unparsedCommandDictionary, user).then(parsedCommandDictionary => { @@ -203,11 +219,17 @@ parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries, + librarySequences, ), ), ), compartmentSeqLinter.reconfigure( - inputLinter(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), + inputLinter( + parsedChannelDictionary, + parsedCommandDictionary, + nonNullParsedParameterDictionaries, + librarySequences, + ), ), compartmentSeqTooltip.reconfigure( sequenceTooltip(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), @@ -251,7 +273,7 @@ EditorView.lineWrapping, EditorView.theme({ '.cm-gutter': { 'min-height': `${clientHeightGridRightTop}px` } }), lintGutter(), - compartmentSeqLanguage.of(setupLanguageSupport($sequenceAdaptation.autoComplete(null, null, []))), + compartmentSeqLanguage.of(setupLanguageSupport($sequenceAdaptation.autoComplete(null, null, [], []))), compartmentSeqLinter.of(inputLinter()), compartmentSeqTooltip.of(sequenceTooltip()), EditorView.updateListener.of(debounce(sequenceUpdateListener, 250)), diff --git a/src/components/sequencing/SequenceForm.svelte b/src/components/sequencing/SequenceForm.svelte index c682ec547e..da4d5e088d 100644 --- a/src/components/sequencing/SequenceForm.svelte +++ b/src/components/sequencing/SequenceForm.svelte @@ -327,6 +327,7 @@ title="{mode === 'create' ? 'New' : 'Edit'} Sequence - Definition Editor" {user} readOnly={!hasPermission} + workspaceId={selectedWorkspaceId} on:sequence={onSequenceChange} on:didChangeModelContent={onDidChangeModelContent} /> diff --git a/src/components/sequencing/Sequences.svelte b/src/components/sequencing/Sequences.svelte index a18e43bf24..7a4651e436 100644 --- a/src/components/sequencing/Sequences.svelte +++ b/src/components/sequencing/Sequences.svelte @@ -104,6 +104,7 @@ sequenceOutput={selectedSequence?.seq_json} title="Sequence - Definition Editor (Read-only)" readOnly={true} + {workspaceId} {user} /> diff --git a/src/types/sequencing.ts b/src/types/sequencing.ts index f65e136967..b05840a656 100644 --- a/src/types/sequencing.ts +++ b/src/types/sequencing.ts @@ -1,12 +1,13 @@ import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import type { IndentContext } from '@codemirror/language'; import type { Diagnostic } from '@codemirror/lint'; -import type { SyntaxNode } from '@lezer/common'; +import type { SyntaxNode, Tree } from '@lezer/common'; import type { ChannelDictionary as AmpcsChannelDictionary, CommandDictionary as AmpcsCommandDictionary, ParameterDictionary as AmpcsParameterDictionary, } from '@nasa-jpl/aerie-ampcs'; +import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; import type { DictionaryTypes } from '../enums/dictionaryTypes'; import type { ArgDelegator } from '../utilities/sequence-editor/extension-points'; @@ -64,6 +65,7 @@ export interface ISequenceAdaptation { channelDictionary: AmpcsChannelDictionary | null, commandDictionary: AmpcsCommandDictionary | null, parameterDictionaries: AmpcsParameterDictionary[], + librarySequences: LibrarySequence[], ) => (context: CompletionContext) => CompletionResult | null; autoIndent?: () => (context: IndentContext, pos: number) => number | null | undefined; globals?: GlobalType[]; @@ -141,6 +143,13 @@ export type UserSequence = { workspace_id: number; }; +export type LibrarySequence = { + name: string; + parameters: VariableDeclaration[]; + tree: Tree; + workspace_id: number; +}; + export type UserSequenceInsertInput = Omit; export type Workspace = { diff --git a/src/utilities/codemirror/codemirror-utils.test.ts b/src/utilities/codemirror/codemirror-utils.test.ts index 5b4dbc89e3..c3b399ed67 100644 --- a/src/utilities/codemirror/codemirror-utils.test.ts +++ b/src/utilities/codemirror/codemirror-utils.test.ts @@ -1,5 +1,7 @@ +import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import { describe, expect, it } from 'vitest'; import { + getDefaultVariableArgs, isHexValue, isQuoted, parseNumericArg, @@ -90,15 +92,15 @@ describe('quoteEscape', () => { }); }); -describe('parseNumericArg', function () { - it("should parse 'float' and 'numeric' args as floats", function () { +describe('parseNumericArg', () => { + it("should parse 'float' and 'numeric' args as floats", () => { expect(parseNumericArg('1.23', 'float')).toEqual(1.23); expect(parseNumericArg('2.34', 'numeric')).toEqual(2.34); expect(parseNumericArg('bad', 'float')).toEqual(NaN); // can't parse hex numbers as float expect(parseNumericArg('0xabc', 'float')).toEqual(0); }); - it("should parse 'integer' and 'unsigned' args as integers", function () { + it("should parse 'integer' and 'unsigned' args as integers", () => { expect(parseNumericArg('123', 'integer')).toEqual(123); expect(parseNumericArg('234', 'unsigned')).toEqual(234); expect(parseNumericArg('234.567', 'integer')).toEqual(234); @@ -107,8 +109,8 @@ describe('parseNumericArg', function () { expect(parseNumericArg('0x1f', 'unsigned')).toEqual(31); }); }); -describe('isHexValue', function () { - it('should correctly identify a hex number string', function () { +describe('isHexValue', () => { + it('should correctly identify a hex number string', () => { expect(isHexValue('12')).toBe(false); expect(isHexValue('ff')).toBe(false); expect(isHexValue('0x99')).toBe(true); @@ -117,3 +119,17 @@ describe('isHexValue', function () { expect(isHexValue('0x12xx')).toBe(false); }); }); +describe('getDefaultVariableArgs', () => { + const mockParameters = [ + { name: 'exampleString', type: 'STRING' }, + { allowable_ranges: [{ min: 1.2 }], type: 'FLOAT' }, + { allowable_ranges: [{ min: 5 }], type: 'INT' }, + { allowable_ranges: [{ min: 7 }], type: 'UINT' }, + { allowable_values: ['VALUE1'], enum_name: 'ExampleEnum', type: 'ENUM' }, + { type: 'INT' }, + ] as VariableDeclaration[]; + it('should return default values for different types', () => { + const result = getDefaultVariableArgs(mockParameters); + expect(result).toEqual(['"exampleString"', 1.2, 5, 7, '"VALUE1"', 0]); + }); +}); diff --git a/src/utilities/codemirror/codemirror-utils.ts b/src/utilities/codemirror/codemirror-utils.ts index 70c0c56abd..454aff9a17 100644 --- a/src/utilities/codemirror/codemirror-utils.ts +++ b/src/utilities/codemirror/codemirror-utils.ts @@ -12,6 +12,7 @@ import type { FswCommandArgumentUnsigned, FswCommandArgumentVarString, } from '@nasa-jpl/aerie-ampcs'; +import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; import { fswCommandArgDefault } from '../sequence-editor/command-dictionary'; import type { CommandInfoMapper } from './commandInfoMapper'; @@ -107,6 +108,48 @@ export function getMissingArgDefs(argInfoArray: ArgTextDef[]): FswCommandArgumen .map(argInfo => argInfo.argDef); } +export function getDefaultVariableArgs(parameters: VariableDeclaration[]): string[] { + return parameters.map(parameter => { + switch (parameter.type) { + case 'STRING': + return `"${parameter.name}"`; + case 'FLOAT': + return parameter.allowable_ranges && parameter.allowable_ranges.length > 0 + ? parameter.allowable_ranges[0].min + : 0; + case 'INT': + case 'UINT': + return parameter.allowable_ranges && parameter.allowable_ranges.length > 0 + ? parameter.allowable_ranges[0].min + : 0; + case 'ENUM': + return parameter.allowable_values && parameter.allowable_values.length > 0 + ? `"${parameter.allowable_values[0]}"` + : parameter.enum_name + ? `${parameter.enum_name}` + : 'UNKNOWN'; + default: + throw Error(`unknown argument type ${parameter.type}`); + } + }) as string[]; +} + +export function addDefaultVariableArgs( + parameters: VariableDeclaration[], + view: EditorView, + commandNode: SyntaxNode, + commandInfoMapper: CommandInfoMapper, +) { + const insertPosition = commandInfoMapper.getArgumentAppendPosition(commandNode); + if (insertPosition !== undefined) { + const str = commandInfoMapper.formatArgumentArray(getDefaultVariableArgs(parameters), commandNode); + const transaction = view.state.update({ + changes: { from: insertPosition, insert: str }, + }); + view.dispatch(transaction); + } +} + export function isQuoted(s: string): boolean { return s.startsWith('"') && s.endsWith('"'); } diff --git a/src/utilities/codemirror/seq-n-tree-utils.ts b/src/utilities/codemirror/seq-n-tree-utils.ts index 12045baa5a..9a2fdcd46c 100644 --- a/src/utilities/codemirror/seq-n-tree-utils.ts +++ b/src/utilities/codemirror/seq-n-tree-utils.ts @@ -58,7 +58,11 @@ export class SeqNCommandInfoMapper implements CommandInfoMapper { } getArgumentAppendPosition(commandOrRepeatArgNode: SyntaxNode | null): number | undefined { - if (commandOrRepeatArgNode?.name === RULE_COMMAND) { + if ( + commandOrRepeatArgNode?.name === RULE_COMMAND || + commandOrRepeatArgNode?.name === TOKEN_ACTIVATE || + commandOrRepeatArgNode?.name === TOKEN_LOAD + ) { const argsNode = commandOrRepeatArgNode.getChild('Args'); const stemNode = commandOrRepeatArgNode.getChild('Stem'); return getFromAndTo([stemNode, argsNode]).to; diff --git a/src/utilities/sequence-editor/extension-points.ts b/src/utilities/sequence-editor/extension-points.ts index ac7700aec1..cb5e782ee2 100644 --- a/src/utilities/sequence-editor/extension-points.ts +++ b/src/utilities/sequence-editor/extension-points.ts @@ -9,7 +9,7 @@ import { } from '@nasa-jpl/aerie-ampcs'; import { get } from 'svelte/store'; import { inputFormat, sequenceAdaptation } from '../../stores/sequence-adaptation'; -import type { IOutputFormat } from '../../types/sequencing'; +import type { IOutputFormat, LibrarySequence } from '../../types/sequencing'; import { seqJsonLinter } from './seq-json-linter'; import { sequenceLinter } from './sequence-linter'; @@ -81,6 +81,7 @@ export function inputLinter( channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], + librarySequences: LibrarySequence[] = [], ): Extension { return linter(view => { const inputLinter = get(sequenceAdaptation).inputFormat.linter; @@ -88,7 +89,7 @@ export function inputLinter( const treeNode = tree.topNode; let diagnostics: Diagnostic[]; - diagnostics = sequenceLinter(view, channelDictionary, commandDictionary, parameterDictionaries); + diagnostics = sequenceLinter(view, channelDictionary, commandDictionary, parameterDictionaries, librarySequences); if (inputLinter !== undefined && commandDictionary !== null) { diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); diff --git a/src/utilities/sequence-editor/sequence-completion.ts b/src/utilities/sequence-editor/sequence-completion.ts index 2edd94e6c8..429bffff67 100644 --- a/src/utilities/sequence-editor/sequence-completion.ts +++ b/src/utilities/sequence-editor/sequence-completion.ts @@ -1,16 +1,21 @@ import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { syntaxTree } from '@codemirror/language'; import type { ChannelDictionary, CommandDictionary, ParameterDictionary } from '@nasa-jpl/aerie-ampcs'; +import { RULE_SEQUENCE_NAME, TOKEN_ACTIVATE, TOKEN_LOAD } from '../../constants/seq-n-grammar-constants'; import { getGlobals } from '../../stores/sequence-adaptation'; +import type { LibrarySequence } from '../../types/sequencing'; import { SeqLanguage } from '../codemirror'; +import { getDefaultVariableArgs } from '../codemirror/codemirror-utils'; import { getDoyTime } from '../time'; import { fswCommandArgDefault } from './command-dictionary'; import { getCustomArgDef } from './extension-points'; +import { getFromAndTo, getNearestAncestorNodeOfType } from './tree-utils'; type CursorInfo = { isAfterActivateOrLoad: boolean; isAfterTimeTag: boolean; isAtLineComment: boolean; + isAtSequenceName: boolean; isAtSymbolBefore: boolean; isBeforeHDWCommands: boolean; isBeforeImmedOrHDWCommands: boolean; @@ -25,6 +30,7 @@ export function sequenceCompletion( channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[], + librarySequences: LibrarySequence[], ) { return (context: CompletionContext): CompletionResult | null => { const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); @@ -48,9 +54,11 @@ export function sequenceCompletion( const fswCommandsCompletions: Completion[] = []; const hwCommandsCompletions: Completion[] = []; const directivesCompletions: Completion[] = []; + const globalCompletions: Completion[] = []; + const libraryCompletions: Completion[] = []; const cursor: CursorInfo = { - isAfterActivateOrLoad: nodeBefore.parent?.name === 'Activate' || nodeBefore.parent?.name === 'Load', + isAfterActivateOrLoad: nodeBefore.parent?.name === TOKEN_ACTIVATE || nodeBefore.parent?.name === TOKEN_LOAD, isAfterTimeTag: (() => { const line = context.state.doc.lineAt(context.pos); const node = SeqLanguage.parser.parse(line.text).resolveInner(context.pos - line.from, -1); @@ -58,6 +66,7 @@ export function sequenceCompletion( return node.parent?.getChild('TimeGroundEpoch') || node.parent?.getChild('TimeTag') ? true : false; })(), isAtLineComment: nodeCurrent.name === 'LineComment' || nodeBefore.name === 'LineComment', + isAtSequenceName: nodeCurrent.parent?.name === RULE_SEQUENCE_NAME, isAtSymbolBefore: isAtTyped(context.state.doc.toString(), word), isBeforeHDWCommands: context.pos < (baseNode.getChild('HardwareCommands')?.from ?? Infinity), isBeforeImmedOrHDWCommands: @@ -239,7 +248,6 @@ export function sequenceCompletion( // } const globals = getGlobals(); - const globalCompletions: Completion[] = []; if (globals) { for (const global of globals) { @@ -253,6 +261,37 @@ export function sequenceCompletion( } } + if (cursor.isAtSequenceName) { + libraryCompletions.push( + ...librarySequences.map(sequence => { + const args = getDefaultVariableArgs(sequence.parameters).join(' '); + + return { + apply: (view: any, _completion: any, from: number, to: number) => { + const t = getNearestAncestorNodeOfType(nodeCurrent, [TOKEN_ACTIVATE, TOKEN_LOAD]); + view.dispatch({ + changes: { + from, + insert: `${sequence.name}") ${args}`, + to: t ? getFromAndTo([t, t?.getChild('Args')]).to : to, + }, + }); + }, + info: 'A library sequence', + label: sequence.name, + section: 'Sequences', + type: 'library', + }; + }), + ); + //clear all other completions + directivesCompletions.length = 0; + timeTagCompletions.length = 0; + fswCommandsCompletions.length = 0; + hwCommandsCompletions.length = 0; + enumerationCompletions.length = 0; + } + return { from: word.from, options: [ @@ -262,6 +301,7 @@ export function sequenceCompletion( ...fswCommandsCompletions, ...hwCommandsCompletions, ...globalCompletions, + ...libraryCompletions, ], }; } diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index 2ccc4952b8..2886e83b90 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -15,11 +15,27 @@ import { closest, distance } from 'fastest-levenshtein'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; -import { TOKEN_COMMAND, TOKEN_ERROR, TOKEN_REPEAT_ARG, TOKEN_REQUEST } from '../../constants/seq-n-grammar-constants'; +import { + RULE_ARGS, + RULE_SEQUENCE_NAME, + TOKEN_ACTIVATE, + TOKEN_COMMAND, + TOKEN_ERROR, + TOKEN_LOAD, + TOKEN_REPEAT_ARG, + TOKEN_REQUEST, +} from '../../constants/seq-n-grammar-constants'; import { TimeTypes } from '../../enums/time'; import { getGlobals } from '../../stores/sequence-adaptation'; +import type { LibrarySequence } from '../../types/sequencing'; import { CustomErrorCodes } from '../../workers/customCodes'; -import { addDefaultArgs, isHexValue, parseNumericArg, quoteEscape } from '../codemirror/codemirror-utils'; +import { + addDefaultArgs, + addDefaultVariableArgs, + isHexValue, + parseNumericArg, + quoteEscape, +} from '../codemirror/codemirror-utils'; import { closeSuggestion, computeBlocks, openSuggestion } from '../codemirror/custom-folder'; import { SeqNCommandInfoMapper } from '../codemirror/seq-n-tree-utils'; import { @@ -72,6 +88,7 @@ export function sequenceLinter( channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], + librarySequences: LibrarySequence[] = [], ): Diagnostic[] { const tree = syntaxTree(view.state); const treeNode = tree.topNode; @@ -130,6 +147,10 @@ export function sequenceLinter( parameterDictionaries, ), ); + diagnostics.push( + ...validateActivateLoad(commandsNode.getChildren(TOKEN_ACTIVATE), 'Activate', docText, librarySequences), + ...validateActivateLoad(commandsNode.getChildren(TOKEN_LOAD), 'Load', docText, librarySequences), + ); } diagnostics.push( @@ -473,6 +494,141 @@ function getVariableInfo( }; } +function validateActivateLoad( + node: SyntaxNode[], + type: 'Activate' | 'Load', + text: string, + librarySequences: LibrarySequence[], +): Diagnostic[] { + if (node.length === 0) { + return []; + } + + const diagnostics: Diagnostic[] = []; + + node.forEach(activate => { + const sequenceName = activate.getChild(RULE_SEQUENCE_NAME); + const argNode = activate.getChild(RULE_ARGS); + + if (sequenceName === null || argNode === null) { + return; + } + const library = librarySequences.find( + library => library.name === text.slice(sequenceName.from, sequenceName.to).replace(/^"|"$/g, ''), + ); + const argsNode = getChildrenNode(argNode); + if (!library) { + diagnostics.push({ + from: sequenceName.from, + message: `Sequence doesn't exist ${text.slice(sequenceName.from, sequenceName.to)}`, + severity: 'warning', + to: sequenceName.to, + }); + } else { + const structureError = validateCommandStructure(activate, argsNode, library.parameters.length, (view: any) => { + addDefaultVariableArgs(library.parameters.slice(argsNode.length), view, activate, new SeqNCommandInfoMapper()); + }); + if (structureError) { + diagnostics.push(structureError); + return diagnostics; + } + + library?.parameters.forEach((parameter, index) => { + const arg = argsNode[index]; + switch (parameter.type) { + case 'STRING': { + if (arg.name !== 'String') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be a string`, + severity: 'error', + to: arg.to, + }); + } + break; + } + case 'FLOAT': + case 'INT': + case 'UINT': + { + let value = 0; + const num = text.slice(arg.from, arg.to); + if (parameter.type === 'FLOAT') { + value = parseFloat(num); + } else { + value = parseInt(num); + } + parameter.allowable_ranges?.forEach(range => { + if (value < range.min || value > range.max) { + diagnostics.push({ + from: arg.from, + message: `Value must be between ${range.min} and ${range.max}`, + severity: 'error', + to: arg.to, + }); + } + }); + + if (parameter.type === 'UINT') { + if (value < 0) { + diagnostics.push({ + from: arg.from, + message: `UINT must be greater than or equal to zero`, + severity: 'error', + to: arg.to, + }); + } + } + if (arg.name !== 'Number') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be a number`, + severity: 'error', + to: arg.to, + }); + } + } + break; + case 'ENUM': + { + if (arg.name === 'Number' || arg.name === 'Boolean') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be an enum`, + severity: 'error', + to: arg.to, + }); + } else if (arg.name !== 'String') { + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Incorrect type - expected double quoted 'enum' but got ${arg.name}`, + severity: 'error', + to: argNode.to, + }); + } + const enumValue = text.slice(arg.from, arg.to).replace(/^"|"$/g, ''); + if (parameter.allowable_values?.indexOf(enumValue) === -1) { + diagnostics.push({ + from: arg.from, + message: `Enum should be "${parameter.allowable_values?.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${parameter.allowable_values!.length > MAX_ENUMS_TO_SHOW ? '...' : ''}"`, + severity: 'error', + to: arg.to, + }); + } + } + + break; + default: + break; + } + }); + } + }); + + return diagnostics; +} + function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { const diagnostics: Diagnostic[] = []; node.getChildren('GenericDirective').forEach(directiveNode => { @@ -987,80 +1143,20 @@ function validateAndLintArguments( // Initialize an array to store the validation errors let diagnostics: Diagnostic[] = []; - // Validate argument presence based on dictionary definition - if (dictArgs.length > 0) { - if (!argNode || argNode.length === 0) { - diagnostics.push({ - actions: [], - from: command.from, - message: 'The command is missing arguments.', - severity: 'error', - to: command.to, - }); - return diagnostics; + const structureError = validateCommandStructure(command, argNode, dictArgs.length, (view: any) => { + if (commandDictionary) { + addDefaultArgs( + commandDictionary, + view, + command, + dictArgs.slice(argNode ? argNode.length : 0), + new SeqNCommandInfoMapper(), + ); } + }); - if (argNode.length > dictArgs.length) { - const extraArgs = argNode.slice(dictArgs.length); - const { from, to } = getFromAndTo(extraArgs); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, to } }); - }, - name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, - }, - ], - from, - message: `Extra arguments, definition has ${dictArgs.length}, but ${argNode.length} are present`, - severity: 'error', - to, - }); - return diagnostics; - } else if (argNode.length < dictArgs.length) { - const { from, to } = getFromAndTo(argNode); - const pluralS = dictArgs.length > argNode.length + 1 ? 's' : ''; - diagnostics.push({ - actions: [ - { - apply(view) { - if (commandDictionary) { - addDefaultArgs( - commandDictionary, - view, - command, - dictArgs.slice(argNode.length), - new SeqNCommandInfoMapper(), - ); - } - }, - name: `Add default missing argument${pluralS}`, - }, - ], - from, - message: `Missing argument${pluralS}, definition has ${argNode.length}, but ${dictArgs.length} are present`, - severity: 'error', - to, - }); - return diagnostics; - } - } else if (argNode && argNode.length > 0) { - const { from, to } = getFromAndTo(argNode); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, to } }); - }, - name: `Remove argument${argNode.length > 1 ? 's' : ''}`, - }, - ], - from: from, - message: 'The command should not have arguments', - severity: 'error', - to: to, - }); + if (structureError) { + diagnostics.push(structureError); return diagnostics; } @@ -1112,6 +1208,86 @@ function validateAndLintArguments( return diagnostics; } +/** + * Validates the command structure. + * @param stemNode - The SyntaxNode representing the command stem. + * @param argsNode - The SyntaxNode representing the command arguments. + * @param exactArgSize - The expected number of arguments. + * @param addDefault - The function to add default arguments. + * @returns A Diagnostic object representing the validation error, or undefined if there is no error. + */ +function validateCommandStructure( + stemNode: SyntaxNode, + argsNode: SyntaxNode[] | null, + exactArgSize: number, + addDefault: (view: any) => any, +): Diagnostic | undefined { + if (arguments.length > 0) { + if (!argsNode || argsNode.length === 0) { + return { + actions: [], + from: stemNode.from, + message: `The stem is missing arguments.`, + severity: 'error', + to: stemNode.to, + }; + } + if (argsNode.length > exactArgSize) { + const extraArgs = argsNode.slice(exactArgSize); + const { from, to } = getFromAndTo(extraArgs); + return { + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, + }, + ], + from, + message: `Extra arguments, definition has ${exactArgSize}, but ${argsNode.length} are present`, + severity: 'error', + to, + }; + } + if (argsNode.length < exactArgSize) { + const { from, to } = getFromAndTo(argsNode); + const pluralS = exactArgSize > argsNode.length + 1 ? 's' : ''; + return { + actions: [ + { + apply(view) { + addDefault(view); + }, + name: `Add default missing argument${pluralS}`, + }, + ], + from, + message: `Missing argument${pluralS}, definition has ${argsNode.length}, but ${exactArgSize} are present`, + severity: 'error', + to, + }; + } + } else if (argsNode && argsNode.length > 0) { + const { from, to } = getFromAndTo(argsNode); + return { + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + name: `Remove argument${argsNode.length > 1 ? 's' : ''}`, + }, + ], + from: from, + message: 'The command should not have arguments', + severity: 'error', + to: to, + }; + } + return undefined; +} + /** + * Validates the given FSW command argument against the provided syntax node, + * and generates diagnostics if the validation fails. diff --git a/src/utilities/sequence-editor/to-seq-json.test.ts b/src/utilities/sequence-editor/to-seq-json.test.ts index fc77fc2a64..dd86bcec23 100644 --- a/src/utilities/sequence-editor/to-seq-json.test.ts +++ b/src/utilities/sequence-editor/to-seq-json.test.ts @@ -7,11 +7,13 @@ import { type FswCommandArgumentMap, type HwCommand, } from '@nasa-jpl/aerie-ampcs'; +import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import { readFileSync } from 'fs'; import { describe, expect, it } from 'vitest'; import { SeqLanguage } from '../codemirror'; +import { parser } from '../codemirror/sequence.grammar'; import { seqJsonToSequence } from './from-seq-json'; -import { sequenceToSeqJson } from './to-seq-json'; +import { parseVariables, sequenceToSeqJson } from './to-seq-json'; function argArrToMap(cmdArgs: FswCommandArgument[]): FswCommandArgumentMap { return cmdArgs.reduce((argMap, arg) => ({ ...argMap, [arg.name]: arg }), {}); @@ -1175,3 +1177,59 @@ C CMD_0 true false [ false true ] }; expect(actual).toEqual(expected); }); + +describe('parseVariables', () => { + const seqN = [ + `@LOCALS_BEGIN +TEMPERATURE INT +SIZE INT "-1...20, 40..." +STORE ENUM STORE_NAME "" "MACY, ROSS, BEST_BUY" +CHARGE +@LOCALS_END`, + `@INPUT_PARAMS_BEGIN +DOOR_ENABLED INT +X_RANGE INT "...10" "-1,0,3,4,5,9" +@INPUT_PARAMS_END`, + ]; + + it('should return undefined if there are no variable children', () => { + let tree = parser.parse(`${seqN[0]}`); + let variable = parseVariables(tree.topNode, seqN[0], 'LocalDeclaration'); + expect(variable).toBeDefined(); + + expect(variable?.length).toEqual(4); + if (variable) { + expect(variable[0]).toEqual({ name: 'TEMPERATURE', type: 'INT' } as VariableDeclaration); + expect(variable[1]).toEqual({ + allowable_ranges: [ + { max: 20, min: -1 }, + { max: Infinity, min: 40 }, + ], + name: 'SIZE', + type: 'INT', + } as VariableDeclaration); + expect(variable[2]).toEqual({ + allowable_values: ['MACY', 'ROSS', 'BEST_BUY'], + enum_name: 'STORE_NAME', + name: 'STORE', + type: 'ENUM', + }); + expect(variable[3]).toEqual({ name: 'CHARGE', type: 'INT' }); + } + + tree = parser.parse(`${seqN[1]}`); + variable = parseVariables(tree.topNode, seqN[1], 'ParameterDeclaration'); + expect(variable).toBeDefined(); + + expect(variable?.length).toEqual(2); + if (variable) { + expect(variable[0]).toEqual({ name: 'DOOR_ENABLED', type: 'INT' } as VariableDeclaration); + expect(variable[1]).toEqual({ + allowable_ranges: [{ max: 10, min: -Infinity }], + allowable_values: ['-1', '0', '3', '4', '5', '9'], + name: 'X_RANGE', + type: 'INT', + } as VariableDeclaration); + } + }); +}); diff --git a/src/utilities/sequence-editor/to-seq-json.ts b/src/utilities/sequence-editor/to-seq-json.ts index bb863b5d23..a885e9d98a 100644 --- a/src/utilities/sequence-editor/to-seq-json.ts +++ b/src/utilities/sequence-editor/to-seq-json.ts @@ -442,7 +442,7 @@ function parseTime(commandNode: SyntaxNode, text: string): Time { // min length of one type VariableDeclarationArray = [VariableDeclaration, ...VariableDeclaration[]]; -function parseVariables( +export function parseVariables( node: SyntaxNode, text: string, type: 'LocalDeclaration' | 'ParameterDeclaration' = 'LocalDeclaration',