export function useManualEditDSL()

in frontend/apps/quantgrid/src/app/hooks/ManualEditDSL/useManualEditDSL.ts [58:1787]


export function useManualEditDSL() {
  const {
    parsedSheets,
    projectName,
    sheetContent,
    projectSheets,
    functions,
    sheetName,
    selectedCell,
  } = useContext(ProjectContext);
  const { viewGridData } = useContext(ViewportContext);
  const {
    updateDSL,
    findTableField,
    findTable,
    findLastTableField,
    findFieldOnLeftOrRight,
    findContext,
    updateFieldSize,
  } = useDSLUtils();
  const gridApi = useGridApi();
  const { deleteTable, deleteTables } = useManualDeleteTableDSL();
  const {
    removeOverride,
    renameOverrideField,
    removeFieldOverride,
    removeOverrideRow,
  } = useOverridesManualEditDSL();
  const { renameSortField, removeSortField } = useApplySortManualEditDSL();
  const { renameFilterField, removeFilterField } =
    useApplyFilterManualEditDSL();
  const { removeTotalByIndex, renameTotalField, removeTotalByField } =
    useTotalManualEditDSL();

  const renameTable = useCallback(
    (oldName: string, newName: string) => {
      if (!projectName || !sheetContent) return;

      if (oldName === newName) return;

      const targetTable = findTable(oldName);

      if (!targetTable?.dslTableNamePlacement) return;

      const { end, start } = targetTable.dslTableNamePlacement;

      const uniqueNewTableName = escapeTableName(
        createUniqueName(
          unescapeTableName(newName),
          getAllTableNames(projectSheets)
        )
      );

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        uniqueNewTableName +
        sheetContent.substring(end);

      const historyTitle = `Rename table "${oldName}" to "${uniqueNewTableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [projectName, findTable, projectSheets, sheetContent, updateDSL]
  );

  const moveTable = useCallback(
    (tableName: string, rowDelta: number, colDelta: number) => {
      if (!projectName || !sheetContent) return;

      const targetTable = findTable(tableName);

      if (!targetTable) return;

      const placementDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === 'placement'
      );

      let dsl = sheetContent;

      if (placementDecorator?.dslPlacement) {
        if (rowDelta === 0 && colDelta === 0) return;

        const { start, end } = placementDecorator.dslPlacement;
        const [startRow, startCol] = placementDecorator.params[0] as [
          number,
          number
        ];

        dsl =
          sheetContent.substring(0, start) +
          getPlacementDecorator(startCol + colDelta, startRow + rowDelta) +
          newLine +
          sheetContent.substring(end);

        const historyTitle = `Move table "${tableName}" to (${
          startRow + rowDelta
        }, ${startCol + colDelta})`;
        updateDSL(dsl, historyTitle);
      } else {
        if (!targetTable.dslTableNamePlacement) return;
        dsl =
          sheetContent.substring(
            0,
            targetTable.dslTableNamePlacement.start - tableKeywordLength
          ) +
          getPlacementDecorator(1 + colDelta, 1 + rowDelta) +
          `${newLine}${tableKeyword} ` +
          sheetContent.substring(targetTable.dslTableNamePlacement.start);

        const historyTitle = `Move table "${tableName}" to (${rowDelta + 1}, ${
          colDelta + 1
        })`;
        updateDSL(dsl, historyTitle);
      }
    },
    [projectName, findTable, sheetContent, updateDSL]
  );

  const moveTableTo = useCallback(
    (tableName: string, row: number, col: number) => {
      if (!projectName || !sheetContent) return;

      const targetTable = findTable(tableName);

      if (!targetTable) return;

      const placementDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === placementDecoratorName
      );

      let dsl = sheetContent;

      if (placementDecorator?.dslPlacement) {
        const { start, end } = placementDecorator.dslPlacement;
        const [startRow, startCol] = placementDecorator.params[0] as [
          number,
          number
        ];

        if (row === startRow && col === startCol) return;

        dsl =
          sheetContent.substring(0, start) +
          getPlacementDecorator(col, row) +
          newLine +
          sheetContent.substring(end);

        const historyTitle = `Move table "${tableName}" to (${row}, ${col})`;
        updateDSL(dsl, historyTitle);
      } else {
        if (!targetTable.dslTableNamePlacement) return;
        dsl =
          sheetContent.substring(
            0,
            targetTable.dslTableNamePlacement.start - tableKeywordLength
          ) +
          `!placement(${row}, ${col})${newLine}${tableKeyword} ` +
          sheetContent.substring(targetTable.dslTableNamePlacement.start);

        const historyTitle = `Move table "${tableName}" to (${row}, ${col})`;
        updateDSL(dsl, historyTitle);
      }
    },
    [projectName, findTable, sheetContent, updateDSL]
  );

  const renameField = useCallback(
    (tableName: string, oldName: string, newName: string) => {
      if (!projectName || !sheetContent) return;

      const targetTable = findTable(tableName);
      const targetField = findTableField(tableName, oldName);

      if (!targetTable || !targetField?.dslFieldNamePlacement) return;

      const { end, start } = targetField.dslFieldNamePlacement;

      const sanitizedNewName = escapeFieldName(newName);

      if (sanitizedNewName === oldName) return;

      const fields = targetTable.fields.map((field) => field.key.fieldName);
      const uniqueNewFieldName = createUniqueName(sanitizedNewName, fields);

      let updatedSheetContent = renameOverrideField(
        tableName,
        oldName,
        uniqueNewFieldName
      );

      updatedSheetContent = renameSortField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = renameFilterField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = renameTotalField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      if (updatedSheetContent) {
        updatedSheetContent =
          updatedSheetContent.substring(0, start) +
          `[${uniqueNewFieldName}]` +
          updatedSheetContent.substring(end);
      } else {
        updatedSheetContent =
          sheetContent.substring(0, start) +
          `[${uniqueNewFieldName}]` +
          sheetContent.substring(end);
      }

      const historyTitle = `Rename field [${oldName}] to [${uniqueNewFieldName}] in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [
      projectName,
      findTable,
      findTableField,
      renameOverrideField,
      sheetContent,
      updateDSL,
      renameFilterField,
      renameSortField,
      renameTotalField,
    ]
  );

  const editExpression = useCallback(
    (tableName: string, fieldName: string, expression: string) => {
      const context = findContext(tableName, fieldName);

      if (!projectName || !context || !context.field) return;

      const { table, sheetContent, field } = context;

      let targetField: ParsedField | undefined = field;

      if (field?.isDynamic) {
        targetField = table.fields.find(
          (f) => f.key.fieldName === dynamicFieldName
        );
      }

      if (!targetField?.dslFieldNamePlacement) return;

      const { expressionMetadata, dslFieldNamePlacement } = targetField;
      const initialExpression = expressionMetadata?.text || '';
      const sanitizedExpression = sanitizeExpression(
        expression,
        initialExpression
      );
      const fixedExpression = autoFixSingleExpression(
        sanitizedExpression,
        functions,
        parsedSheets,
        targetField.key.tableName
      );

      let updatedSheetContent = '';

      if (expressionMetadata && dslFieldNamePlacement) {
        const { end } = expressionMetadata;
        const start = fixedExpression
          ? expressionMetadata.start
          : dslFieldNamePlacement.end;

        updatedSheetContent =
          sheetContent.substring(0, start) +
          fixedExpression +
          sheetContent.substring(end + 1);
      } else {
        const { end } = targetField.dslFieldNamePlacement;
        updatedSheetContent =
          sheetContent.substring(0, end) +
          ` = ${fixedExpression}` +
          sheetContent.substring(end + 1);
      }

      const historyTitle = `Update expression of field [${fieldName}] in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [parsedSheets, projectName, findContext, functions, updateDSL]
  );

  const editExpressionWithOverrideRemove = useCallback(
    (
      tableName: string,
      fieldName: string,
      expression: string,
      overrideIndex: number,
      overrideValue: OverrideValue
    ) => {
      const context = findContext(tableName, fieldName);

      if (!projectName || !context || !context.field) return;

      const { table, sheetContent, field } = context;

      let targetField: ParsedField | undefined = field;

      if (field?.isDynamic) {
        targetField = table.fields.find(
          (f) => f.key.fieldName === dynamicFieldName
        );
      }

      if (
        !targetField?.dslFieldNamePlacement ||
        !targetField.expressionMetadata
      )
        return;

      const { end, start, text } = targetField.expressionMetadata;
      const sanitizedExpression = sanitizeExpression(expression, text);
      const fixedExpression = autoFixSingleExpression(
        sanitizedExpression,
        functions,
        parsedSheets,
        targetField.key.tableName
      );

      const result = removeOverrideDSL(
        table,
        fieldName,
        overrideIndex,
        overrideValue,
        sheetContent
      );

      if (!result) return;

      if (result.tableRemoved) {
        const { updatedSheetContent, historyTitle } = result;
        updateDSL(updatedSheetContent, historyTitle);

        return;
      }

      const updatedSheetContent =
        result.updatedSheetContent.substring(0, start) +
        fixedExpression +
        result.updatedSheetContent.substring(end + 1);

      const historyTitle = `Update expression of field [${fieldName}] in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);

      return true;
    },
    [parsedSheets, projectName, findContext, functions, updateDSL]
  );

  const renameFieldWithHeaderDisplay = useCallback(
    (tableName: string, oldName: string, newName: string) => {
      if (!projectName || !sheetContent) return;

      const targetTable = findTable(tableName);
      const targetField = findTableField(tableName, oldName);

      if (!targetTable || !targetField?.dslFieldNamePlacement) return;

      const { end, start } = targetField.dslFieldNamePlacement;

      const sanitizedNewName = escapeFieldName(newName);

      if (sanitizedNewName === oldName) return;

      const fields = targetTable.fields.map((field) => field.key.fieldName);
      const uniqueNewFieldName = createUniqueName(sanitizedNewName, fields);

      let updatedSheetContent = renameOverrideField(
        tableName,
        oldName,
        uniqueNewFieldName
      );

      updatedSheetContent = renameSortField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = renameFilterField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = renameTotalField(
        tableName,
        oldName,
        uniqueNewFieldName,
        updatedSheetContent || sheetContent
      );

      if (updatedSheetContent) {
        updatedSheetContent =
          updatedSheetContent.substring(0, start) +
          `[${uniqueNewFieldName}]` +
          updatedSheetContent.substring(end);
      } else {
        updatedSheetContent =
          sheetContent.substring(0, start) +
          `[${uniqueNewFieldName}]` +
          sheetContent.substring(end);
      }

      const placementDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === placementDecoratorName
      );
      const hideFieldsHeaderDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === hideTableFieldsDecoratorName
      );

      if (!placementDecorator?.dslPlacement || !hideFieldsHeaderDecorator)
        return;

      const [placementStartRow, placementStartCol] = placementDecorator
        .params[0] as [number, number];
      const { start: placementStartOffset, end: placementEndOffset } =
        placementDecorator.dslPlacement;
      const newPlacementDecorator = getPlacementDecorator(
        targetTable.getIsTableDirectionHorizontal()
          ? placementStartCol - 1
          : placementStartCol,
        targetTable.getIsTableDirectionHorizontal()
          ? placementStartRow
          : placementStartRow - 1
      );

      if (hideFieldsHeaderDecorator?.dslPlacement) {
        const { start: hideHeaderStartOffset, end: hideHeaderEndOffset } =
          hideFieldsHeaderDecorator.dslPlacement;

        if (hideHeaderStartOffset < placementStartOffset) {
          updatedSheetContent =
            updatedSheetContent.substring(0, hideHeaderStartOffset) +
            updatedSheetContent.substring(
              hideHeaderEndOffset,
              placementStartOffset
            ) +
            newPlacementDecorator +
            newLine +
            updatedSheetContent.substring(placementEndOffset);
        } else {
          updatedSheetContent =
            updatedSheetContent.substring(0, placementStartOffset) +
            newPlacementDecorator +
            newLine +
            updatedSheetContent.substring(
              placementEndOffset,
              hideHeaderStartOffset
            ) +
            updatedSheetContent.substring(hideHeaderEndOffset);
        }
      }

      const historyTitle = `Rename field [${oldName}] to [${uniqueNewFieldName}] in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [
      projectName,
      sheetContent,
      findTable,
      findTableField,
      renameOverrideField,
      renameSortField,
      renameFilterField,
      renameTotalField,
      updateDSL,
    ]
  );

  const renameTableWithHeaderDisplay = useCallback(
    (oldName: string, newName: string) => {
      if (!projectName || !sheetContent) return;

      if (oldName === newName) return;

      const targetTable = findTable(oldName);

      if (!targetTable?.dslTableNamePlacement) return;

      const { end, start } = targetTable.dslTableNamePlacement;

      const uniqueNewTableName = escapeTableName(
        createUniqueName(
          unescapeTableName(newName),
          getAllTableNames(projectSheets)
        )
      );

      let updatedSheetContent =
        sheetContent.substring(0, start) +
        uniqueNewTableName +
        sheetContent.substring(end);

      const placementDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === placementDecoratorName
      );
      const hideTableNameHeaderDecorator = targetTable.decorators.find(
        ({ decoratorName }) => decoratorName === hideTableHeaderDecoratorName
      );

      if (!placementDecorator?.dslPlacement || !hideTableNameHeaderDecorator)
        return;

      const [placementStartRow, placementStartCol] = placementDecorator
        .params[0] as [number, number];
      const { start: placementStartOffset, end: placementEndOffset } =
        placementDecorator.dslPlacement;
      const newPlacementDecorator = getPlacementDecorator(
        placementStartCol,
        placementStartRow - 1
      );

      if (hideTableNameHeaderDecorator.dslPlacement) {
        const { start: hideHeaderStartOffset, end: hideHeaderEndOffset } =
          hideTableNameHeaderDecorator.dslPlacement;

        if (hideHeaderStartOffset < placementStartOffset) {
          updatedSheetContent =
            updatedSheetContent.substring(0, hideHeaderStartOffset) +
            updatedSheetContent.substring(
              hideHeaderEndOffset,
              placementStartOffset
            ) +
            newPlacementDecorator +
            newLine +
            updatedSheetContent.substring(placementEndOffset);
        } else {
          updatedSheetContent =
            updatedSheetContent.substring(0, placementStartOffset) +
            newPlacementDecorator +
            newLine +
            updatedSheetContent.substring(
              placementEndOffset,
              hideHeaderStartOffset
            ) +
            updatedSheetContent.substring(hideHeaderEndOffset);
        }
      }

      const historyTitle = `Rename table "${oldName}" to "${uniqueNewTableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [projectName, sheetContent, findTable, projectSheets, updateDSL]
  );

  const deleteField = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName, fieldName);

      if (!context || !projectName) return;

      const { table, sheetContent, sheetName, field } = context;

      if (!field) return;

      if (table.getFieldsCount() === 1) {
        deleteTable(tableName);

        return;
      }

      let targetField: ParsedField | undefined = field;

      if (field?.isDynamic) {
        targetField = table.fields.find(
          (f) => f.key.fieldName === dynamicFieldName
        );
      }

      if (!targetField?.dslPlacement) return;

      const { start, end } = targetField.dslPlacement;
      const targetTableName = targetField.key.fieldName;

      let updatedSheetContent = removeTotalByField(
        tableName,
        targetTableName,
        sheetContent
      );

      updatedSheetContent = removeFieldOverride(
        tableName,
        targetTableName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = removeSortField(
        tableName,
        targetTableName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent = removeFilterField(
        tableName,
        targetTableName,
        updatedSheetContent || sheetContent
      );

      updatedSheetContent =
        updatedSheetContent.substring(0, start) +
        updatedSheetContent
          .substring(end + 1)
          .replace(newLine, '')
          .trimStart();

      const historyTitle = `Delete field [${fieldName}] from table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);

      if (gridApi) {
        const selection = gridApi.selection;

        if (selection) {
          gridApi.updateSelectionAfterDataChanged({
            startCol: selection.startCol,
            endCol: selection.startCol,
            startRow: selection.startRow,
            endRow: selection.startRow,
          });
        }
      }
    },
    [
      findContext,
      projectName,
      removeTotalByField,
      removeSortField,
      removeFilterField,
      removeFieldOverride,
      updateDSL,
      gridApi,
      deleteTable,
    ]
  );

  const deleteSelectedFieldOrTable = useCallback(() => {
    if (!gridApi || !sheetContent || gridApi?.isCellEditorOpen()) return;

    const selection = gridApi.getSelection();
    if (!selection) return;

    const { startCol, endCol, startRow, endRow } = selection;

    const tableStructure = viewGridData.getGridTableStructure();
    const tableMetas = findTablesInSelection(tableStructure, selection);
    if (!tableMetas.length) return;

    const tableNamesToDelete: string[] = [];
    tableMetas.forEach((tableMeta) => {
      const { tableName } = tableMeta;
      const startRowCell = gridApi.getCell(startCol, startRow);
      const endRowCell = gridApi.getCell(startCol, endRow);

      const tableHeaderRowSelected =
        startRowCell?.isTableHeader || endRowCell?.isTableHeader;
      const entireTableHeaderSelected =
        startCol === tableMeta.startCol && endCol === tableMeta.endCol;
      const entireTableSelected = isTableInsideSelection(tableMeta, selection);
      if (
        (tableHeaderRowSelected && entireTableHeaderSelected) ||
        entireTableSelected
      ) {
        tableNamesToDelete.push(tableName);

        return;
      }

      const table = findTable(tableName);
      if (!table) return;

      const fieldNames = new Array(Math.abs(startCol - endCol) + 1)
        .fill(0)
        .map((_, index) =>
          gridApi.getCell(Math.min(startCol, endCol) + index, startRow)
        )
        .filter((c) => c?.table?.tableName === tableName)
        .map((cell) => cell?.field?.fieldName)
        .filter(Boolean);
      const uniqueFields = Array.from(new Set(fieldNames))
        .map((fieldName) => findTableField(tableName, fieldName as string))
        .filter(Boolean);

      if (uniqueFields.length !== 1) {
        return;
      }

      let field = uniqueFields[0];
      if (!field) return;

      if (field.isDynamic) {
        field = findTableField(tableName, dynamicFieldName);

        if (!field) return;
      }

      const { fieldName } = field.key;
      const startCell = gridApi.getCell(startCol, startRow);
      const endCell = gridApi.getCell(startCol, endRow);

      if (startCell?.totalIndex) {
        removeTotalByIndex(tableName, fieldName, startCell.totalIndex);

        return;
      }

      if (startCell?.isFieldHeader || endCell?.isFieldHeader) {
        deleteField(tableName, fieldName);
      } else {
        if (
          !startCell ||
          startCell.overrideIndex === undefined ||
          startCell.value === undefined
        )
          return;

        const currentCellFieldName = uniqueFields[0]?.key.fieldName;

        removeOverride(
          tableName,
          currentCellFieldName ?? fieldName,
          startCell.overrideIndex,
          startCell.value
        );
      }
    });

    if (tableNamesToDelete.length > 1) {
      deleteTables(tableNamesToDelete);
    } else if (tableNamesToDelete.length === 1) {
      deleteTable(tableNamesToDelete[0]);
    }
  }, [
    deleteField,
    deleteTable,
    deleteTables,
    findTable,
    findTableField,
    gridApi,
    removeOverride,
    removeTotalByIndex,
    sheetContent,
    viewGridData,
  ]);

  const onRemoveRow = useCallback(
    (tableName: string, overrideIndex: number) => {
      if (!projectName) return;

      const table = findTable(tableName);

      if (!table || !table.isManual() || !sheetContent) return;

      const { overrides } = table;

      if (!overrides || !overrides.overrideRows) return;

      if (overrides.overrideRows.length > 1) {
        removeOverrideRow(tableName, overrideIndex);

        return;
      } else {
        deleteTable(tableName);
      }
    },
    [deleteTable, findTable, projectName, removeOverrideRow, sheetContent]
  );

  const onCloneTable = useCallback(
    (tableName: string, options: { col?: number; row?: number } = {}) => {
      if (!projectName) return;

      const sourceParsedSheet = Object.entries(parsedSheets).find(
        ([sheetName, sheet]) =>
          !!sheet.tables.find((table) => table.tableName === tableName)
      );
      const sourceProjectSheetContent = projectSheets?.find(
        (sheet) => sheet.sheetName === sourceParsedSheet?.[0]
      )?.content;
      const sourceTable = sourceParsedSheet?.[1]?.tables.find(
        (table) => table.tableName === tableName
      );
      if (!sourceTable || !sourceProjectSheetContent || !sheetName) return;

      const tableDSLPlacement = sourceTable.dslPlacement;
      const tableNameDSLPlacement = sourceTable.dslTableNamePlacement;
      const placementDecorator = sourceTable.decorators.find(
        (dec) => dec.decoratorName === placementDecoratorName
      );
      const placementDecoratorDSLPlacement = placementDecorator?.dslPlacement;
      if (!tableDSLPlacement || !placementDecorator?.dslPlacement) return;

      const tableBeforePlacementDSL = sourceProjectSheetContent.slice(
        tableDSLPlacement?.startOffset,
        placementDecoratorDSLPlacement?.start
      );
      const tableBeforeNameDSL = sourceProjectSheetContent.slice(
        placementDecoratorDSLPlacement?.end,
        tableNameDSLPlacement?.start
      );
      const tableAfterNameDSL = sourceProjectSheetContent.slice(
        tableNameDSLPlacement?.end,
        tableDSLPlacement?.stopOffset + 1
      );
      const uniqueNewTableName = escapeTableName(
        createUniqueName(
          unescapeTableName(tableName + ' clone'),
          getAllTableNames(projectSheets)
        )
      );

      let newTableCol = options.col;
      let newTableRow = options.row;
      if (!newTableCol || !newTableRow) {
        newTableCol = (placementDecorator.params[0][1] ?? 1) + 1;
        newTableRow = (placementDecorator.params[0][0] ?? 1) + 1;
      }

      const newTableDSL =
        newLine +
        newLine +
        tableBeforePlacementDSL +
        getPlacementDecorator(newTableCol!, newTableRow!) +
        newLine +
        tableBeforeNameDSL +
        uniqueNewTableName +
        tableAfterNameDSL +
        newLine;

      const updatedSheetContent =
        stripNewLinesAtEnd(sheetContent ?? '') + newTableDSL;

      const historyTitle = `Cloned table "${tableName}" with new name "${uniqueNewTableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [
      parsedSheets,
      projectName,
      projectSheets,
      sheetContent,
      sheetName,
      updateDSL,
    ]
  );

  const swapFields = useCallback(
    (
      tableName: string,
      rightFieldName: string,
      leftFieldName: string,
      direction: HorizontalDirection
    ) => {
      const rfContext = findContext(tableName, rightFieldName);
      const lfContext = findContext(tableName, leftFieldName);

      if (!rfContext || !lfContext || !projectName) return;

      if (!rfContext.sheetContent) return;

      const { sheetContent, sheetName } = rfContext;
      let rightField = rfContext.field || null;
      let leftField = lfContext.field || null;

      if (rightField?.isDynamic && leftField?.isDynamic) {
        if (direction === 'left') {
          leftField = findFieldOnLeftOrRight(
            tableName,
            rightFieldName,
            direction
          );
          rightField = findTableField(tableName, dynamicFieldName);
        } else {
          rightField = findFieldOnLeftOrRight(
            tableName,
            leftFieldName,
            direction
          );
          leftField = findTableField(tableName, dynamicFieldName);
        }

        if (!leftField || !rightField) return;
      } else if (rightField?.isDynamic) {
        rightField = findTableField(tableName, dynamicFieldName);
      } else if (leftField?.isDynamic) {
        leftField = findTableField(tableName, dynamicFieldName);
      }

      if (!rightField?.dslPlacement || !leftField?.dslPlacement) return;

      const { start: rightStart, end: rightEnd } = rightField.dslPlacement;
      const { start: leftStart, end: leftEnd } = leftField.dslPlacement;

      const updatedSheetContent =
        sheetContent.substring(0, leftStart) +
        sheetContent.substring(rightStart, rightEnd + 1) +
        sheetContent.substring(leftEnd + 1, rightStart) +
        sheetContent.substring(leftStart, leftEnd + 1) +
        sheetContent.substring(rightEnd + 1);

      const historyTitle = `Swap fields [${rightFieldName}] and [${leftFieldName}] in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [
      findContext,
      findFieldOnLeftOrRight,
      findTableField,
      projectName,
      updateDSL,
    ]
  );

  const swapFieldsHandler = useCallback(
    (tableName: string, fieldName: string, direction: HorizontalDirection) => {
      const context = findContext(tableName, fieldName);

      if (!context) return;

      const { table } = context;
      const { fields } = table;

      let rightFieldName = '';
      let leftFieldName = '';

      for (let i = 0; i < fields.length; i++) {
        const field = fields[i];

        if (field.key.fieldName === fieldName) {
          if (direction === 'left') {
            if (i === 0) return;
            rightFieldName = field.key.fieldName;
            leftFieldName = fields[i - 1].key.fieldName;
          } else {
            if (i === fields.length - 1) return;
            rightFieldName = fields[i + 1].key.fieldName;
            leftFieldName = field.key.fieldName;
          }
        }
      }

      if (rightFieldName && leftFieldName) {
        swapFields(tableName, rightFieldName, leftFieldName, direction);
      }
    },
    [findContext, swapFields]
  );

  const handleChangeColumnSize = useCallback(
    (tableName: string, fieldName: string, valueAdd: number) => {
      const isIncrease = valueAdd > 0;
      const targetTable = findTable(tableName);
      if (!projectName || !sheetContent || !targetTable) return;

      const fieldIndex = targetTable.fields.findIndex(
        (field) => field.key.fieldName === fieldName
      );
      const field = targetTable.fields[fieldIndex];

      if (!field) {
        return;
      }
      const newValue = field.getSize() + valueAdd;

      if (newValue <= 0 || !valueAdd) {
        return;
      }

      const { sizesDecoratorPlacement, fieldSizesSheetContent } =
        updateFieldSize({
          newValue,
          targetField: field,
        });

      const updatedSheetContent =
        sheetContent.substring(0, sizesDecoratorPlacement.start) +
        fieldSizesSheetContent +
        sheetContent.substring(sizesDecoratorPlacement.end);

      const historyTitle = `${
        isIncrease ? 'Increase' : 'Decrease'
      } [${fieldName}] column width in table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);

      if (!gridApi) {
        return;
      }

      const selection = gridApi.selection;
      if (!selection) return;

      gridApi.updateSelectionAfterDataChanged({
        startCol: selection.startCol,
        startRow: selection.startRow,
        endCol: selection.startCol,
        endRow: selection.startRow,
      });
    },
    [findTable, gridApi, projectName, sheetContent, updateDSL, updateFieldSize]
  );

  const onToggleTableHeaderVisibility = useCallback(
    (tableName: string) => {
      const targetTable = findTable(tableName);
      const tableDSLPlacement = targetTable?.dslPlacement;
      if (!projectName || !sheetContent || !targetTable || !tableDSLPlacement)
        return;

      const hideDecorator = targetTable.decorators.find(
        (decorator) => decorator.decoratorName === hideTableHeaderDecoratorName
      );

      let updatedSheetContent;

      if (hideDecorator?.dslPlacement) {
        updatedSheetContent =
          sheetContent.substring(0, hideDecorator.dslPlacement.start) +
          sheetContent.substring(hideDecorator.dslPlacement.end);
      } else {
        updatedSheetContent =
          sheetContent.substring(0, tableDSLPlacement.startOffset) +
          hideTableHeaderDecorator +
          newLine +
          sheetContent.substring(tableDSLPlacement.startOffset);
      }

      const historyTitleStart = hideDecorator ? 'Show' : 'Hide';
      const historyTitle = `${historyTitleStart} header of table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);

      if (!gridApi) {
        return;
      }

      const selection = gridApi.selection;
      if (!selection) return;

      gridApi.updateSelectionAfterDataChanged({
        startCol: selection.startCol,
        startRow: selection.startRow,
        endCol: selection.startCol,
        endRow: selection.startRow,
      });
    },
    [findTable, gridApi, projectName, sheetContent, updateDSL]
  );

  const onFlipTable = useCallback(
    (tableName: string) => {
      const targetTable = findTable(tableName);
      const tableDSLPlacement = targetTable?.dslPlacement;
      if (!projectName || !sheetContent || !targetTable || !tableDSLPlacement)
        return;

      const decorator = targetTable.decorators.find(
        (decorator) =>
          decorator.decoratorName === horizontalDirectionDecoratorName
      );

      let updatedSheetContent;

      if (decorator?.dslPlacement) {
        updatedSheetContent =
          sheetContent.substring(0, decorator.dslPlacement.start) +
          sheetContent.substring(decorator.dslPlacement.end);
      } else {
        updatedSheetContent =
          sheetContent.substring(0, tableDSLPlacement.startOffset) +
          horizontalDirectionDecorator +
          newLine +
          sheetContent.substring(tableDSLPlacement.startOffset);
      }

      const historyTitle = decorator
        ? `Make table "${tableName}" vertical`
        : `Make table "${tableName}" horizontal`;
      updateDSL(updatedSheetContent, historyTitle);

      if (!gridApi) {
        return;
      }

      const selection = gridApi.selection;
      if (!selection) return;

      gridApi.updateSelectionAfterDataChanged({
        startCol: selection.startCol,
        startRow: selection.startRow,
        endCol: selection.startCol,
        endRow: selection.startRow,
      });
    },
    [findTable, gridApi, projectName, sheetContent, updateDSL]
  );

  const onToggleTableFieldsVisibility = useCallback(
    (tableName: string) => {
      const targetTable = findTable(tableName);
      const tableDSLPlacement = targetTable?.dslPlacement;
      if (!projectName || !sheetContent || !targetTable || !tableDSLPlacement)
        return;

      const hideDecorator = targetTable.decorators.find(
        (decorator) => decorator.decoratorName === hideTableFieldsDecoratorName
      );

      let updatedSheetContent;

      if (hideDecorator?.dslPlacement) {
        updatedSheetContent =
          sheetContent.substring(0, hideDecorator.dslPlacement.start) +
          sheetContent.substring(hideDecorator.dslPlacement.end);
      } else {
        updatedSheetContent =
          sheetContent.substring(0, tableDSLPlacement.startOffset) +
          hideTableFieldsDecorator +
          newLine +
          sheetContent.substring(tableDSLPlacement.startOffset);
      }

      const historyTitleStart = hideDecorator ? 'Show' : 'Hide';
      const historyTitle = `${historyTitleStart} fields of table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);

      if (!gridApi) {
        return;
      }

      const selection = gridApi.selection;
      if (!selection) return;

      gridApi.updateSelectionAfterDataChanged({
        startCol: selection.startCol,
        startRow: selection.startRow,
        endCol: selection.startCol,
        endRow: selection.startRow,
      });
    },
    [findTable, gridApi, projectName, sheetContent, updateDSL]
  );

  const onIncreaseFieldColumnSize = useCallback(
    (tableName: string, fieldName: string) => {
      handleChangeColumnSize(tableName, fieldName, 1);
    },
    [handleChangeColumnSize]
  );
  const onDecreaseFieldColumnSize = useCallback(
    (tableName: string, fieldName: string) => {
      handleChangeColumnSize(tableName, fieldName, -1);
    },
    [handleChangeColumnSize]
  );

  const addField = useCallback(
    (
      tableName: string,
      fieldText: string,
      insertOptions: {
        direction?: HorizontalDirection;
        insertFromFieldName?: string;
        withSelection?: boolean;
      } = {}
    ) => {
      const targetTable = findTable(tableName);
      const { direction, insertFromFieldName, withSelection } = insertOptions;

      let targetField = findLastTableField(tableName);

      if (direction === 'left' && insertFromFieldName) {
        targetField = findFieldOnLeftOrRight(
          tableName,
          insertFromFieldName,
          'left'
        );
      }

      if (direction === 'right' && insertFromFieldName) {
        targetField = findTableField(tableName, insertFromFieldName);
      }

      if (targetField?.isDynamic) {
        targetField = findTableField(tableName, dynamicFieldName);
      }

      if (!targetTable || !sheetContent || !projectName) return;

      const { updatedSheetContent, fieldName } = addFieldToSheet({
        targetTable,
        targetField,
        sheetContent,
        fieldText,
        functions,
        parsedSheets,
      });

      if (!updatedSheetContent) return;

      const historyTitle = `Add [${fieldName}] to table "${targetTable.tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);

      if (withSelection) {
        setTimeout(() => {
          if (!selectedCell) return;

          const { col, row } = selectedCell;
          const cell = gridApi?.getCell(col, row);
          if (!cell) return;

          const { table } = cell;
          if (!table) return;

          const { isTableHorizontal, isTableNameHeaderHidden } = table;

          let fieldCell;
          if (isTableHorizontal) {
            let currentRow;
            const startRow = table.startRow + (isTableNameHeaderHidden ? 0 : 1);

            for (
              currentRow = startRow;
              currentRow <= table.endRow;
              currentRow++
            ) {
              fieldCell = gridApi?.getCell(table.startCol, currentRow);

              if (fieldCell?.field?.fieldName === fieldName) {
                break;
              }

              fieldCell = undefined;
            }
          } else {
            let currentCol;
            const startCol = table.startCol;
            const startRow = table.startRow + (isTableNameHeaderHidden ? 0 : 1);

            for (
              currentCol = startCol;
              currentCol <= table.endCol;
              currentCol++
            ) {
              fieldCell = gridApi?.getCell(currentCol, startRow);

              if (fieldCell?.field?.fieldName === fieldName) {
                break;
              }

              fieldCell = undefined;
            }
          }

          if (fieldCell) {
            gridApi?.updateSelection({
              startCol: fieldCell.col,
              startRow: fieldCell.row,
              endCol: fieldCell.col,
              endRow: fieldCell.row,
            });
            gridApi?.showCellEditor(fieldCell.col, fieldCell.row, '=', {
              dimFieldName: fieldName,
              withFocus: true,
            });
          }
        }, 500);
      }
    },
    [
      findTable,
      findLastTableField,
      sheetContent,
      projectName,
      functions,
      parsedSheets,
      updateDSL,
      findFieldOnLeftOrRight,
      findTableField,
      selectedCell,
      gridApi,
    ]
  );

  const removeDimension = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName, fieldName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName, field } = context;

      if (!sheetContent || !field?.dslDimensionPlacement) return;

      const { start, end } = field.dslDimensionPlacement;

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        sheetContent.substring(end + 1).trimStart();

      const historyTitle = `Remove dimension [${fieldName}] from table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, updateDSL]
  );

  const addDimension = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName, fieldName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName, field } = context;

      if (!sheetContent || !field?.dslFieldNamePlacement || field.isDim) return;

      const { start } = field.dslFieldNamePlacement;

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        `${dimKeyword} ` +
        sheetContent.substring(start).trimStart();

      const historyTitle = `Add dimension [${fieldName}] to table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, updateDSL]
  );

  const addKey = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName, fieldName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName, field } = context;

      if (!sheetContent || !field?.dslFieldNamePlacement || field.isKey) return;

      const start =
        field.isDim && field.dslDimensionPlacement
          ? field.dslDimensionPlacement.start
          : field.dslFieldNamePlacement.start;

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        `${keyKeyword} ` +
        sheetContent.substring(start).trimStart();

      const historyTitle = `Add key [${fieldName}] to table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, updateDSL]
  );

  const removeKey = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName, fieldName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName, field } = context;

      if (!sheetContent || !field?.dslKeyPlacement) return;

      const { start, end } = field.dslKeyPlacement;

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        sheetContent.substring(end + 1).trimStart();

      const historyTitle = `Remove key [${fieldName}] from table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, updateDSL]
  );

  const addFieldWithOverride = useCallback(
    ({
      tableName,
      fieldText,
      overrideCol,
      overrideRow,
      overrideValue,
    }: {
      tableName: string;
      fieldText: string;
      overrideCol: number;
      overrideRow: number;
      overrideValue: string;
    }) => {
      const table = findTable(tableName);
      const targetField = findLastTableField(tableName);

      if (!projectName || !sheetContent || !table || !targetField) return;

      const addFieldResult = addFieldToSheet({
        targetTable: table,
        targetField,
        sheetContent,
        fieldText,
        functions,
        parsedSheets,
      });

      table.fields.push(
        new ParsedField(
          false,
          false,
          false,
          {
            fieldName: addFieldResult.fieldName,
            fullFieldName: addFieldResult.fieldName,
            tableName: table.tableName,
          },
          naExpression,
          undefined,
          addFieldResult.fieldDslPlacement
        )
      );
      if (table.dslPlacement && addFieldResult.updatedSheetContent) {
        table.dslPlacement.stopOffset =
          table.dslPlacement.stopOffset +
          addFieldResult.updatedSheetContent.length -
          sheetContent.length;
      }
      if (table.dslOverridePlacement && addFieldResult.updatedSheetContent) {
        table.dslOverridePlacement.stopOffset =
          table.dslOverridePlacement.stopOffset +
          addFieldResult.updatedSheetContent.length -
          sheetContent.length;
        table.dslOverridePlacement.startOffset =
          table.dslOverridePlacement.startOffset +
          addFieldResult.updatedSheetContent.length -
          sheetContent.length;
      }

      const updatedSheetContent = addOverridesToSheet({
        table,
        cells: [[overrideValue]],
        gridApi,
        selectedCol: overrideCol,
        selectedRow: overrideRow,
        sheetContent: addFieldResult.updatedSheetContent,
      });

      if (!updatedSheetContent) {
        return;
      }

      const historyTitle = `Add new field "${addFieldResult.fieldName}" with override "${overrideValue}" to table "${tableName}"`;
      updateDSL(updatedSheetContent, historyTitle);
    },
    [
      findLastTableField,
      findTable,
      functions,
      gridApi,
      parsedSheets,
      projectName,
      sheetContent,
      updateDSL,
    ]
  );

  const chartResize = useCallback(
    (tableName: string, cols: number, rows: number) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName, table } = context;

      if (!sheetContent) return;

      const sizeDecorator = table.decorators.find(
        ({ decoratorName }) => decoratorName === 'size'
      );

      let updatedSheetContent = sheetContent;

      if (sizeDecorator?.dslPlacement) {
        const { start, end } = sizeDecorator.dslPlacement;
        const currentSize = sizeDecorator.params[0] as [number, number];

        if (currentSize[0] === rows && currentSize[1] === cols) return;

        updatedSheetContent =
          sheetContent.substring(0, start) +
          `!size(${rows}, ${cols})` +
          newLine +
          sheetContent.substring(end).trimStart();
      } else {
        if (!table.dslTableNamePlacement) return;
        updatedSheetContent =
          sheetContent.substring(
            0,
            table.dslTableNamePlacement.start - tableKeywordLength
          ) +
          `!size(${rows}, ${cols})${newLine}${tableKeyword} ` +
          sheetContent.substring(table.dslTableNamePlacement.start);
      }

      const historyTitle = `Resize chart [${tableName}] to (${rows}, ${cols})`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, updateDSL]
  );

  const addChart = useCallback(
    (tableName: string) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { table, sheetContent, sheetName } = context;

      if (!table?.dslPlacement || !table.dslTableNamePlacement) return;

      const { stopOffset } = table.dslPlacement;
      const { end } = table.dslTableNamePlacement;

      const chartTableName = createUniqueName(
        `${tableName}_chart`,
        getAllTableNames(projectSheets)
      );

      const [row, col] = table.getPlacement();
      const newCol = col + table.fields.length + 1;

      const decorators = table.decorators
        .filter(
          (d) =>
            d.decoratorName !== 'visualization' &&
            d.decoratorName !== 'size' &&
            d.decoratorName !== 'placement'
        )
        .map((d) => `!${d.decoratorName}(${d.params.join(',')})${newLine}`);

      const chartTable =
        'table ' + chartTableName + sheetContent.substring(end, stopOffset + 1);

      const updatedSheetContent =
        sheetContent +
        `${newLine}${newLine}!visualization("line-chart")${newLine}` +
        `!placement(${row},${newCol})${newLine}` +
        decorators +
        chartTable;

      const historyTitle = `Add chart "${chartTableName}"`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, projectName, projectSheets, updateDSL]
  );

  const convertToChart = useCallback(
    (tableName: string) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { table, sheetContent, sheetName } = context;

      const chartDecorator = table.decorators.find(
        ({ decoratorName }) => decoratorName === 'visualization'
      );

      if (chartDecorator || !table.dslTableNamePlacement) return;

      const { start } = table.dslTableNamePlacement;

      const updatedSheetContent =
        sheetContent.substring(0, start - tableKeywordLength) +
        `!visualization("line-chart")${newLine}${tableKeyword} ` +
        sheetContent.substring(start);

      const historyTitle = `Convert table "${tableName}" to chart`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [projectName, updateDSL, findContext]
  );

  const convertToTable = useCallback(
    (tableName: string) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { table, sheetContent, sheetName } = context;

      const chartDecorator = table.decorators.find(
        ({ decoratorName }) => decoratorName === 'visualization'
      );

      if (
        !chartDecorator ||
        !table.dslPlacement ||
        !table.dslTableNamePlacement
      )
        return;

      const { startOffset } = table.dslPlacement;
      const { start } = table.dslTableNamePlacement;

      const decorators = table.decorators
        .filter(
          (d) =>
            d.decoratorName !== 'visualization' && d.decoratorName !== 'size'
        )
        .map((d) => `!${d.decoratorName}(${d.params.join(',')})${newLine}`);

      const updatedSheetContent =
        sheetContent.substring(0, startOffset) +
        decorators +
        'table ' +
        sheetContent.substring(start);

      const historyTitle = `Convert chart "${tableName}" to table`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [projectName, updateDSL, findContext]
  );

  const removeNote = useCallback(
    (tableName: string, fieldName: string) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName } = context;

      const targetField = findTableField(tableName, fieldName);

      if (!targetField || !targetField.note) return;

      const { start, end } = targetField.note;

      const updatedSheetContent =
        sheetContent.substring(0, start) +
        sheetContent.substring(end + 1).replace(/^(\r\n|\r|\n)/, '');

      const historyTitle = `Remove note from ${tableName}[${fieldName}]`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, findTableField, projectName, updateDSL]
  );

  const updateNote = useCallback(
    (tableName: string, fieldName: string, note: string) => {
      const context = findContext(tableName);

      if (!context || !projectName) return;

      const { sheetContent, sheetName } = context;

      const targetField = findTableField(tableName, fieldName);

      if (!targetField) return;

      let updatedSheetContent = sheetContent;
      const dslComment = noteToComment(note);

      if (targetField.note) {
        const { start, end } = targetField.note;

        updatedSheetContent =
          sheetContent.substring(0, start) +
          dslComment +
          newLine +
          sheetContent.substring(end + 1);
      } else if (targetField.dslPlacement) {
        const { start } = targetField.dslPlacement;

        updatedSheetContent =
          sheetContent.substring(0, start).trimEnd() +
          `${newLine}${dslComment}${newLine}` +
          sheetContent.substring(start);
      }

      const historyTitle = `Update note for ${tableName}[${fieldName}]`;
      updateDSL(updatedSheetContent, historyTitle, sheetName);
    },
    [findContext, findTableField, projectName, updateDSL]
  );

  return {
    addChart,
    addDimension,
    addField,
    addKey,
    chartResize,
    convertToChart,
    convertToTable,
    deleteField,
    deleteSelectedFieldOrTable,
    editExpression,
    editExpressionWithOverrideRemove,
    moveTable,
    moveTableTo,
    onCloneTable,
    removeNote,
    removeDimension,
    removeKey,
    renameField,
    renameTable,
    updateNote,
    swapFields,
    swapFieldsHandler,
    onIncreaseFieldColumnSize,
    onDecreaseFieldColumnSize,
    onChangeFieldColumnSize: handleChangeColumnSize,
    addFieldWithOverride,
    onToggleTableHeaderVisibility,
    onToggleTableFieldsVisibility,
    onFlipTable,
    renameFieldWithHeaderDisplay,
    renameTableWithHeaderDisplay,
    onRemoveRow,
  };
}