import * as vscode from 'vscode' ; import { Remote } from '../remotes/remotebase' ; import * as util from 'util' ; import { Utility } from '../misc/utility' ; import { Labels } from '../labels/labels' ; import { MetaBlock , MemoryDump } from '../misc/memorydump' ; import { Settings } from '../settings' ; import { Z80RegistersClass } from '../remotes/z80registers' ; import { BaseView } from './baseview' ; const MEM_DUMP_BOUNDARY = 16 ; /**
 * A Webview that shows a memory dump.
 * The memory dump can also be edited.
 * There is a rather complex messaging between the webview's html javascript (the webview
 * panel) and the extension (the typescript code):
 * - Display:
 * 		- Register coloring: If the address of a value is the same as a register value it is colored
 * differently. 'setColorsForRegisterPointers' calls 'setAddressColor' in the webview. This is done
 * when the panel is created or updated (each step) or when the webview becomes visible (e.g. if
 * it was hidden).
 * 		- The dump contents is updated on every step ('update').
 * 		- The hovering text (labels etc.): When the mouse is over a value or address the webview asks for the hovering text ('getValueInfoText/getAddressInfoText'.
 * 		This way the calculation of all labels is delayed. A message with the info is sent to the webview ('valueInfoText/addressInfoText').
 * - Editing:
 * 		- On double click the webview turns the cell in editable mode.
 * 		- When the user presses enter the new value is sent from the webview('valueChanged').
 * 		- The value is then changed in the remote and the real value (from the remote) is sent to
 * 		to the webview. The webview will then update all cells with the same address. I.e. if there
 * 		are 2 same cells both are updated.
 * 		- If there are several memory views all are informed about the new value to update their display.
 *
 * See design.md for a sequence chart.
 */ export class MemoryDumpView extends BaseView { protected static MemoryViews = Array < MemoryDumpView > ( ) ; protected memDump = new MemoryDump ( ) ; protected prevRegAddr = new Map < string , number > ( ) ; protected titlePrefix = "Memory " ; /**
     * Creates the basic panel.
     */ constructor ( ) { super ( ) ; MemoryDumpView . MemoryViews . push ( this ) ; } /**
     * Dispose the view (called e.g. on close).
     * Removes it from the static list.
     */ public disposeView ( ) { super . disposeView ( ) ; const arr = MemoryDumpView . MemoryViews ; const index = arr . indexOf ( this ) ; Utility . assert ( index >= 0 ) ; arr . splice ( index , 1 ) ; } param message protected async webViewMessageReceived ( message ) { switch ( message . command ) { case 'valueChanged' : try { const address = parseInt ( message . address ) ; const value = Utility . evalExpression ( message . value ) ; await this . changeMemory ( address , value ) ; } catch ( e ) { vscode . window . showWarningMessage ( "Could not evaluate: '" + message . value + "'" ) ; } break ; case 'getValueInfoText' : { const address = parseInt ( message . address ) ; await this . getValueInfoText ( address ) ; } break ; case 'getAddressInfoText' : { const address = parseInt ( message . address ) ; await this . getAddressInfoText ( address ) ; } break ; default : await super . webViewMessageReceived ( message ) ; break ; } } param startAddress param size public addBlock ( startAddress , size , title ) { this . memDump . addBlock ( startAddress , size , title ) ; } /**
     * Merges nearby blocks into one block.
     */ public mergeBlocks ( ) { this . memDump . mergeBlocks ( ) ; } param address param value protected async changeMemory ( address , value ) { const realValue = await Remote . writeMemory ( address , value ) ; for ( const mdv of MemoryDumpView . MemoryViews ) { if ( ! isNaN ( mdv . memDump . getValueFor ( address ) ) ) { mdv . memDump . setValueFor ( address , realValue ) ; } } BaseView . staticCallUpdateWithoutRemote ( ) ; BaseView . sendChangeEvent ( ) ; } param address protected async getValueInfoText ( address ) { const value = this . memDump . getValueFor ( address ) ; const valFormattedString = await Utility . numberFormatted ( '' , value , 1 , Settings . launch . memoryViewer . valueHoverFormat , undefined ) ; let text = valFormattedString + '\n' ; const addrFormattedString = await Utility . numberFormatted ( '' , address , 2 , Settings . launch . memoryViewer . addressHoverFormat , undefined ) ; text += '@\n' + addrFormattedString ; const prevValue = this . memDump . getPrevValueFor ( address ) ; if ( ! isNaN ( prevValue ) ) { if ( prevValue != value ) { text += '\nPrevious value: ' + Utility . getHexString ( prevValue , 2 ) + 'h' ; } } const msg = { command : 'valueInfoText' , address : address . toString ( ) , text : text } ; this . sendMessageToWebView ( msg ) ; } param address protected async getAddressInfoText ( address ) { const formattedString = await Utility . numberFormatted ( '' , address , 2 , Settings . launch . memoryViewer . addressHoverFormat , undefined ) ; const msg = { command : 'addressInfoText' , address : address . toString ( ) , text : formattedString } ; this . sendMessageToWebView ( msg ) ; } /**
     * View is informed that a register has changed (manually).
     */ public async updateRegisterChanged ( ) { this . setColorsForRegisterPointers ( ) ; } param reason public async update ( reason ? ) { for ( let metaBlock of this . memDump . metaBlocks ) { const data = await Remote . readMemoryDump ( metaBlock . address , metaBlock . size ) ; metaBlock . prevData = metaBlock . data || new Uint8Array ( data ) ; metaBlock . data = data ; } this . updateWithoutRemote ( ) ; } /**
     * Updates the html. E.g. after the change of a value.
     * Without getting the memory from the Remote.
     */ protected updateWithoutRemote ( ) { if ( ! this . vscodePanel . webview . html ) { this . setHtml ( ) ; if ( this . vscodePanel ) { let title = '' ; for ( let metaBlock of this . memDump . metaBlocks ) { if ( title ) title += ', ' ; title += metaBlock . title ; } title = this . titlePrefix + title ; this . vscodePanel . title = title ; } } else { const msg = { command : 'setMemoryTable' , index : 0 , html : "" } ; let i = 0 ; for ( let metaBlock of this . memDump . metaBlocks ) { msg . html = this . createHtmlTable ( metaBlock ) ; msg . index = i ; this . sendMessageToWebView ( msg ) ; i ++ ; } } this . setColorsForRegisterPointers ( ) ; } /**
     * Creates the script (i.e. functions) for all blocks (html tables).
     */ protected createHtmlScript ( ) { const html = `
		<script>
		const vscode = acquireVsCodeApi();


		//---- Handle Mouse Over, Calculation of hover text -------
		function mouseOverValue(obj) {
			const address = obj.getAttribute("address");
			// Send request to vscode to calculate the hover text
			vscode.postMessage({
				command: 'getValueInfoText',
				address: address
			});
		}

		function mouseOverAddress(obj) {
			// Send request to vscode to calculate the hover text
			const address = obj.getAttribute("addressLine");
			vscode.postMessage({
				command: 'getAddressInfoText',
				address: address
			});
		}


		//---- Handle Editing Cells --------
		let prevValue = '';	// Used to restore the value if ESC is pressed.
		let curObj = null;	// The currently used object (the tabbed cell)

		function keyPress(e) {
			let key = e.keyCode;

			if(key == 13) {	// ENTER key
				const value = curObj.innerText;
				const address = curObj.getAttribute("address");
				e.preventDefault();
				curObj.blur();
				// Send new value for address to vscode
				vscode.postMessage({
					command: 'valueChanged',
					address: address,
					value: value
				});
			}
			else if(key == 27) {	// ESC key, does not work in vscode
				// Use previous value
				e.preventDefault();
				curObj.blur();
			}
		}

		function focusLost(e) {	// = "blur"
			// Undo: Use previous value
			if(prevValue.length > 0)
				curObj.innerText = prevValue;
			curObj.contentEditable = false;
			curObj.removeEventListener("blur", focusLost);
			curObj.removeEventListener("keypress", keyPress);
			curObj = null;
		}

		function makeEditable(obj) {
			// makes the object editable on double click.
			curObj = obj;	// store object for use in other functions
			prevValue = curObj.innerText;	// store for undo
			if(!curObj.innerText.endsWith('h'))
				curObj.innerText += 'h';
			curObj.contentEditable = true;
			curObj.focus();
			selection = window.getSelection();    // Save the selection.

			// Select the text
			range = document.createRange();
			range.selectNodeContents(curObj);
			selection.removeAllRanges();          // Remove all ranges from the selection.
			selection.addRange(range);

			// Add listeners
			curObj.addEventListener("blur", focusLost, true);
			curObj.addEventListener("keypress", keyPress, true);
		}


		//---- Handle Messages from vscode extension --------
		window.addEventListener('message', event => {
			const message = event.data;

            switch (message.command) {
				case 'valueInfoText':
				{
					// HEX numbers
					const objs = document.querySelectorAll("td[address='"+message.address+"']");
					for(let obj of objs) {
						obj.title = message.text;
					}
					// ASCII
					const spanObjs = document.querySelectorAll("span[address='"+message.address+"']");
					for(let obj of spanObjs) {
						obj.title = message.text;
					}
                }   break;

				case 'addressInfoText':
				{
					const objs = document.querySelectorAll("td[addressLine='"+message.address+"']");
					for(let obj of objs) {
						obj.title = message.text;
					}
				}   break;

				case 'setAddressColor':
				{
					// HEX
					const objs = document.querySelectorAll("td[address='"+message.address+"']");
					for(let obj of objs) {
						obj.style.backgroundColor = message.color;
						obj.style.borderRadius = '3px';
					}
					// ASCII
					const spanObjs = document.querySelectorAll("span[address='"+message.address+"']");
					for(let obj of spanObjs) {
						obj.style.color = "white";
						obj.style.backgroundColor = message.color;
						obj.style.borderRadius = '3px';
					}
				 }   break;

				case 'setMemoryTable':
				{
					// Set table as html string
			        const tableDiv=document.getElementById("mem_table_"+message.index);
					tableDiv.innerHTML=message.html;
 				}   break;

           }
        });

		//# sourceURL=memorydumpview.js
		</script>
` ; return html ; } param index param metaBlock protected createHtmlTableTemplate ( index , metaBlock ) { let caption = metaBlock . title || '...' ; const table = this . createHtmlTable ( metaBlock ) ; const html = `
		<details open="true">
			<summary>${ caption }</summary>
			<div id="mem_table_${ index }">
			${ table }
			</div>
		</details>
		` ; return html ; } param index param metaBlock protected createHtmlTable ( metaBlock ) { if ( ! metaBlock . data ) return '' ; const format = `			<table style="">
				<colgroup>
					<col>
					<col width="10em">
					<col span="%d" width="20em">
					<col width="10em">
				</colgroup>

			%s
			</table>
		` ; let table = '' ; let address = metaBlock . address ; let i = 0 ; const clmns = MEM_DUMP_BOUNDARY ; const data = metaBlock . data ; const len = data . length ; const addressColor = Settings . launch . memoryViewer . addressColor ; const asciiColor = Settings . launch . memoryViewer . asciiColor ; const bytesColor = Settings . launch . memoryViewer . bytesColor ; let clmStart = address % clmns ; table += '<tr>\n<th>Address:</th> <th></th>' ; for ( let k = 0 ; k < clmns ; k ++ ) { const c = clmStart + k ; table += '<th>' + c . toString ( 16 ) . toUpperCase ( ) + '</th>' ; } table += '\n</tr>' ; let ascii = '' ; for ( let k = 0 ; k < len ; k ++ ) { const addr64k = address & 0xFFFF ; if ( i == 0 ) { let addrText = Utility . getHexString ( addr64k , 4 ) + ':' ; table += '<tr>\n<td addressLine="' + addr64k + '" style="color:' + addressColor + '; border-radius:3px; cursor: pointer" onmouseover="mouseOverAddress(this)">' + addrText + '</td>\n' ; table += '<td> </td>\n' ; ascii = '' ; } const value = data [ k ] ; let valueText = Utility . getHexString ( value , 2 ) ; if ( metaBlock . isInRange ( address ) ) valueText = this . addEmphasizeInRange ( valueText ) ; else valueText = this . addDeemphasizeNotInRange ( valueText ) ; if ( Labels . getLabelsForNumber64k ( addr64k ) . length > 0 ) valueText = this . addEmphasizeLabelled ( valueText ) ; const prevData = metaBlock . prevData ; if ( prevData ) { if ( prevData . length > 0 ) { const prevValue = prevData [ k ] ; if ( value != prevValue ) { valueText = this . addEmphasizeChanged ( valueText ) ; } } } table += '<td address="' + addr64k + '" ondblclick="makeEditable(this)" onmouseover="mouseOverValue(this)" style="color:' + bytesColor + '">' + valueText + '</td>\n' ; ascii += '<span address="' + addr64k + '" onmouseover="mouseOverValue(this)">' + Utility . getHTMLChar ( value ) + '</span>' ; if ( i == clmns - 1 ) { table += '<td> </td>\n' ; table += '<td style="color:' + asciiColor + '">' + ascii + '</td>\n' ; table += '</tr>\n' ; } address ++ ; i ++ ; if ( i >= clmns ) i = 0 ; } const html = util . format ( format , clmns , table ) ; return html ; } /**
     * Sets the html code to display the memory dump.
     * Is called only once at creation time as it does not hold the actual data.
     */ protected setHtml ( ) { const format = `<!DOCTYPE html>
		<html lang="en">
		<head>
			<meta charset="UTF-8">
			<meta name="viewport" content="width=device-width, initial-scale=1.0">
			<title>Dump</title>
		</head>

		<body style="font-family: Courier">

		%s

		%s

		</body>
		</html>` ; let legend = `
		<br>
		Legend:<br>
		` ; const regColors = Settings . launch . memoryViewer . registerPointerColors ; const regColorsLen = regColors . length ; for ( let k = 0 ; k < regColorsLen ; k += 2 ) { const color = regColors [ k + 1 ] ; legend += '<span style="background-color: ' + color + ';border-radius: 3px">&nbsp; ' + regColors [ k ] + ' &nbsp;</span> &nbsp;&nbsp; ' ; } let tables ; const vertBreak = this . getHtmlVertBreak ( ) ; let i = 0 ; for ( let mb of this . memDump . metaBlocks ) { const table = this . createHtmlTableTemplate ( i , mb ) ; tables = ( tables ) ? tables + vertBreak + table : table ; i ++ ; } const scripts = this . createHtmlScript ( ) ; const html = util . format ( format , scripts + tables , legend ) ; this . vscodePanel . webview . html = html ; } /**
     * Set colors for register pointers.
     * Colors are only set if the webview is visible.
     */ protected setColorsForRegisterPointers ( ) { const setAddrs = new Array < number > ( ) ; const arr = Settings . launch . memoryViewer . registerPointerColors ; for ( let i = 0 ; i < arr . length - 1 ; i += 2 ) { const reg = arr [ i ] ; if ( ! Z80RegistersClass . isRegister ( reg ) ) continue ; const address = Remote . getRegisterValue ( reg ) ; let prevAddr = this . prevRegAddr . get ( reg ) ; if ( prevAddr != undefined ) { if ( ! setAddrs . includes ( prevAddr ) ) { const msgPrev = { command : 'setAddressColor' , address : prevAddr . toString ( ) , color : "transparent" } ; this . sendMessageToWebView ( msgPrev ) ; } } const color = arr [ i + 1 ] ; const msg = { command : 'setAddressColor' , address : address . toString ( ) , color : color } ; this . sendMessageToWebView ( msg ) ; this . prevRegAddr . set ( reg , address ) ; setAddrs . push ( address ) ; } } /**
     * Determines what is shown between the tables,
     * e.g. "...".
     */ protected getHtmlVertBreak ( ) { return '\n' ; } param origText param colorText returns protected addBckgColor ( origText , colorText ) { const resText = '<div style="background-color:' + colorText + '">' + origText + '</div>' ; return resText ; } param origText returns protected addEmphasizeChanged ( origText ) { const resText = '<font color="red">' + origText + '</font>' ; return resText ; } param origText returns protected addEmphasizeLabelled ( origText ) { const resText = '<u>' + origText + '</u>' ; return resText ; } param origText returns protected addEmphasizeInRange ( origText ) { const resText = '<b>' + origText + '</b>' ; return resText ; } protected addDeemphasizeNotInRange ( origText ) { const resText = '<font color="gray">' + origText + '</font>' ; return resText ; } }