diff --git a/src/common/ksx/eval-tree-async.ts b/src/common/ksx/eval-tree-async.ts index 250ba0c22..43a0ecc46 100644 --- a/src/common/ksx/eval-tree-async.ts +++ b/src/common/ksx/eval-tree-async.ts @@ -48,7 +48,6 @@ export type EvaluatorAsyncFunction = ( expr: Expression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ) => Promise; /** @@ -61,8 +60,7 @@ export type EvaluatorAsyncFunction = ( export async function evalBindingAsync ( expr: Expression, evalContext: EvaluationContext, - thread: LogicalThread | undefined, - onStatementCompleted?: OnStatementCompletedCallback + thread: LogicalThread | undefined ): Promise { const thisStack: any[] = []; ensureMainThread(evalContext); @@ -71,8 +69,7 @@ export async function evalBindingAsync ( thisStack, expr, evalContext, - thread ?? evalContext.mainThread!, - onStatementCompleted + thread ?? evalContext.mainThread! ); } @@ -87,7 +84,6 @@ export async function evalBindingAsync ( export async function executeArrowExpression ( expr: ArrowExpression, evalContext: EvaluationContext, - onStatementCompleted: OnStatementCompletedCallback, thread?: LogicalThread, ...args: any[] ): Promise { @@ -112,7 +108,6 @@ export async function executeArrowExpression ( expr.args, evalContext, thread ?? evalContext.mainThread, - onStatementCompleted, ...args ); } @@ -132,8 +127,7 @@ export async function evalBindingExpressionTreeAsync ( thisStack: any[], expr: Expression, evalContext: EvaluationContext, - thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback + thread: LogicalThread ): Promise { if (!evalContext.options) { evalContext.options = { defaultToOptionalMemberAccess: true }; @@ -159,8 +153,7 @@ export async function evalBindingExpressionTreeAsync ( thisStack, expr, evalContext, - thread, - onStatementCompleted + thread ); case "CalculatedMemberAccess": @@ -170,7 +163,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "SequenceExpression": @@ -180,7 +172,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "ArrayLiteral": @@ -190,7 +181,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "ObjectLiteral": @@ -200,7 +190,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "UnaryExpression": @@ -210,7 +199,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "BinaryExpression": @@ -220,7 +208,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "ConditionalExpression": @@ -230,7 +217,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "AssignmentExpression": @@ -240,7 +226,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "PrefixOpExpression": @@ -251,7 +236,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "FunctionInvocation": @@ -262,7 +246,6 @@ export async function evalBindingExpressionTreeAsync ( expr, evalContext, thread, - onStatementCompleted ); case "ArrowExpression": @@ -283,15 +266,13 @@ async function evalMemberAccessAsync ( thisStack: any[], expr: MemberAccessExpression, evalContext: EvaluationContext, - thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback + thread: LogicalThread ): Promise { const parentObj = await evaluator( thisStack, expr.object, evalContext, thread, - onStatementCompleted ); return evalMemberAccessCore(parentObj, thisStack, expr, evalContext); } @@ -302,14 +283,12 @@ async function evalCalculatedMemberAccessAsync ( expr: CalculatedMemberAccessExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const parentObj = await evaluator( thisStack, expr.object, evalContext, thread, - onStatementCompleted ); // --- At this point we definitely keep the parent object on `thisStack`, as it will be the context object @@ -320,7 +299,6 @@ async function evalCalculatedMemberAccessAsync ( expr.member, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return evalCalculatedMemberAccessCore( @@ -338,7 +316,6 @@ async function evalSequenceAsync ( expr: SequenceExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { if (!expr.expressions || expr.expressions.length === 0) { throw new Error(`Missing expression sequence`); @@ -349,7 +326,6 @@ async function evalSequenceAsync ( e, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return exprValue; @@ -365,7 +341,6 @@ async function evalArrayLiteralAsync ( expr: ArrayLiteral, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const arrayValue: any[] = []; for (const item of expr.items) { @@ -375,7 +350,6 @@ async function evalArrayLiteralAsync ( item.operand, evalContext, thread, - onStatementCompleted ); thisStack.pop(); if (!Array.isArray(spreadArray)) { @@ -391,7 +365,6 @@ async function evalArrayLiteralAsync ( item, evalContext, thread, - onStatementCompleted ) ); thisStack.pop(); @@ -407,7 +380,6 @@ async function evalObjectLiteralAsync ( expr: ObjectLiteral, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const objectHash: any = {}; for (const prop of expr.props) { @@ -418,7 +390,6 @@ async function evalObjectLiteralAsync ( prop.operand, evalContext, thread, - onStatementCompleted ); thisStack.pop(); if (Array.isArray(spreadItems)) { @@ -450,7 +421,6 @@ async function evalObjectLiteralAsync ( prop[1], evalContext, thread, - onStatementCompleted ); thisStack.pop(); } @@ -464,14 +434,12 @@ async function evalUnaryAsync ( expr: UnaryExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const operand = await evaluator( thisStack, expr.operand, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return evalUnaryCore(expr, operand, thisStack); @@ -483,14 +451,12 @@ async function evalBinaryAsync ( expr: BinaryExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const l = await evaluator( thisStack, expr.left, evalContext, thread, - onStatementCompleted ); thisStack.pop(); const r = await evaluator( @@ -498,7 +464,6 @@ async function evalBinaryAsync ( expr.right, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return evalBinaryCore(l, r, thisStack, expr.operator); @@ -510,14 +475,12 @@ async function evalConditionalAsync ( expr: ConditionalExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { const condition = await evaluator( thisStack, expr.condition, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return await evaluator( @@ -525,7 +488,6 @@ async function evalConditionalAsync ( condition ? expr.consequent : expr.alternate, evalContext, thread, - onStatementCompleted ); } @@ -547,7 +509,6 @@ async function evalAssignmentAsync ( expr: AssignmentExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { return runAssignment(evalContext, async () => { const leftValue = expr.leftValue; @@ -556,7 +517,6 @@ async function evalAssignmentAsync ( leftValue, evalContext, thread, - onStatementCompleted ); thisStack.pop(); const newValue = await evaluator( @@ -564,7 +524,6 @@ async function evalAssignmentAsync ( expr.operand, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return evalAssignmentCore(leftValue, newValue, thisStack, expr, thread); @@ -577,7 +536,6 @@ async function evalPreOrPostAsync ( expr: PrefixOpExpression | PostfixOpExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { return runAssignment(evalContext, async () => { const operand = expr.operand; @@ -586,7 +544,6 @@ async function evalPreOrPostAsync ( operand, evalContext, thread, - onStatementCompleted ); thisStack.pop(); return evalPreOrPostCore(operand, thisStack, expr, evalContext, thread); @@ -599,7 +556,6 @@ async function evalFunctionInvocationAsync ( expr: FunctionInvocationExpression, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback ): Promise { let functionObj: any; let hostObject: any; @@ -611,7 +567,6 @@ async function evalFunctionInvocationAsync ( expr.object.object, evalContext, thread, - onStatementCompleted ); functionObj = evalMemberAccessCore( hostObject, @@ -626,7 +581,6 @@ async function evalFunctionInvocationAsync ( expr.object, evalContext, thread, - onStatementCompleted ); } thisStack.pop(); @@ -640,7 +594,6 @@ async function evalFunctionInvocationAsync ( functionObj.args, evalContext, thread, - onStatementCompleted, ...expr.arguments.map(a => ({ ...a, _EXPRESSION_: true })) ); functionObj = await createArrowFunctionAsync( @@ -654,7 +607,6 @@ async function evalFunctionInvocationAsync ( expr.object.args.map(a => (a as Identifier).name), evalContext, thread, - onStatementCompleted, ...expr.arguments.map(a => ({ ...a, _EXPRESSION_: true })) ); } else { @@ -667,7 +619,6 @@ async function evalFunctionInvocationAsync ( arg.operand, evalContext, thread, - onStatementCompleted ); if (!Array.isArray(funcArg)) { throw new Error( @@ -683,7 +634,6 @@ async function evalFunctionInvocationAsync ( arg.args, evalContext, thread, - onStatementCompleted, ...args ); }; @@ -694,7 +644,6 @@ async function evalFunctionInvocationAsync ( arg, evalContext, thread, - onStatementCompleted ); functionArgs.push(funcArg); } @@ -744,7 +693,6 @@ export async function createArrowFunctionAsync ( // --- Prepare the variables to pass const runTimeEvalContext = args[1] as EvaluationContext; const runtimeThread = args[2] as LogicalThread; - const runTimeOnStatementCompleted = args[3] as OnStatementCompletedCallback; // --- Create the thread that runs the arrow function const workingThread: LogicalThread = { @@ -788,21 +736,19 @@ export async function createArrowFunctionAsync ( } if (decl) { // --- Get the actual value to work with - let argVal = args[i + 4]; + let argVal = args[i + 3]; if (argVal?._EXPRESSION_) { argVal = await evaluator( [], argVal, runTimeEvalContext, runtimeThread, - runTimeOnStatementCompleted ); } await processDeclarationsAsync( arrowBlock, runTimeEvalContext, runtimeThread, - runTimeOnStatementCompleted, [decl], false, true, @@ -841,8 +787,7 @@ export async function createArrowFunctionAsync ( await processStatementQueueAsync( statements, runTimeEvalContext, - workingThread, - runTimeOnStatementCompleted + workingThread ); // --- Return value is in a return value slot diff --git a/src/common/ksx/process-statement-async.ts b/src/common/ksx/process-statement-async.ts index 0b1a017ae..0c004af7b 100644 --- a/src/common/ksx/process-statement-async.ts +++ b/src/common/ksx/process-statement-async.ts @@ -60,8 +60,7 @@ export type OnStatementCompletedCallback = export async function processStatementQueueAsync ( statements: Statement[], evalContext: EvaluationContext, - thread?: LogicalThread, - onStatementCompleted?: OnStatementCompletedCallback + thread?: LogicalThread ): Promise { if (!thread) { // --- Create the main thread for the queue @@ -103,8 +102,7 @@ export async function processStatementQueueAsync ( outcome = await processStatementAsync( queueItem!.statement, evalContext, - thread, - onStatementCompleted + thread ); } catch (err) { if (thread.tryBlocks && thread.tryBlocks.length > 0) { @@ -150,8 +148,6 @@ export async function processStatementQueueAsync ( } } - await onStatementCompleted?.(evalContext, queueItem!.statement); - // --- Provide diagnostics if (queue.length > diagInfo.maxQueueLength) { diagInfo.maxQueueLength = queue.length; @@ -180,8 +176,7 @@ export async function processStatementQueueAsync ( async function processStatementAsync ( statement: Statement, evalContext: EvaluationContext, - thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback + thread: LogicalThread ): Promise { // --- These items should be put in the statement queue after return let toUnshift: StatementQueueItem[] = []; @@ -259,7 +254,6 @@ async function processStatementAsync ( statement.expression, evalContext, thread, - onStatementCompleted ); if (thread.blocks && thread.blocks.length !== 0) { thread.blocks[thread.blocks.length - 1].returnValue = statementValue; @@ -271,7 +265,6 @@ async function processStatementAsync ( const arrowFuncValue = await executeArrowExpression( statement.expression, evalContext, - onStatementCompleted, thread, ...(evalContext.eventArgs ?? []) ); @@ -290,7 +283,6 @@ async function processStatementAsync ( block, evalContext, thread, - onStatementCompleted, statement.declarations ); break; @@ -306,7 +298,6 @@ async function processStatementAsync ( block, evalContext, thread, - onStatementCompleted, statement.declarations, true ); @@ -319,7 +310,6 @@ async function processStatementAsync ( statement.condition, evalContext, thread, - onStatementCompleted )); if (condition) { toUnshift = mapToItem(statement.thenBranch); @@ -341,7 +331,6 @@ async function processStatementAsync ( statement.expression, evalContext, thread, - onStatementCompleted ) : undefined; @@ -377,7 +366,6 @@ async function processStatementAsync ( statement.condition, evalContext, thread, - onStatementCompleted )); if (condition) { toUnshift = provideLoopBody( @@ -408,7 +396,6 @@ async function processStatementAsync ( statement.condition, evalContext, thread, - onStatementCompleted )); if (condition) { toUnshift = provideLoopBody( @@ -531,7 +518,6 @@ async function processStatementAsync ( statement.condition, evalContext, thread, - onStatementCompleted )) ) { // --- Stay in the loop, inject the body, the update expression, and the loop guard @@ -749,7 +735,6 @@ async function processStatementAsync ( statement.expression, evalContext, thread, - onStatementCompleted ) ); } @@ -941,7 +926,6 @@ export async function processDeclarationsAsync ( block: BlockScope, evalContext: EvaluationContext, thread: LogicalThread, - onStatementCompleted: OnStatementCompletedCallback, declarations: VarDeclaration[], addConst = false, useValue = false, @@ -957,7 +941,6 @@ export async function processDeclarationsAsync ( decl.expression, evalContext, thread, - onStatementCompleted ); } visitDeclaration(block, decl, value, addConst); diff --git a/src/common/messaging/MainToIdeMessenger.ts b/src/common/messaging/MainToIdeMessenger.ts index eb22886e7..2a60d58b8 100644 --- a/src/common/messaging/MainToIdeMessenger.ts +++ b/src/common/messaging/MainToIdeMessenger.ts @@ -12,6 +12,7 @@ class MainToIdeMessenger extends MessengerBase { */ constructor (public readonly window: BrowserWindow) { super(); + this._requestSeqNo = 1000; ipcMain?.on( this.responseChannel, (_ev: IpcMainEvent, response: ResponseMessage) => diff --git a/src/common/messaging/MessengerBase.ts b/src/common/messaging/MessengerBase.ts index 2e65015ce..9730689ae 100644 --- a/src/common/messaging/MessengerBase.ts +++ b/src/common/messaging/MessengerBase.ts @@ -11,7 +11,7 @@ import { Channel, RequestMessage, ResponseMessage } from "./messages-core"; */ export abstract class MessengerBase { // Sequential number of the next request - private _requestSeqNo = 0; + protected _requestSeqNo = 1; /** * Stores resolvers to correlate incoming messages with outcoming ones @@ -44,7 +44,7 @@ export abstract class MessengerBase { // --- Create a promise and store the resolver function with the message ID. const promise = new Promise((resolve, reject) => { this._messageResolvers.set( - message.correlationId ?? 0, + message.correlationId, resolve as ( value: ResponseMessage | PromiseLike ) => void diff --git a/src/common/messaging/any-to-ide.ts b/src/common/messaging/any-to-ide.ts index 9822f8836..811607c56 100644 --- a/src/common/messaging/any-to-ide.ts +++ b/src/common/messaging/any-to-ide.ts @@ -54,6 +54,7 @@ export interface IdeShowDialogRequest extends MessageBase { export interface IdeExecuteCommandRequest extends MessageBase { type: "IdeExecuteCommand"; commandText: string; + scriptId?: number; } // --- Ask the IDE to save all files before quitting diff --git a/src/main/app-menu.ts b/src/main/app-menu.ts index c80b3c3bb..0fd8e0394 100644 --- a/src/main/app-menu.ts +++ b/src/main/app-menu.ts @@ -243,6 +243,21 @@ export function setupMenu ( await saveKliveProject(); } }, + ...(!kliveProject + ? [] + : ([ + { type: "separator" }, + { + id: EXCLUDED_PROJECT_ITEMS, + label: "\nManage Excluded Items", + enabled: true, + click: () => { + mainStore.dispatch( + displayDialogAction(EXCLUDED_PROJECT_ITEMS_DIALOG) + ); + } + } + ] as MenuItemConstructorOptions[])), ...(__DARWIN__ ? [] : ([ @@ -757,86 +772,6 @@ export function setupMenu ( // Project Menu if (kliveProject) { - // --- Machine-specific view menu items - let specificProjectMenus: MenuItemConstructorOptions[] = []; - if (machineMenus && machineMenus.projectItems) { - specificProjectMenus = machineMenus.projectItems( - { - emuWindow, - ideWindow - }, - currentMachine, - currentModel - ); - } - - template.push({ - label: "Project", - submenu: [ - { - id: COMPILE_CODE, - label: "Compile code", - enabled: !!buildRoot, - click: async () => { - await executeIdeCommand(ideWindow, "outp build", undefined, true); - await executeIdeCommand(ideWindow, "compile", "Compile Code"); - } - }, - { - id: INJECT_CODE, - label: "Inject code", - enabled: !!buildRoot && execState === MachineControllerState.Paused, - click: async () => { - await executeIdeCommand(ideWindow, "outp build", undefined, true); - await executeIdeCommand(ideWindow, "inject", "Inject Code", true); - } - }, - { type: "separator" }, - { - id: RUN_CODE, - label: "Run", - enabled: !!buildRoot, - click: async () => { - mainStore.dispatch(setRestartTarget("project")); - await executeIdeCommand(ideWindow, "outp build", undefined, true); - await executeIdeCommand(ideWindow, "run", "Run Code", true); - } - }, - { - id: DEBUG_CODE, - label: "Debug", - enabled: !!buildRoot, - click: async () => { - mainStore.dispatch(setRestartTarget("project")); - await executeIdeCommand(ideWindow, "outp build", undefined, true); - await executeIdeCommand(ideWindow, "debug", "Debug Code", true); - } - }, - { type: "separator" }, - { - id: EXPORT_CODE, - label: "Export code...", - enabled: !!buildRoot, - click: () => { - mainStore.dispatch(displayDialogAction(EXPORT_CODE_DIALOG)); - } - }, - { type: "separator" }, - { - id: EXCLUDED_PROJECT_ITEMS, - label: "\nManage Excluded Items", - enabled: true, - click: () => { - mainStore.dispatch( - displayDialogAction(EXCLUDED_PROJECT_ITEMS_DIALOG) - ); - } - }, - { type: "separator" }, - ...specificProjectMenus - ] - }); - if (hasBuildFile) { let buildTasks: MenuItemConstructorOptions[] = []; for (const task of collectedBuildTasks) { @@ -1134,7 +1069,7 @@ function visitMenu ( } } -async function executeIdeCommand ( +export async function executeIdeCommand ( window: BrowserWindow, commandText: string, title?: string, diff --git a/src/main/build.ts b/src/main/build.ts index 26aae3479..588613d11 100644 --- a/src/main/build.ts +++ b/src/main/build.ts @@ -119,7 +119,7 @@ const predefinedTasks: Record = { ideCommand: "debug" }, exportCode: { - displayName: "Export artifacts", + displayName: "Export code...", separatorBefore: true, ideCommand: "export" } diff --git a/src/main/ksx-runner/MainScriptManager.ts b/src/main/ksx-runner/MainScriptManager.ts index 4ab1a7b87..7159e6ab3 100644 --- a/src/main/ksx-runner/MainScriptManager.ts +++ b/src/main/ksx-runner/MainScriptManager.ts @@ -27,6 +27,7 @@ import { } from "../../common/ksx/script-runner"; import { Z88DK } from "../../script-packages/z88dk/Z88DK"; import { createProjectStructure } from "./ProjectStructure"; +import { executeIdeCommand } from "./ide-commands"; const MAX_SCRIPT_HISTORY = 128; @@ -89,7 +90,7 @@ class MainScriptManager implements IScriptManager { ); if (script) { // --- The script is already running, nothing to do - this.outputFn?.(`Script ${scriptFileName} is already running.`, { + await this.outputFn?.(`Script ${scriptFileName} is already running.`, { color: "yellow" }); return { id: -script.id }; @@ -102,7 +103,7 @@ class MainScriptManager implements IScriptManager { this.id++; // --- Now, start the script - this.outputFn?.(`Starting script ${scriptFileName}...`); + await this.outputFn?.(`Starting script ${scriptFileName}...`); const cancellationToken = new CancellationToken(); const evalContext = createEvalContext({ scriptId: this.id, @@ -146,9 +147,9 @@ class MainScriptManager implements IScriptManager { // --- Update the script status newScript.execTask = execTask; mainStore.dispatch(setScriptsStatusAction(this.getScriptsStatus())); - this.outputFn?.(`Script started`, { color: "green" }); - // --- Await the script execution + // --- We intentionally do not await the script execution here + this.outputFn?.(`Script started`, { color: "green" }); concludeScript( mainStore, execTask, @@ -158,6 +159,8 @@ class MainScriptManager implements IScriptManager { this.outputFn, () => delete newScript.execTask ); + + // --- Done. return { id: this.id }; } @@ -174,7 +177,7 @@ class MainScriptManager implements IScriptManager { this.id++; // --- Now, start the script - this.outputFn?.(`Starting script ${scriptFunction}...`); + await this.outputFn?.(`Starting script ${scriptFunction}...`); const cancellationToken = new CancellationToken(); const evalContext = createEvalContext({ scriptId: this.id, @@ -212,9 +215,9 @@ class MainScriptManager implements IScriptManager { // --- Update the script status newScript.execTask = execTask; mainStore.dispatch(setScriptsStatusAction(this.getScriptsStatus())); - this.outputFn?.(`Script started`, { color: "green" }); - // --- Await the script execution + // --- We intentionally do not await the script execution here + this.outputFn?.(`Script started`, { color: "green" }); concludeScript( mainStore, execTask, @@ -224,6 +227,8 @@ class MainScriptManager implements IScriptManager { this.outputFn, () => delete newScript.execTask ); + + // --- Done. return { id: this.id }; } @@ -245,7 +250,7 @@ class MainScriptManager implements IScriptManager { } // --- Stop the script - this.outputFn?.(`Stopping script ${script.scriptFileName}...`); + await this.outputFn?.(`Stopping script ${script.scriptFileName}...`); script.evalContext?.cancellationToken?.cancel(); await script.execTask; } @@ -406,8 +411,8 @@ class MainScriptManager implements IScriptManager { // --- The script has errors, display them Object.keys(module).forEach(moduleName => { const errors = module[moduleName]; - errors.forEach(error => { - this.outputFn?.( + errors.forEach(async (error) => { + await this.outputFn?.( `${error.code}: ${error.text} (${moduleName}:${error.line}:${error.column})`, { color: "bright-red" @@ -426,7 +431,8 @@ class MainScriptManager implements IScriptManager { private async prepareAppContext (): Promise> { return { Output: createScriptConsole(mainStore, getMainToIdeMessenger(), this.id), - "#project": await createProjectStructure() + "#project": await createProjectStructure(), + "#command": (commandText: string) => executeIdeCommand(this.id, commandText), }; } } diff --git a/src/main/ksx-runner/ide-commands.ts b/src/main/ksx-runner/ide-commands.ts new file mode 100644 index 000000000..9f2e3f5a5 --- /dev/null +++ b/src/main/ksx-runner/ide-commands.ts @@ -0,0 +1,14 @@ +import { sendFromMainToIde } from "../../common/messaging/MainToIdeMessenger"; +import { IdeExecuteCommandResponse } from "../../common/messaging/any-to-ide"; + +export async function executeIdeCommand ( + scriptId: number, + commandText: string +): Promise { + const response = await sendFromMainToIde({ + type: "IdeExecuteCommand", + commandText, + scriptId + }); + return response; +} diff --git a/src/public/project-templates/sp48/default/build.ksx b/src/public/project-templates/sp48/default/build.ksx new file mode 100644 index 000000000..afc3831e5 --- /dev/null +++ b/src/public/project-templates/sp48/default/build.ksx @@ -0,0 +1,19 @@ +export function buildCode() { + // TODO: Implement build +} + +export function injectCode() { + // TODO: Implement inject +} + +export function runCode() { + // TODO: Implement run +} + +export function debugCode() { + // TODO: Implement debug +} + +export function exportCode() { + // TODO: Implement export +} diff --git a/src/renderer/abstractions/IIdeCommandService.ts b/src/renderer/abstractions/IIdeCommandService.ts index 2156a699b..cb475da1e 100644 --- a/src/renderer/abstractions/IIdeCommandService.ts +++ b/src/renderer/abstractions/IIdeCommandService.ts @@ -59,11 +59,13 @@ export interface IIdeCommandService { * @param command Command to execute * @param buffer Optional output buffer * @param useHistory Add the command to the history + * @param interactiveContex Indicates that the command is executed in interactive context */ executeInteractiveCommand( command: string, buffer?: IOutputBuffer, - useHistory?: boolean + useHistory?: boolean, + interactiveContex?: boolean ): Promise; /** diff --git a/src/renderer/abstractions/IdeCommandInfo.ts b/src/renderer/abstractions/IdeCommandInfo.ts index 67d69e43f..fa6d8847b 100644 --- a/src/renderer/abstractions/IdeCommandInfo.ts +++ b/src/renderer/abstractions/IdeCommandInfo.ts @@ -26,6 +26,11 @@ export type IdeCommandInfo = { */ readonly usage: string | string[]; + /** + * Indicates whether the command can be used interactively + */ + readonly noInteractiveUsage?: boolean; + /** * Executes the command within the specified context */ diff --git a/src/renderer/appIde/DocumentArea/DocumentsHeader.tsx b/src/renderer/appIde/DocumentArea/DocumentsHeader.tsx index a1e199db6..a3636c867 100644 --- a/src/renderer/appIde/DocumentArea/DocumentsHeader.tsx +++ b/src/renderer/appIde/DocumentArea/DocumentsHeader.tsx @@ -4,10 +4,7 @@ import { TabButtonSeparator, TabButtonSpace } from "@controls/TabButton"; -import { - useDispatch, - useSelector, -} from "@renderer/core/RendererProvider"; +import { useDispatch, useSelector } from "@renderer/core/RendererProvider"; import { useEffect, useRef, useState } from "react"; import { useAppServices } from "../services/AppServicesProvider"; import { CloseMode, DocumentTab } from "./DocumentTab"; @@ -18,10 +15,14 @@ import { useDocumentHubServiceVersion } from "../services/DocumentServiceProvider"; import { ProjectDocumentState } from "@renderer/abstractions/ProjectDocumentState"; -import { incProjectViewStateVersionAction, setRestartTarget } from "@common/state/actions"; +import { + incProjectViewStateVersionAction, + setRestartTarget +} from "@common/state/actions"; import { PANE_ID_BUILD } from "@common/integration/constants"; import { FileTypeEditor } from "@renderer/abstractions/FileTypePattern"; import { getFileTypeEntry } from "../project/project-node"; +import { delay } from "@renderer/utils/timing"; /** * This component represents the header of a document hub @@ -226,7 +227,10 @@ export const DocumentsHeader = () => {
- {editorInfo && editorInfo.documentTabRenderer?.(openDocs?.[activeDocIndex]?.node?.fullPath)} + {editorInfo && + editorInfo.documentTabRenderer?.( + openDocs?.[activeDocIndex]?.node?.fullPath + )} {selectedIsBuildRoot && } { title='Compile code' disabled={compiling} clicked={async () => { - const buildPane = outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); - await ideCommandsService.executeCommand("compile", buildPane); - await ideCommandsService.executeCommand(`outp ${PANE_ID_BUILD}`); + const buildPane = + outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); + const commandResult = await ideCommandsService.executeCommand( + "run-build-function buildCode", + buildPane + ); + console.log("commandResult", commandResult); + await delay(100); + await ideCommandsService.executeCommand( + `script-output ${commandResult.value}`, buildPane + ); }} /> @@ -277,8 +289,12 @@ const BuildRootCommandBar = () => { title={"Inject code into\nthe virtual machine"} disabled={compiling} clicked={async () => { - const buildPane = outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); - await ideCommandsService.executeCommand("inject", buildPane); + const buildPane = + outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); + await ideCommandsService.executeCommand( + "run-build-function injectCode", + buildPane + ); await ideCommandsService.executeCommand(`outp ${PANE_ID_BUILD}`); }} /> @@ -289,8 +305,12 @@ const BuildRootCommandBar = () => { disabled={compiling} clicked={async () => { storeDispatch(setRestartTarget("project")); - const buildPane = outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); - await ideCommandsService.executeCommand("run", buildPane); + const buildPane = + outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); + await ideCommandsService.executeCommand( + "run-build-function runCode", + buildPane + ); await ideCommandsService.executeCommand(`outp ${PANE_ID_BUILD}`); }} /> @@ -301,8 +321,12 @@ const BuildRootCommandBar = () => { disabled={compiling} clicked={async () => { storeDispatch(setRestartTarget("project")); - const buildPane = outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); - await ideCommandsService.executeCommand("debug", buildPane); + const buildPane = + outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); + await ideCommandsService.executeCommand( + "run-build-function debugCode", + buildPane + ); await ideCommandsService.executeCommand(`outp ${PANE_ID_BUILD}`); }} /> diff --git a/src/renderer/appIde/IdeApp.tsx b/src/renderer/appIde/IdeApp.tsx index 1ba50f250..c1efda3e0 100644 --- a/src/renderer/appIde/IdeApp.tsx +++ b/src/renderer/appIde/IdeApp.tsx @@ -12,7 +12,7 @@ import { EXPORT_CODE_DIALOG, NEW_PROJECT_DIALOG, EXCLUDED_PROJECT_ITEMS_DIALOG, - FIRST_STARTUP_DIALOG_IDE + FIRST_STARTUP_DIALOG_IDE, } from "@messaging/dialog-ids"; import { RequestMessage, @@ -110,6 +110,8 @@ import { setCachedStore } from "../CachedServices"; import { ResetZ88DkCommand } from "./commands/Z88DkCommands"; +import { KliveCompileCommand, KliveDebugCodeCommand, KliveInjectCodeCommand, KliveRunCodeCommand } from "./commands/KliveCompilerCommands"; +import { DisplayDialogCommand } from "./commands/DialogCommands"; const IdeApp = () => { // --- Used services @@ -320,6 +322,11 @@ function registerCommands (cmdSrv: IIdeCommandService): void { cmdSrv.registerCommand(new NewProjectCommand()); cmdSrv.registerCommand(new CloseFolderCommand()); + cmdSrv.registerCommand(new KliveCompileCommand()); + cmdSrv.registerCommand(new KliveInjectCodeCommand()); + cmdSrv.registerCommand(new KliveRunCodeCommand()); + cmdSrv.registerCommand(new KliveDebugCodeCommand()); + cmdSrv.registerCommand(new CompileCommand()); cmdSrv.registerCommand(new InjectCodeCommand()); cmdSrv.registerCommand(new RunCodeCommand()); @@ -341,4 +348,6 @@ function registerCommands (cmdSrv: IIdeCommandService): void { cmdSrv.registerCommand(new DisplayScriptOutputCommand()); cmdSrv.registerCommand(new ResetZ88DkCommand()); + + cmdSrv.registerCommand(new DisplayDialogCommand()); } diff --git a/src/renderer/appIde/MainToIdeProcessor.ts b/src/renderer/appIde/MainToIdeProcessor.ts index b8f26e20a..bf3a90466 100644 --- a/src/renderer/appIde/MainToIdeProcessor.ts +++ b/src/renderer/appIde/MainToIdeProcessor.ts @@ -9,7 +9,7 @@ import { BASIC_PANEL_ID, BASIC_EDITOR, MEMORY_PANEL_ID, - DISASSEMBLY_PANEL_ID, + DISASSEMBLY_PANEL_ID } from "@state/common-ids"; import { Store } from "@state/redux-light"; import { dimMenuAction } from "@common/state/actions"; @@ -19,7 +19,10 @@ import { IdeScriptOutputRequest } from "@common/messaging/any-to-ide"; import { getCachedAppServices } from "../CachedServices"; import { ITreeNode, ITreeView } from "@renderer/core/tree-node"; import { ProjectNode } from "./project/project-node"; -import { ProjectStructure, ProjectTreeNode } from "@main/ksx-runner/ProjectStructure"; +import { + ProjectStructure, + ProjectTreeNode +} from "@main/ksx-runner/ProjectStructure"; /** * Process the messages coming from the emulator to the main process @@ -29,7 +32,12 @@ import { ProjectStructure, ProjectTreeNode } from "@main/ksx-runner/ProjectStruc export async function processMainToIdeMessages ( message: RequestMessage, store: Store, - { outputPaneService, ideCommandsService, projectService }: AppServices + { + outputPaneService, + ideCommandsService, + projectService, + scriptService + }: AppServices ): Promise { const documentHubService = projectService.getActiveDocumentHubService(); switch (message.type) { @@ -93,7 +101,9 @@ export async function processMainToIdeMessages ( } case "IdeExecuteCommand": { - const pane = outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); + const pane = /*message.scriptId + ? scriptService.getScriptOutputBuffer(message.scriptId) + : */outputPaneService.getOutputPaneBuffer(PANE_ID_BUILD); const response = await ideCommandsService.executeCommand( message.commandText, pane @@ -119,7 +129,10 @@ export async function processMainToIdeMessages ( case "IdeGetProjectStructure": { return { type: "IdeGetProjectStructureResponse", - projectStructure: convertToProjectStructure(store, projectService.getProjectTree()) + projectStructure: convertToProjectStructure( + store, + projectService.getProjectTree() + ) }; } } @@ -186,7 +199,10 @@ function executeScriptOutput (message: IdeScriptOutputRequest): void { } } -function convertToProjectStructure(store: Store, tree: ITreeView): ProjectStructure { +function convertToProjectStructure ( + store: Store, + tree: ITreeView +): ProjectStructure { const project = store.getState().project; const nodes = collectNodes(tree.rootNode.children); @@ -194,10 +210,10 @@ function convertToProjectStructure(store: Store, tree: ITreeView[]): ProjectTreeNode[] { + function collectNodes (children: ITreeNode[]): ProjectTreeNode[] { if (!children) return []; const result: ProjectTreeNode[] = []; @@ -213,9 +229,8 @@ function convertToProjectStructure(store: Store, tree: ITreeView 0 ? nodeChildren : [] - }) + }); }); - console.log(result); return result; } } diff --git a/src/renderer/appIde/commands/CompilerCommand.ts b/src/renderer/appIde/commands/CompilerCommand.ts index 48022b767..4b805e605 100644 --- a/src/renderer/appIde/commands/CompilerCommand.ts +++ b/src/renderer/appIde/commands/CompilerCommand.ts @@ -58,6 +58,7 @@ export class CompileCommand extends CommandWithNoArgBase { readonly description = "Compiles the current project"; readonly usage = "compile"; readonly aliases = ["co"]; + readonly noInteractiveUsage = true; async doExecute (context: IdeCommandContext): Promise { const compileResult = await compileCode(context); diff --git a/src/renderer/appIde/commands/DialogCommands.ts b/src/renderer/appIde/commands/DialogCommands.ts new file mode 100644 index 000000000..1c101f508 --- /dev/null +++ b/src/renderer/appIde/commands/DialogCommands.ts @@ -0,0 +1,35 @@ +import { + NEW_PROJECT_DIALOG, + EXPORT_CODE_DIALOG, + CREATE_DISK_DIALOG +} from "@common/messaging/dialog-ids"; +import { IdeCommandContext } from "../../abstractions/IdeCommandContext"; +import { IdeCommandResult } from "../../abstractions/IdeCommandResult"; +import { + writeSuccessMessage, + commandSuccess, + commandError +} from "../services/ide-commands"; +import { CommandWithSingleStringBase } from "./CommandWithSimpleStringBase"; +import { displayDialogAction } from "@common/state/actions"; + +export class DisplayDialogCommand extends CommandWithSingleStringBase { + readonly id = "display-dialog"; + readonly description = "Displays the spceified dialog"; + readonly usage = "display-dialog "; + readonly aliases = []; + + async doExecute (context: IdeCommandContext): Promise { + if (this.arg in publicDialogIds) { + context.store.dispatch(displayDialogAction(publicDialogIds[this.arg])); + return commandSuccess; + } + return commandError(`Unknown dialog ID: ${this.arg}`); + } +} + +export const publicDialogIds: Record = { + newProject: NEW_PROJECT_DIALOG, + export: EXPORT_CODE_DIALOG, + createDisk: CREATE_DISK_DIALOG +}; diff --git a/src/renderer/appIde/commands/KliveCompilerCommands.ts b/src/renderer/appIde/commands/KliveCompilerCommands.ts new file mode 100644 index 000000000..962ded67d --- /dev/null +++ b/src/renderer/appIde/commands/KliveCompilerCommands.ts @@ -0,0 +1,1256 @@ +import { + MainCompileResponse, + MainSaveFileResponse + } from "@messaging/any-to-main"; + import { IdeCommandContext } from "../../abstractions/IdeCommandContext"; + import { IdeCommandResult } from "../../abstractions/IdeCommandResult"; + import { + getFileTypeEntry, + getNodeName as getNodeFileName + } from "../project/project-node"; + import { + IdeCommandBase, + commandError, + commandSuccess, + commandSuccessWith, + getNumericTokenValue, + toHexa2, + toHexa4, + validationError + } from "../services/ide-commands"; + import { CommandWithNoArgBase } from "./CommandWithNoArgsBase"; + import { + InjectableOutput, + KliveCompilerOutput, + isInjectableCompilerOutput + } from "../../../main/compiler-integration/compiler-registry"; + import { MachineControllerState } from "@abstractions/MachineControllerState"; + import { CodeToInject } from "@abstractions/CodeToInject"; + import { + BinarySegment, + CompilerOutput, + SpectrumModelType + } from "@abstractions/IZ80CompilerService"; + import { ValidationMessage } from "../../abstractions/ValidationMessage"; + import { BinaryReader } from "@utils/BinaryReader"; + import { TapReader } from "@emu/machines/tape/TapReader"; + import { TzxReader } from "@emu/machines/tape/TzxReader"; + import { TapeDataBlock } from "@common/structs/TapeDataBlock"; + import { SpectrumTapeHeader } from "@emu/machines/tape/SpectrumTapeHeader"; + import { BinaryWriter } from "@utils/BinaryWriter"; + import { TzxHeader } from "@emu/machines/tape/TzxHeader"; + import { TzxStandardSpeedBlock } from "@emu/machines/tape/TzxStandardSpeedBlock"; + import { reportMessagingError } from "@renderer/reportError"; + import { + endCompileAction, + incBreakpointsVersionAction, + incInjectionVersionAction, + startCompileAction + } from "@common/state/actions"; + import { refreshSourceCodeBreakpoints } from "@common/utils/breakpoints"; + + const EXPORT_FILE_FOLDER = "KliveExports"; + + type CodeInjectionType = "inject" | "run" | "debug"; + + export class KliveCompileCommand extends CommandWithNoArgBase { + readonly id = "klive.compile"; + readonly description = "Compiles the current project with the Klive Z80 Compiler"; + readonly usage = "klive.compile"; + readonly aliases = ["kl.co"]; + readonly noInteractiveUsage = true; + + async doExecute (context: IdeCommandContext): Promise { + const compileResult = await compileCode(context); + return compileResult.message + ? commandError(compileResult.message) + : commandSuccessWith(`Project file successfully compiled.`); + } + } + + export class KliveInjectCodeCommand extends CommandWithNoArgBase { + readonly id = "klive.inject"; + readonly description = "Injects the current project code into the machine (using the Klive Z80 Compiler)"; + readonly usage = "klive.inject"; + readonly aliases = ["kl.inj"]; + readonly noInteractiveUsage = true; + + async doExecute (context: IdeCommandContext): Promise { + return await injectCode(context, "inject"); + } + } + + export class KliveRunCodeCommand extends CommandWithNoArgBase { + readonly id = "klive.run"; + readonly description = + "Runs the current project's code in the virtual machine (using the Klive Z80 Compiler)"; + readonly usage = "klive.run"; + readonly aliases = ["kl.r"]; + readonly noInteractiveUsage = true; + + async doExecute (context: IdeCommandContext): Promise { + console.log("Before injectCode"); + const result = await injectCode(context, "run"); + console.log("After injectCode"); + return result; + } + } + + export class KliveDebugCodeCommand extends CommandWithNoArgBase { + readonly id = "klive.debug"; + readonly description = + "Runs the current project's code in the virtual machine with debugging (using the Klive Z80 Compiler)"; + readonly usage = "klive.debug"; + readonly aliases = ["kl.rd"]; + readonly noInteractiveUsage = true; + + async doExecute (context: IdeCommandContext): Promise { + return await injectCode(context, "debug"); + } + } + + export class ExportCodeCommand extends IdeCommandBase { + readonly id = "expc"; + readonly description = "Export the code of the current project"; + readonly usage = + "expc filename [-n name] [-f format] [-as] [-p] [-c] [-b border] [-sb] [-addr address] [-scr screenfile]"; + + private filename?: string; + private name?: string; + private format = "tzx"; + private autoStart = false; + private pause = false; + private applyClear = false; + private border?: number; + private singleBlock = false; + private address?: number; + private screenFile?: string; + + prepareCommand (): void { + this.format = "tzx"; + this.autoStart = false; + this.pause = false; + this.applyClear = false; + this.singleBlock = false; + this.filename = this.name = undefined; + this.border = this.address = this.screenFile = undefined; + } + + /** + * Validates the input arguments + * @param _args Arguments to validate + * @returns A list of issues + */ + async validateArgs ( + context: IdeCommandContext + ): Promise { + const args = context.argTokens; + if (args.length < 1) { + return validationError("This command expects at least 2 arguments"); + } + this.filename = args[0].text; + this.name = getNodeFileName(this.filename); + // --- Obtain additional arguments + let argPos = 1; + while (argPos < args.length) { + switch (args[argPos].text) { + case "-n": + argPos++; + if (args[argPos] === undefined) { + return validationError("Missing value for '-n'"); + } + this.name = args[argPos].text; + break; + case "-f": + argPos++; + if (args[argPos] === undefined) { + return validationError("Missing value for '-f'"); + } + this.format = args[argPos].text; + if ( + this.format !== "tzx" && + this.format !== "tap" && + this.format !== "hex" + ) { + return validationError( + "Format should be one 'tzx', 'tap', or 'hex'" + ); + } + break; + case "-p": + this.pause = true; + break; + case "-as": + this.autoStart = true; + break; + case "-c": + this.applyClear = true; + break; + case "-b": { + argPos++; + if (args[argPos] === undefined) { + return validationError("Missing value for '-b'"); + } + const { value } = getNumericTokenValue(args[argPos]); + if (value === null) { + return validationError("Numeric value expected for '-b'"); + } + this.border = value & 0x07; + break; + } + case "-sb": + this.singleBlock = true; + break; + case "-addr": { + argPos++; + if (args[argPos] === undefined) { + return validationError("Missing value for '-addr'"); + } + const { value } = getNumericTokenValue(args[argPos]); + if (value === null) { + return validationError("Numeric value expected for '-addr'"); + } + this.address = value & 0xffff; + break; + } + case "-scr": { + argPos++; + if (args[argPos] === undefined) { + return validationError("Missing value for '-scr'"); + } + this.screenFile = args[argPos].text; + break; + } + default: + return validationError(`Unexpected argument: '${args[argPos].text}'`); + } + argPos++; + } + return []; + } + + async doExecute (context: IdeCommandContext): Promise { + // --- Compile before export + const { message, result } = await compileCode(context); + const errorNo = result?.errors?.length ?? 0; + if (message) { + if (!result) { + return commandError(message); + } + if (errorNo > 0) { + const message = "Code compilation failed, no program to export."; + const response = await context.messenger.sendMessage({ + type: "MainDisplayMessageBox", + messageType: "error", + title: "Exporting code", + message + }); + if (response.type === "ErrorResponse") { + reportMessagingError( + `MainDisplayMessageBox call failed: ${response.message}` + ); + } + return commandError(message); + } + } + + if (!isInjectableCompilerOutput(result)) { + return commandError("Compiled code is not injectable."); + } + + return this.exportCompiledCode(context, result); + } + + async exportCompiledCode ( + context: IdeCommandContext, + output: KliveCompilerOutput + ): Promise { + const exporter = this; + if (this.format == "hex") { + return await saveIntelHexFile(context, this.filename, output); + } + + // --- Check for screen file error + let scrContent: Uint8Array; + if (this.screenFile) { + // --- Check the validity of the screen file + const scrContent = (await context.service.projectService.readFileContent( + this.screenFile, + true + )) as Uint8Array; + context.service.projectService.forgetFile(this.screenFile); + if (!isScreenFile(scrContent)) { + return commandError( + `File '${this.screenFile}' is not a valid screen file` + ); + } + } + + // --- Step #6: Create code segments + const codeBlocks = createTapeBlocks(output as InjectableOutput); + let screenBlocks: Uint8Array[] | undefined; + if (this.screenFile) { + const blocks = readTapeData(scrContent); + if (blocks) { + screenBlocks = [blocks[0].data, blocks[1].data]; + } + } + + // --- Step #7: Create Auto Start header block, if required + const blocksToSave: Uint8Array[] = []; + if (!this.address) { + this.address = + (output as CompilerOutput).exportEntryAddress ?? + (output as CompilerOutput).entryAddress ?? + (output as CompilerOutput).segments[0].startAddress; + } + + if (this.autoStart) { + const autoStartBlocks = createAutoStartBlock(output as CompilerOutput); + blocksToSave.push(...autoStartBlocks); + } + + // --- Step #8: Save all the blocks + if (screenBlocks) { + blocksToSave.push(...screenBlocks); + } + + blocksToSave.push(...codeBlocks); + return await saveDataBlocks(blocksToSave); + + // --- Reads tape data from the specified contents + function readTapeData (contents: Uint8Array): TapeDataBlock[] | null { + let dataBlocks: TapeDataBlock[] = []; + try { + const reader = new BinaryReader(contents); + const tzxReader = new TzxReader(reader); + let result = tzxReader.readContent(); + if (result) { + reader.seek(0); + const tapReader = new TapReader(reader); + result = tapReader.readContent(); + if (result) { + // --- Not a TZX or a TAP file + return null; + } else { + dataBlocks = tapReader.dataBlocks; + } + } else { + dataBlocks = tzxReader.dataBlocks + .map(b => b.getDataBlock()) + .filter(b => b); + } + return dataBlocks; + } catch { + return null; + } + } + + function isScreenFile (contents: Uint8Array): boolean { + // --- Try to read a .TZX file + const dataBlocks = readTapeData(contents); + if (!dataBlocks) { + return false; + } + + // --- Block lenghts should be 19 and 6914 + var header = dataBlocks[0].data; + if (header.length !== 19 || dataBlocks[1].data.length != 6914) { + // --- Problem with block length + return false; + } + + // --- Test header bytes + return ( + header[0] === 0x00 && + header[1] == 0x03 && // --- Code header + header[12] == 0x00 && + header[13] == 0x1b && // --- Length: 0x1B00 + header[14] == 0x00 && + header[15] == 0x40 && // --- Address: 0x4000 + header[16] == 0x00 && + header[17] == 0x80 + ); // --- Param2: 0x8000 + } + + // --- Create tap blocks + function createTapeBlocks (output: InjectableOutput): Uint8Array[] { + var result: Uint8Array[] = []; + if ( + output.segments + .map(s => s.emittedCode.length) + .reduce((a, b) => a + b, 0) === 0 + ) { + // --- No code to return + return null; + } + + if (exporter.singleBlock) { + // --- Merge all blocks together + const startAddr = Math.min(...output.segments.map(s => s.startAddress)); + const endAddr = Math.max( + ...output.segments + .filter(s => s.bank == undefined) + .map(s => s.startAddress + s.emittedCode.length - 1) + ); + + // --- Normal code segments + const mergedSegment = new Uint8Array(endAddr - startAddr + 3); + for (const segment of output.segments.filter( + s => s.bank == undefined + )) { + for (let i = 0; i < segment.emittedCode.length; i++) { + mergedSegment[segment.startAddress - startAddr + 1 + i] = + segment.emittedCode[i]; + } + } + + // --- The first byte of the merged segment is 0xFF (Data block) + mergedSegment[0] = 0xff; + setTapeCheckSum(mergedSegment); + + // --- Create the single header + var singleHeader = new SpectrumTapeHeader(); + singleHeader.type = 3; // --- Code block + singleHeader.name = exporter.name; + singleHeader.dataLength = mergedSegment.length - 2; + singleHeader.parameter1 = startAddr; + singleHeader.parameter2 = 0x8000; + // --- Create the two tape blocks (header + data) + result.push(singleHeader.headerBytes); + result.push(mergedSegment); + } else { + // --- Create separate block for each segment + let segmentIdx = 0; + + // --- Normal code segments + for (const segment of output.segments.filter(s => s.bank == null)) { + segmentIdx++; + const startAddr = segment.startAddress; + const endAddr = segment.startAddress + segment.emittedCode.length - 1; + + const codeSegment = new Uint8Array(endAddr - startAddr + 3); + for (let i = 0; i < segment.emittedCode.length; i++) { + codeSegment[i + 1] = segment.emittedCode[i]; + } + + // --- The first byte of the code segment is 0xFF (Data block) + codeSegment[0] = 0xff; + setTapeCheckSum(codeSegment); + + // --- Create the single header + const header = new SpectrumTapeHeader(); + (header.type = 3), // --- Code block + (header.name = `${segmentIdx}_${exporter.name}`), + (header.dataLength = (codeSegment.length - 2) & 0xffff), + (header.parameter1 = startAddr), + (header.parameter2 = 0x8000); + + // --- Create the two tape blocks (header + data) + result.push(header.headerBytes); + result.push(codeSegment); + } + } + + // --- Create blocks for the banks + const segments = output.segments.filter(s => s.bank != null); + segments.sort((a, b) => a.bank - b.bank); + for (const bankSegment of segments) { + const startAddr = (0xc000 + bankSegment.bankOffset) & 0xffff; + const endAddr = startAddr + bankSegment.emittedCode.length - 1; + + const codeSegment = new Uint8Array(endAddr - startAddr + 3); + for (let i = 0; i < bankSegment.emittedCode.length; i++) { + codeSegment[i + 1] = bankSegment.emittedCode[i]; + } + + // --- The first byte of the code segment is 0xFF (Data block) + codeSegment[0] = 0xff; + setTapeCheckSum(codeSegment); + + // --- Create the single header + const header = new SpectrumTapeHeader(); + header.type = 3; // --- Code block + header.name = `bank${bankSegment.bank}.code`; + header.dataLength = (codeSegment.length - 2) & 0xffff; + header.parameter1 = startAddr; + header.parameter2 = 0x8000; + + // --- Create the two tape blocks (header + data) + result.push(header.headerBytes); + result.push(codeSegment); + } + + // --- Done + return result; + } + + // --- Sets the checksum byte for a tape block + function setTapeCheckSum (bytes: Uint8Array): void { + let chk = 0x00; + for (let i = 0; i < bytes.length - 1; i++) { + chk ^= bytes[i]; + } + + bytes[bytes.length - 1] = chk & 0xff; + } + + // --- Saves an Intel HEX file + async function saveIntelHexFile ( + context: IdeCommandContext, + filename: string, + output: KliveCompilerOutput + ): Promise { + const rowLen = 0x10; + let hexOut = ""; + + for (const segment of (output as InjectableOutput).segments) { + var offset = 0; + while (offset + rowLen < segment.emittedCode.length) { + // --- Write an entire data row + writeDataRecord(segment, offset, rowLen); + offset += rowLen; + } + + // --- Write the left of the data row + var leftBytes = segment.emittedCode.length - offset; + writeDataRecord(segment, offset, leftBytes); + } + + // --- Write End-Of-File record + hexOut += ":00000001FF"; + + // --- Save the data to a file + if (filename) { + const response = await context.messenger.sendMessage({ + type: "MainSaveTextFile", + path: filename, + data: hexOut, + resolveIn: `home:${EXPORT_FILE_FOLDER}` + }); + if (response.type === "ErrorResponse") { + return commandError(response.message); + } + return commandSuccessWith( + `Code successfully exported to '${ + (response as MainSaveFileResponse).path + }'` + ); + } + return commandError("Filename not specified"); + + // --- Write out a single data record + function writeDataRecord ( + segment: BinarySegment, + offset: number, + bytesCount: number + ): void { + if (bytesCount === 0) return; + var addr = + ((segment.xorgValue ?? segment.startAddress) + offset) & 0xffff; + hexOut += `:${toHexa2(bytesCount)}${toHexa4(addr)}00`; // --- Data record header + let checksum = bytesCount + (addr >> 8) + (addr & 0xff); + for (var i = offset; i < offset + bytesCount; i++) { + var data = segment.emittedCode[i]; + checksum += data; + hexOut += `${toHexa2(data)}`; + } + const chk = (256 - (checksum & 0xff)) & 0xff; + hexOut += `${toHexa2(chk)}\r\n`; + } + } + + // --- Creates blocks with autostart functionality + function createAutoStartBlock (output: CompilerOutput): Uint8Array[] { + const clearAddr = exporter.applyClear + ? Math.min( + ...(output as InjectableOutput).segments.map(s => s.startAddress) + ) + : null; + return output.modelType == SpectrumModelType.Spectrum48 || + output.segments.filter(s => s.bank != undefined).length === 0 + ? // --- No banks to emit, use the ZX Spectrum 48 auto-loader format + createSpectrum48StartBlock(output, clearAddr) + : // --- There are banks to emit, use the ZX Spectrum 128 auto-loader format + createSpectrum128StartBlock(output, clearAddr); + } + + // --- Auto start block for ZX Spectrum 48 + function createSpectrum48StartBlock ( + output: CompilerOutput, + clearAddr?: number + ): Uint8Array[] { + const result: Uint8Array[] = []; + + // --- Step #1: Create the code line for auto start + const codeLine: number[] = []; + if (clearAddr !== undefined && clearAddr >= 0x6000) { + // --- Add clear statement + codeLine.push(CLEAR_TKN); + writeNumber(codeLine, clearAddr - 1); + codeLine.push(COLON); + } + + // --- Add optional border color + if (exporter.border != null) { + codeLine.push(BORDER_TKN); + writeNumber(codeLine, exporter.border); + codeLine.push(COLON); + } + + // --- Add optional screen loader, LET o = PEEK 23739 : LOAD "" SCREEN$ : POKE 23739,111 + if (exporter.screenFile) { + codeLine.push(LET_TKN); + writeString(codeLine, "o="); + codeLine.push(PEEK_TKN); + writeNumber(codeLine, 23739); + codeLine.push(COLON); + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(SCREEN_TKN); + codeLine.push(COLON); + codeLine.push(POKE_TKN); + writeNumber(codeLine, 23739); + codeLine.push(COMMA); + writeNumber(codeLine, 111); + codeLine.push(COLON); + } + + // --- Add 'LOAD "" CODE' for each block + if (exporter.singleBlock) { + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(CODE_TKN); + codeLine.push(COLON); + } else { + for (var i = 0; i < output.segments.length; i++) { + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(CODE_TKN); + codeLine.push(COLON); + } + } + + // --- Add 'PAUSE 0' + if (exporter.pause) { + codeLine.push(PAUSE_TKN); + writeNumber(codeLine, 0); + codeLine.push(COLON); + } + + // --- Some SCREEN$ related poking + if (exporter.screenFile) { + codeLine.push(POKE_TKN); + writeNumber(codeLine, 23739); + writeString(codeLine, ",o:"); + } + + // --- Add 'RANDOMIZE USR address' + codeLine.push(RAND_TKN); + codeLine.push(USR_TKN); + writeNumber(codeLine, exporter.address); + + // --- Complete the line + codeLine.push(NEW_LINE); + + // --- Step #2: Now, complete the data block + // --- Allocate extra 6 bytes: 1 byte - header, 2 byte - line number + // --- 2 byte - line length, 1 byte - checksum + const dataBlock = new Uint8Array(codeLine.length + 6); + for (let i = 0; i < codeLine.length; i++) dataBlock[i + 5] = codeLine[i]; + dataBlock[0] = 0xff; + // --- Set line number to 10. Line number uses MSB/LSB order + dataBlock[1] = 0x00; + dataBlock[2] = 10; + // --- Set line length + dataBlock[3] = codeLine.length & 0xff; + dataBlock[4] = (codeLine.length >> 8) & 0xff; + setTapeCheckSum(dataBlock); + + // --- Step #3: Create the header + const header = new SpectrumTapeHeader(); + // --- Program block + header.type = 0; + header.name = exporter.name; + header.dataLength = (dataBlock.length - 2) & 0xffff; + // --- Auto-start at Line 10 + header.parameter1 = 10; + // --- Variable area offset + header.parameter2 = (dataBlock.length - 2) & 0xffff; + + // --- Step #4: Retrieve the auto start header and data block for save + result.push(header.headerBytes); + result.push(dataBlock); + return result; + } + + // --- Auto start block for ZX Spectrum 48 + function createSpectrum128StartBlock ( + output: CompilerOutput, + clearAddr?: number + ): Uint8Array[] { + const result: Uint8Array[] = []; + + // --- We keep the code lines here + var lines: number[][] = []; + + // --- Create placeholder for the paging code (line 10) + var codeLine: number[] = []; + codeLine.push(REM_TKN); + writeString(codeLine, "012345678901234567890"); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Create code for CLEAR/PEEK program address (line 20) + codeLine = []; + if (clearAddr !== undefined && clearAddr >= 0x6000) { + // --- Add clear statement + codeLine.push(CLEAR_TKN); + writeNumber(codeLine, (clearAddr - 1) & 0xffff); + codeLine.push(COLON); + } + + // --- Add "LET c=(PEEK 23635 + 256*PEEK 23636)+5 + codeLine.push(LET_TKN); + writeString(codeLine, "c=("); + codeLine.push(PEEK_TKN); + writeNumber(codeLine, 23635); + writeString(codeLine, "+"); + writeNumber(codeLine, 256); + writeString(codeLine, "*"); + codeLine.push(PEEK_TKN); + writeNumber(codeLine, 23636); + writeString(codeLine, ")+"); + writeNumber(codeLine, 5); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Setup the machine code + codeLine = []; + codeLine.push(FOR_TKN); + writeString(codeLine, "i="); + writeNumber(codeLine, 0); + codeLine.push(TO_TKN); + writeNumber(codeLine, 20); + codeLine.push(COLON); + codeLine.push(READ_TKN); + writeString(codeLine, "d:"); + codeLine.push(POKE_TKN); + writeString(codeLine, "c+i,d:"); + codeLine.push(NEXT_TKN); + writeString(codeLine, "i"); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Create code for BORDER/SCREEN and loading normal code blocks (line 30) + codeLine = []; + if (exporter.border !== undefined) { + codeLine.push(BORDER_TKN); + writeNumber(codeLine, exporter.border & 0xff); + codeLine.push(COLON); + } + + // --- Add optional screen loader, LET o = PEEK 23739:LOAD "" SCREEN$ : POKE 23739,111 + if (exporter.screenFile) { + codeLine.push(LET_TKN); + writeString(codeLine, "o="); + codeLine.push(PEEK_TKN); + writeNumber(codeLine, 23739); + codeLine.push(COLON); + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(SCREEN_TKN); + codeLine.push(COLON); + codeLine.push(POKE_TKN); + writeNumber(codeLine, 23739); + codeLine.push(COMMA); + writeNumber(codeLine, 111); + codeLine.push(COLON); + } + + // --- Add 'LOAD "" CODE' for each block + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(CODE_TKN); + + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Code for reading banks + codeLine = []; + codeLine.push(READ_TKN); + writeString(codeLine, "b"); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- "IF b = 8 THEN GO TO 80"; + codeLine = []; + codeLine.push(IF_TKN); + writeString(codeLine, "b="); + writeNumber(codeLine, 8); + codeLine.push(THEN_TKN); + codeLine.push(GOTO_TKN); + writeNumber(codeLine, 80); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- "POKE 23608,b: RANDOMIZE USR c: LOAD "" CODE: GO TO 50" + codeLine = []; + codeLine.push(POKE_TKN); + writeNumber(codeLine, 23608); + writeString(codeLine, ",b:"); + codeLine.push(RAND_TKN); + codeLine.push(USR_TKN); + writeString(codeLine, "c:"); + codeLine.push(LOAD_TKN); + codeLine.push(DQUOTE); + codeLine.push(DQUOTE); + codeLine.push(CODE_TKN); + codeLine.push(COLON); + codeLine.push(GOTO_TKN); + writeNumber(codeLine, 50); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Finishing + codeLine = []; + + // --- PAUSE and START + if (exporter.pause) { + codeLine.push(PAUSE_TKN); + writeNumber(codeLine, 0); + codeLine.push(COLON); + } + if (exporter.screenFile) { + codeLine.push(POKE_TKN); + writeNumber(codeLine, 23739); + writeString(codeLine, ",o:"); + } + + // --- Add 'RANDOMIZE USR address: STOP' + codeLine.push(RAND_TKN); + codeLine.push(USR_TKN); + writeNumber(codeLine, exporter.address); + codeLine.push(COLON); + codeLine.push(STOP_TKN); + codeLine.push(NEW_LINE); + lines.push(codeLine); + + // --- Add data lines with the machine code subroutine + /* + di 243 + ld a, ($5b5c) 58,92,91 + and $f8 240, 248 + ld b, a 71 + + ld a, ($5c38) 58,56,92 + or b 176 + + ld ($5b5c),a 50,92,91 + + ld bc, ($7ffd) 1,253,127 + out (c), a 237,121 + ei 251 + ret 201 + */ + codeLine = []; + writeDataStatement( + codeLine, + [ + 243, 58, 92, 91, 230, 248, 71, 58, 56, 92, 176, 50, 92, 91, 1, 253, + 127, 237, 121, 251, 201 + ] + ); + lines.push(codeLine); + + // --- Add data lines with used banks and terminating 8 + codeLine = []; + const banks = output.segments + .filter(s => s.bank != null) + .map(s => s.bank); + banks.sort((a, b) => a - b); + banks.push(8); + writeDataStatement(codeLine, banks); + lines.push(codeLine); + + // --- All code lines are set up, create the file blocks + const dataBlock = createDataBlockForCodeLines(lines); + const header = new SpectrumTapeHeader(); + // --- Program block + header.type = 0; + header.name = exporter.name; + header.dataLength = (dataBlock.length - 2) & 0xffff; + // --- Auto-start at Line 10 + header.parameter1 = 10; + // --- Variable area offset + header.parameter2 = (dataBlock.length - 2) & 0xffff; + + // --- Step #4: Retrieve the auto start header and data block for save + result.push(header.headerBytes); + result.push(dataBlock); + return result; + } + + function writeNumber (codeArray: number[], num: number) { + // --- Number in string form + for (const ch of num.toString()) codeArray.push(ch.charCodeAt(0)); + codeArray.push(NUMB_SIGN); + // --- Five bytes as the short form of an integer + codeArray.push(0x00); + codeArray.push(0x00); + codeArray.push(num); + codeArray.push(num >> 8); + codeArray.push(0x00); + } + + function writeString (codeArray: number[], str: string) { + for (const ch of str) codeArray.push(ch.charCodeAt(0)); + } + + function writeDataStatement (codeLine: number[], data: number[]) { + codeLine.push(DATA_TKN); + let comma = false; + for (const item of data) { + if (comma) { + writeString(codeLine, ","); + } + writeNumber(codeLine, item); + comma = true; + } + codeLine.push(NEW_LINE); + } + + function createDataBlockForCodeLines (lines: number[][]): Uint8Array { + const length = + lines.map(cl => cl.length + 4).reduce((a, b) => a + b, 0) + 2; + const dataBlock: Uint8Array = new Uint8Array(length); + dataBlock[0] = 0xff; + let index = 1; + let lineNo = 10; + for (const line of lines) { + // --- Line number in MSB/LSB format + dataBlock[index++] = (lineNo >> 8) & 0xff; + dataBlock[index++] = lineNo & 0xff; + + // --- Set line length in LSB/MSB format + dataBlock[index++] = line.length & 0xff; + dataBlock[index++] = (line.length >> 8) & 0xff; + + // --- Copy the code line + for (let i = 0; i < line.length; i++) { + dataBlock[i + index] = line[i]; + } + + // --- Move to the next line + index += line.length; + lineNo += 10; + } + setTapeCheckSum(dataBlock); + return dataBlock; + } + + // --- Save the collected data blocks + async function saveDataBlocks ( + blocks: Uint8Array[] + ): Promise { + const writer = new BinaryWriter(); + try { + // --- Save data blocks + if (exporter.format === "tzx") { + const header = new TzxHeader(); + header.writeTo(writer); + for (const block of blocksToSave) { + const tzxBlock = new TzxStandardSpeedBlock(); + tzxBlock.data = block; + tzxBlock.dataLength = block.length & 0xffff; + tzxBlock.writeTo(writer); + } + } else { + for (const block of blocksToSave) { + writer.writeUint16(block.length & 0xffff); + writer.writeBytes(block); + } + } + + // --- Save the data to a file + if (exporter.filename) { + const response = await context.messenger.sendMessage({ + type: "MainSaveBinaryFile", + path: exporter.filename, + data: writer.buffer, + resolveIn: `home:${EXPORT_FILE_FOLDER}` + }); + if (response.type === "ErrorResponse") { + return commandError(response.message); + } + return commandSuccessWith( + `Code successfully exported to '${ + (response as MainSaveFileResponse).path + }'` + ); + } + + return commandSuccess; + } catch (err) { + return commandError(err.toString()); + } + } + } + } + + // --- Gets the model code according to machine type + function modelTypeToMachineType (model: SpectrumModelType): string | null { + switch (model) { + case SpectrumModelType.Spectrum48: + return "sp48"; + case SpectrumModelType.Spectrum128: + return "sp128"; + case SpectrumModelType.SpectrumP3: + return "spp3e"; + case SpectrumModelType.Next: + return "next"; + default: + return null; + } + } + + // --- Compile the current project's code + async function compileCode ( + context: IdeCommandContext + ): Promise<{ result?: KliveCompilerOutput; message?: string }> { + // --- Shortcuts + const out = context.output; + const ideCmd = context.service.ideCommandsService; + + // --- Check if we have a build root to compile + const state = context.store.getState(); + if (!state.project?.isKliveProject) { + return { message: "No Klive project loaded." }; + } + const buildRoot = state.project.buildRoots?.[0]; + if (!buildRoot) { + return { message: "No build root selected in the current Klive project." }; + } + const fullPath = `${state.project.folderPath}/${buildRoot}`; + const language = getFileTypeEntry(fullPath)?.subType; + + // --- Compile the build root + out.color("bright-blue"); + out.write("Start compiling "); + ideCmd.writeNavigationAction(context, buildRoot); + out.writeLine(); + out.resetStyle(); + + context.store.dispatch(startCompileAction(fullPath)); + let result: KliveCompilerOutput; + let response: MainCompileResponse; + try { + response = await context.messenger.sendMessage({ + type: "MainCompileFile", + filename: fullPath, + language + }); + if (response.type === "MainCompileFileResponse") { + result = response.result; + } + } finally { + context.store.dispatch(endCompileAction(result)); + await refreshSourceCodeBreakpoints(context.store, context.messenger); + context.store.dispatch(incBreakpointsVersionAction()); + } + + // --- Display optional trace output + const traceOutput = result?.traceOutput; + if (traceOutput?.length > 0) { + out.resetStyle(); + traceOutput.forEach(msg => out.writeLine(msg)); + } + + // --- Collect errors + const errorCount = result?.errors.filter(m => !m.isWarning).length ?? 0; + + if (response.failed) { + if (!result || errorCount === 0) { + // --- Some unexpected error with the compilation + return { message: response.failed }; + } + } + + // --- Display the errors + if ((result.errors?.length ?? 0) > 0) { + for (let i = 0; i < result.errors.length; i++) { + const err = result.errors[i]; + out.color(err.isWarning ? "yellow" : "bright-red"); + out.bold(true); + out.write(`${err.errorCode}: ${err.message}`); + out.write(" - "); + out.bold(false); + out.color("bright-cyan"); + ideCmd.writeNavigationAction( + context, + err.fileName, + err.line, + err.startColumn + ); + out.writeLine(); + out.resetStyle(); + } + } + + // --- Done. + return errorCount > 0 + ? { + result, + message: `Compilation failed with ${errorCount} error${ + errorCount > 1 ? "s" : "" + }.` + } + : { result }; + } + + async function injectCode ( + context: IdeCommandContext, + operationType: CodeInjectionType + ): Promise { + const { message, result } = await compileCode(context); + const errorNo = result?.errors?.length ?? 0; + if (message) { + if (!result) { + return commandError(message); + } + if (errorNo > 0) { + const returnMessage = "Code compilation failed, no program to inject."; + const response = await context.messenger.sendMessage({ + type: "MainDisplayMessageBox", + messageType: "error", + title: "Injecting code", + message: returnMessage + }); + if (response.type === "ErrorResponse") { + reportMessagingError( + `MainDisplayMessageBox call failed: ${response.message}` + ); + } + return commandError(returnMessage); + } + } + + if (!isInjectableCompilerOutput(result)) { + return commandError("Compiled code is not injectable."); + } + + let sumCodeLength = 0; + result.segments.forEach(s => (sumCodeLength += s.emittedCode.length)); + if (sumCodeLength === 0) { + return commandSuccessWith("Code length is 0, no code injected"); + } + + if (operationType === "inject") { + if ( + context.store.getState().emulatorState?.machineState !== + MachineControllerState.Paused + ) { + return commandError("Machine must be in paused state."); + } + } + + // --- Create the code to inject into the emulator + const codeToInject: CodeToInject = { + model: modelTypeToMachineType(result.modelType), + entryAddress: result.entryAddress, + subroutine: result.injectOptions["subroutine"], + segments: result.segments.map(s => ({ + startAddress: s.startAddress, + bank: s.bank, + bankOffset: s.bankOffset ?? 0, + emittedCode: s.emittedCode + })), + options: result.injectOptions + }; + + const dispatch = context.store.dispatch; + let returnMessage = ""; + + switch (operationType) { + case "inject": + const response = await context.messenger.sendMessage({ + type: "EmuInjectCode", + codeToInject + }); + if (response.type === "ErrorResponse") { + return commandError(`EmuInjectCode call failed: ${response.message}`); + } + returnMessage = `Successfully injected ${sumCodeLength} bytes in ${ + codeToInject.segments.length + } segment${ + codeToInject.segments.length > 1 ? "s" : "" + } from start address $${codeToInject.segments[0].startAddress + .toString(16) + .padStart(4, "0") + .toUpperCase()}`; + break; + + case "run": { + const response = await context.messenger.sendMessage({ + type: "EmuRunCode", + codeToInject, + debug: false + }); + if (response.type === "ErrorResponse") { + return commandError(response.message); + } + returnMessage = `Code injected and started.`; + break; + } + + case "debug": { + const response = await context.messenger.sendMessage({ + type: "EmuRunCode", + codeToInject, + debug: true + }); + if (response.type === "ErrorResponse") { + return commandError(response.message); + } + returnMessage = `Code injected and started in debug mode.`; + break; + } + } + + // --- Injection done + dispatch(incInjectionVersionAction()); + return commandSuccessWith(returnMessage); + } + + const CLEAR_TKN = 0xfd; + const CODE_TKN = 0xaf; + const DATA_TKN = 0xe4; + const FOR_TKN = 0xeb; + const IF_TKN = 0xfa; + const GOTO_TKN = 0xec; + const LET_TKN = 0xf1; + const LOAD_TKN = 0xef; + const NEXT_TKN = 0xf3; + const PEEK_TKN = 0xbe; + const POKE_TKN = 0xf4; + const READ_TKN = 0xe3; + const REM_TKN = 0xea; + const THEN_TKN = 0xcb; + const TO_TKN = 0xcc; + const SCREEN_TKN = 0xaa; + const DQUOTE = 0x22; + const STOP_TKN = 0xe2; + const COLON = 0x3a; + const COMMA = 0x2c; + const RAND_TKN = 0xf9; + const USR_TKN = 0xc0; + const NUMB_SIGN = 0x0e; + const NEW_LINE = 0x0d; + const PAUSE_TKN = 0xf2; + const BORDER_TKN = 0xe7; + \ No newline at end of file diff --git a/src/renderer/appIde/commands/ScriptCommands.ts b/src/renderer/appIde/commands/ScriptCommands.ts index 8ec78a101..e878c6c50 100644 --- a/src/renderer/appIde/commands/ScriptCommands.ts +++ b/src/renderer/appIde/commands/ScriptCommands.ts @@ -164,8 +164,9 @@ export class RunBuildScriptCommand extends CommandWithSingleStringBase { // --- Create the script to run const script = `import { ${this.arg} } from "./${BUILD_FILE}";\n\n${this.arg}();`; + let id = 0; try { - const id = await context.service.scriptService.runScriptText( + id = await context.service.scriptService.runScriptText( script, this.arg, buildFileName, @@ -178,7 +179,7 @@ export class RunBuildScriptCommand extends CommandWithSingleStringBase { id ); } catch (err) { - return commandError(err.message); + return commandError(err.message, id); } } } diff --git a/src/renderer/appIde/services/IdeCommandService.ts b/src/renderer/appIde/services/IdeCommandService.ts index 4e573b213..d4d45344e 100644 --- a/src/renderer/appIde/services/IdeCommandService.ts +++ b/src/renderer/appIde/services/IdeCommandService.ts @@ -110,11 +110,15 @@ class IdeCommandService implements IIdeCommandService { /** * Executes the specified command line * @param command Command to execute + * @param buffer Optional output buffer + * @param useHistory Add the command to the history + * @param interactiveContex Indicates that the command is executed in interactive context */ async executeInteractiveCommand ( command: string, buffer?: IOutputBuffer, - useHistory = true + useHistory = true, + interactiveContex = true ): Promise { // --- Create a buffer if that does not exists buffer ??= new OutputPaneBuffer(); @@ -151,6 +155,18 @@ class IdeCommandService implements IIdeCommandService { }; } + // --- Allow if the command can be executed interactively + if (!!commandInfo.noInteractiveUsage && interactiveContex) { + const finalMessage = `Command '${commandId}' cannot be executed interactively`; + buffer.color("bright-red"); + buffer.writeLine(finalMessage); + buffer.resetStyle(); + return { + success: false, + finalMessage + }; + } + // --- Execute the registered command const machineId = this.store.getState().emulatorState?.machineId; const machineInfo = machineRegistry.find(m => m.machineId === machineId); @@ -193,7 +209,7 @@ class IdeCommandService implements IIdeCommandService { command: string, buffer?: IOutputBuffer ): Promise { - return this.executeInteractiveCommand(command, buffer, false); + return this.executeInteractiveCommand(command, buffer, false, false); } /** @@ -303,13 +319,17 @@ class HelpCommand extends IdeCommandBase { selectedCommands .sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)) .forEach(ci => { - out.color("bright-magenta"); + out.color(ci.noInteractiveUsage ? "magenta" : "bright-magenta"); out.bold(true); out.write(`${ci.id}`); out.bold(false); if ((ci.aliases ?? []).length > 0) { out.write(` (${ci.aliases.join(", ")})`); } + if (ci.noInteractiveUsage) { + out.color("cyan"); + out.write(" [non-interactive]"); + } context.output.color("bright-blue"); context.output.write(`: `); context.output.writeLine(ci.description); diff --git a/src/renderer/appIde/services/ide-commands.ts b/src/renderer/appIde/services/ide-commands.ts index 3f0eb2ccd..6cfca9b92 100644 --- a/src/renderer/appIde/services/ide-commands.ts +++ b/src/renderer/appIde/services/ide-commands.ts @@ -173,10 +173,11 @@ export function validationError(message: string): ValidationMessage { * Represents a command execution error * @param message Error message */ -export function commandError(message: string): IdeCommandResult { +export function commandError(message: string, value?: any): IdeCommandResult { return { success: false, - finalMessage: message + finalMessage: message, + value }; } @@ -282,7 +283,7 @@ export function getPartitionedValue(token: Token): { if (segments.length === 2 && segments[0].length <= 2) { // --- Extract partition information let partition: number | undefined; - let partitionType = "B" + let partitionType = "B"; const partStr = segments[0].toUpperCase(); let partNoIdx = 0; if (partStr.startsWith("R")) { @@ -300,7 +301,7 @@ export function getPartitionedValue(token: Token): { if (valueInfo.messages) break; // --- Return with the info - return { value: valueInfo.value, partition, partitionType} + return { value: valueInfo.value, partition, partitionType }; } break; }