import fs from "fs"; import bootstrap from "bootstrap"; import React from "react"; import { createRoot } from "react-dom/client"; import { observable, computed, action, autorun, Lambda, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { humanize } from "eez-studio-shared/string"; import { Loader } from "eez-studio-ui/loader"; import { createObject, objectToJS, ProjectStore } from "project-editor/store"; import { InstrumentCommand } from "project-editor/features/instrument-commands/instrument-commands"; import { ProjectContext } from "project-editor/project/context"; import type { Project } from "project-editor/project/project"; //////////////////////////////////////////////////////////////////////////////// interface CommandDefinition { command: string; helpLink?: string; } interface Changes { added: CommandDefinition[]; deleted: CommandDefinition[]; updated: CommandDefinition[]; } class FindChanges { constructor(private projectStore: ProjectStore) {} cleanUpScpiCommand(command: string) { command = command.trim(); let i = command.lastIndexOf(" "); if (i !== -1) { command = command.slice(i + 1); } if (command.indexOf("(") != -1 || command.indexOf(")") != -1) { return undefined; } let m = command.match(/[A-Z]{3,}/); if (!m) { // no 3 or more upper case letters return false; } if (command.length < 4) { return undefined; } return command; } getCommandFromSyntax(command: string) { command = command.trim(); let i = command.indexOf(" "); if (i !== -1) { return command.slice(0, i); } return command; } getCommandsFromInstrumentFileDoc( file: string, anchorElements: NodeListOf ) { let commands: CommandDefinition[] = []; let commandBookmark: string | undefined; for (let i = 0; i < anchorElements.length; i++) { let bookmark = anchorElements[i].getAttribute("name"); if (bookmark) { let parentElement = anchorElements[i].parentElement; if (parentElement != null) { let text = parentElement.textContent; if (text) { if (bookmark.startsWith("_eez-iext-cmd_")) { commands.push({ command: this.getCommandFromSyntax(text), helpLink: file + "#" + (commandBookmark || bookmark) }); } else { commandBookmark = bookmark; } } } } } return commands; } getCommandsFromScpiDoc() { return new Promise((resolve, reject) => { if ( this.projectStore.project.settings.general.commandsDocFolder === undefined ) { reject( this.projectStore.project.scpi ? "SCPI help folder is not defined" : "Commands help folder is not defined" ); return; } let commandsHelpFolderPath = this.projectStore.getAbsoluteFilePath( this.projectStore.project.settings.general.commandsDocFolder ); fs.exists(commandsHelpFolderPath, (exists: boolean) => { if (!exists) { reject( this.projectStore.project.scpi ? `SCPI help folder "${commandsHelpFolderPath}" doesn't exists.` : `Commands help folder "${commandsHelpFolderPath}" doesn't exists.` ); } else { fs.readdir( commandsHelpFolderPath, (err: any, files: string[]) => { if (err) { reject(err); return; } files = files.filter(file => file.endsWith("html")); let promises: Promise[] = files.map(file => { return new Promise( (resolve, reject) => { fs.readFile( commandsHelpFolderPath + "/" + file, "utf-8", (err: any, data: string) => { if (!err) { let element = document.createElement( "div" ); element.innerHTML = data; let anchorElements = element.querySelectorAll( "A[name]" ); let commands = this.getCommandsFromInstrumentFileDoc( file, anchorElements ); resolve(commands); } else { resolve([]); } } ); } ); }); Promise.all(promises) .then(results => { let allCommands: CommandDefinition[] = []; results.forEach(commands => { if (commands) { allCommands = allCommands.concat(commands); } }); resolve(allCommands); }) .catch(err => reject(err)); } ); } }); }); } static compareCommandDefinitions( a: CommandDefinition, b: CommandDefinition ) { let c1 = a.command.toUpperCase(); let c2 = b.command.toUpperCase(); return c1 < c2 ? -1 : c1 > c2 ? 1 : 0; } findMissingCommands( commands1: CommandDefinition[], commands2: CommandDefinition[] ) { let missingCommands: CommandDefinition[] = []; commands1.forEach(command1 => { if ( !commands2.find( command2 => command1.command == command2.command ) ) { missingCommands.push(command1); } }); return missingCommands.sort(FindChanges.compareCommandDefinitions); } getChanges() { return new Promise((resolve, reject) => { this.getCommandsFromScpiDoc() .then(commands => { let existingCommands = objectToJS( this.projectStore.project.instrumentCommands.commands ) as CommandDefinition[]; // added let added = this.findMissingCommands( commands, existingCommands ); // deleted let deleted = this.findMissingCommands( existingCommands, commands ); // updated let updated: CommandDefinition[] = []; existingCommands.forEach(existingCommand => { commands.forEach(command => { if ( command.command == existingCommand.command && command.helpLink != existingCommand.helpLink ) { updated.push(command); } }); }); resolve({ added, deleted, updated }); }) .catch(reject); }); } } //////////////////////////////////////////////////////////////////////////////// function findInstrumentCommand(project: Project, commandName: string) { for (const command of project.instrumentCommands.commands) { if (command.command === commandName) { return command; } } return undefined; } //////////////////////////////////////////////////////////////////////////////// type Section = "added" | "deleted" | "updated"; const SECTIONS: Section[] = ["added", "deleted", "updated"]; export const ImportCommandsDocDialog = observer( class ImportCommandsDocDialog extends React.Component<{ onHidden: () => void; }> { static contextType = ProjectContext; declare context: React.ContextType; constructor(props: any) { super(props); makeObservable(this, { changes: observable, selectedChanges: observable, error: observable, activeTab: observable, hasChanges: computed, hasSelectedChanges: computed, onOk: action, handleTabClick: action, handleSelectCommand: action }); } dialog: HTMLDivElement; modal: bootstrap.Modal; addedSelectAllCheckbox: HTMLInputElement; deletedSelectAllCheckbox: HTMLInputElement; updatedSelectAllCheckbox: HTMLInputElement; changes: Changes; selectedChanges: Changes = { added: [], deleted: [], updated: [] }; error: any; activeTab: Section; selectAllCheckboxDisposers: { added?: Lambda; deleted?: Lambda; updated?: Lambda; } = {}; get hasChanges() { return ( this.changes.added.length > 0 || this.changes.deleted.length > 0 || this.changes.updated.length > 0 ); } get hasSelectedChanges() { return ( this.selectedChanges.added.length > 0 || this.selectedChanges.deleted.length > 0 || this.selectedChanges.updated.length > 0 ); } componentDidMount() { $(this.dialog).on("hidden.bs.modal", () => { this.props.onHidden(); }); this.modal = new bootstrap.Modal(this.dialog); this.modal.show(); const findChanges = new FindChanges(this.context); findChanges .getChanges() .then( action((changes: Changes) => { this.changes = changes; if (changes.added.length > 0) { this.activeTab = "added"; } else if (changes.deleted.length > 0) { this.activeTab = "deleted"; } else if (changes.updated.length > 0) { this.activeTab = "updated"; } }) ) .catch( action((err: any) => { this.error = err; }) ); this.handleSelectAllCheckboxes(); } componentDidUpdate() { this.handleSelectAllCheckboxes(); } componentWillUnmount() { SECTIONS.forEach(section => { let disposer = this.selectAllCheckboxDisposers[section]; if (disposer) { disposer(); } }); } onOkCalled = false; onOk = (event: any) => { event.preventDefault(); if (this.onOkCalled) { return; } this.onOkCalled = true; this.context.backgroundCheckEnabled = false; this.modal.hide(); this.context.undoManager.setCombineCommands(true); this.selectedChanges.added.forEach(commandDefinition => { const command = createObject( this.context, commandDefinition as any, InstrumentCommand ); this.context.addObject( this.context.project.instrumentCommands.commands, command ); }); this.selectedChanges.deleted.forEach(commandDefinition => { let instrumentCommand = findInstrumentCommand( this.context.project, commandDefinition.command ); if (instrumentCommand) { this.context.deleteObject(instrumentCommand); } }); this.selectedChanges.updated.forEach(commandDefinition => { let InstrumentCommand = findInstrumentCommand( this.context.project, commandDefinition.command ); if (InstrumentCommand) { this.context.updateObject(InstrumentCommand, { helpLink: commandDefinition.helpLink }); } }); this.context.undoManager.setCombineCommands(false); this.context.backgroundCheckEnabled = true; }; onCancel = () => { this.modal.hide(); }; handleTabClick(activeTab: Section, event: any) { event.preventDefault(); this.activeTab = activeTab; } handleSelectAllCheckboxes() { SECTIONS.forEach(section => { if (this.selectAllCheckboxDisposers[section]) { return; } let checkbox: HTMLInputElement; if (section === "added") { checkbox = this.addedSelectAllCheckbox; } else if (section === "deleted") { checkbox = this.deletedSelectAllCheckbox; } else { // section === "updated" checkbox = this.updatedSelectAllCheckbox; } if (!checkbox) { return; } this.selectAllCheckboxDisposers[section] = autorun(() => { if (this.selectedChanges[section].length == 0) { checkbox.indeterminate = false; checkbox.checked = false; } else if ( this.selectedChanges[section].length == this.changes[section].length ) { checkbox.indeterminate = false; checkbox.checked = true; } else { checkbox.indeterminate = true; checkbox.checked = false; } }); checkbox.addEventListener( "click", action((event: any) => { if (this.selectedChanges[section].length == 0) { (this.selectedChanges as any)[section] = this.changes[section].slice(); } else { this.selectedChanges[section] = []; } }) ); }); } isChangeSelected( section: Section, changeDefinition: CommandDefinition ) { let commandDefinitions: any = this.selectedChanges[section]; return commandDefinitions.indexOf(changeDefinition) !== -1; } handleSelectCommand( section: Section, commandDefinition: CommandDefinition, event: any ) { let commandDefinitions: any = this.selectedChanges[section]; if (event.target.checked) { commandDefinitions.push(commandDefinition); } else { let i = commandDefinitions.indexOf(commandDefinition); commandDefinitions.splice(i, 1); } } render() { let content; let buttons; if (this.error) { content =
{this.error}
; buttons = [ ]; } else if (this.changes) { if (this.hasChanges) { let tabs = SECTIONS.filter( section => this.changes[section].length > 0 ).map(section => (
  • {humanize(section).toUpperCase()} {this.changes[section].length}
  • )); let tables = SECTIONS.map(section => { if (this.changes[section].length === 0) { return; } let thead; if (section === "added") { thead = ( (this.addedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command To ); } else if (section === "deleted") { thead = ( (this.deletedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command From ); } else if (section === "updated") { thead = ( (this.updatedSelectAllCheckbox = ref!) } type="checkbox" />{" "} Command In ); } let tbody = (this.changes[section] as any).map( (commandDefinition: CommandDefinition) => { let checkbox = ( ); return ( {checkbox}{" "} {commandDefinition.command} ); } ); return ( {thead}{tbody}
    ); }); content = (
      {tabs}
    {tables}
    ); buttons = [ , ]; } else { content = (

    No changes!

    ); buttons = [ ]; } } else { content = ; } let footer; if (buttons && buttons.length > 0) { footer =
    {buttons}
    ; } return (
    (this.dialog = ref!)} className={"modal fade EezStudio_ImportScpiDocDialogDiv"} tabIndex={-1} role="dialog" >
    Detected Instrument Command Changes
    {content}
    {footer}
    ); } } ); //////////////////////////////////////////////////////////////////////////////// export function showImportCommandsDocDialog(projectStore: ProjectStore) { let el = document.createElement("div"); document.body.appendChild(el); const root = createRoot(el); root.render( { el.remove(); }} /> ); }