Bug 8519 [Macros] - Code review feedback

- use XML instead of JavaScript for persisting macro
- fix up javadocs
- use Assert, no RuntimeExceptions
- MacroServiceImplementation -> MacroServiceImpl
- allow clients to change current macro on stop record notification

Change-Id: I8f6f7689ef3cf302c7a6378a66ab779045c9c1ac
Signed-off-by: Fabio Zadrozny <fabiofz@gmail.com>
diff --git a/bundles/org.eclipse.e4.core.macros/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.core.macros/META-INF/MANIFEST.MF
index 5306d02..ba41e27 100644
--- a/bundles/org.eclipse.e4.core.macros/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.e4.core.macros/META-INF/MANIFEST.MF
@@ -12,8 +12,7 @@
  org.eclipse.e4.core.di;bundle-version="1.6",
  org.eclipse.equinox.registry;bundle-version="3.6",
  javax.inject;bundle-version="1.0",
- org.eclipse.core.runtime;bundle-version="3.12",
- org.eclipse.e4.ui.bindings;bundle-version="0.11"
+ org.eclipse.core.runtime;bundle-version="3.12"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Bundle-ActivationPolicy: lazy
 Bundle-Activator: org.eclipse.e4.core.macros.Activator
diff --git a/bundles/org.eclipse.e4.core.macros/OSGI-INF/l10n/bundle.properties b/bundles/org.eclipse.e4.core.macros/OSGI-INF/l10n/bundle.properties
index 0ae6505..583a125 100644
--- a/bundles/org.eclipse.e4.core.macros/OSGI-INF/l10n/bundle.properties
+++ b/bundles/org.eclipse.e4.core.macros/OSGI-INF/l10n/bundle.properties
@@ -2,5 +2,5 @@
 Bundle-Name = Core Macro Recording and Playback
 Bundle-Vendor = Eclipse.org
 extension-point.macroInstructionsFactory.name = Register factory class to restore a macro instruction which has been previously recorded through the macro record/playback engine.
-extension-point.macroStateListeners.name = Listeners for changes in the macro service (i.e.: record/playback mode).
+extension-point.macroStateListeners.name = Listeners for changes in the macro service (i.e., record/playback mode).
 extension-point.commandHandling.name = Describe Eclipse Core Command ids which should have some customization in macro handling (by default eclipse core commands have their activation automatically record an IMacroInstruction and this extension allows a different handling).
diff --git a/bundles/org.eclipse.e4.core.macros/schema/commandHandling.exsd b/bundles/org.eclipse.e4.core.macros/schema/commandHandling.exsd
index b2a339a..fbaddb6 100644
--- a/bundles/org.eclipse.e4.core.macros/schema/commandHandling.exsd
+++ b/bundles/org.eclipse.e4.core.macros/schema/commandHandling.exsd
@@ -69,7 +69,7 @@
          <attribute name="recordMacroInstruction" type="boolean" use="required">
             <annotation>
                <documentation>
-                  If true, the activation of the command will be recorded in the macro (i.e.: an IMacroInstruction will be automatically created and added to the macro when in record mode) and if false it won&apos;t.
+                  If true, the activation of the command will be recorded in the macro (i.e., an IMacroInstruction will be automatically created and added to the macro when in record mode) and if false it won&apos;t.
 
 Examples of actions whose activation should be recorded include copy, paste, delete line and anything whose playback is simply re-executing that action.
 
diff --git a/bundles/org.eclipse.e4.core.macros/schema/macroInstructionsFactory.exsd b/bundles/org.eclipse.e4.core.macros/schema/macroInstructionsFactory.exsd
index bef089e..6b7bec9 100644
--- a/bundles/org.eclipse.e4.core.macros/schema/macroInstructionsFactory.exsd
+++ b/bundles/org.eclipse.e4.core.macros/schema/macroInstructionsFactory.exsd
@@ -59,7 +59,7 @@
          <attribute name="macroInstructionId" type="string" use="required">
             <annotation>
                <documentation>
-                  The id of the macro instruction which this factory can be used to create (i.e.: the method org.eclipse.e4.core.macros.IMacroInstruction.getId(), of the macro instructions created by this factory must return the same id).
+                  The id of the macro instruction which this factory can be used to create (i.e., the method org.eclipse.e4.core.macros.IMacroInstruction.getId(), of the macro instructions created by this factory must return the same id).
                </documentation>
                <appinfo>
                   <meta.attribute kind="identifier"/>
diff --git a/bundles/org.eclipse.e4.core.macros/schema/macroStateListeners.exsd b/bundles/org.eclipse.e4.core.macros/schema/macroStateListeners.exsd
index 88c91cf..ee5f66e 100644
--- a/bundles/org.eclipse.e4.core.macros/schema/macroStateListeners.exsd
+++ b/bundles/org.eclipse.e4.core.macros/schema/macroStateListeners.exsd
@@ -6,7 +6,7 @@
          <meta.schema plugin="org.eclipse.e4.core.macros" id="macroStateListeners" name="Listeners for changes on the macro state"/>
       </appinfo>
       <documentation>
-         Allows for other plugins to register listeners to be notified of changes in the macro service (i.e.: when a record or playback is started or stopped).
+         Allows for other plugins to register listeners to be notified of changes in the macro service (i.e., when a record or playback is started or stopped).
 
 It is possible, for instance, to add listeners which will do the recording of keystrokes for playing back later when that happens, disable commands/actions which are not allowed during macro record/playback, add a listener to record changes to preferences, etc.
       </documentation>
@@ -54,7 +54,7 @@
          <attribute name="class" type="string">
             <annotation>
                <documentation>
-                  A listener for changes in the macro state (i.e.: macro record or playback started or stopped).
+                  A listener for changes in the macro state (i.e., macro record or playback started or stopped).
 
 These events signal that packages should install and remove instrumentation for generating macro instructions in response to user gestures.
 
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/Activator.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/Activator.java
index ca98f0f..1d1482a 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/Activator.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/Activator.java
@@ -30,19 +30,45 @@
 		return plugin;
 	}
 
+	/**
+	 * Logs a message.
+	 *
+	 * @param severity
+	 *            the {@link IStatus} severity
+	 * @param message
+	 *            the status message
+	 */
+	public static void log(int severity, String message) {
+		log(new Status(severity, plugin.getBundle().getSymbolicName(), message));
+	}
+
+	/**
+	 * Logs an exception.
+	 *
+	 * @param exception
+	 *            the exception to be logged
+	 */
 	public static void log(Throwable exception) {
+		log(new Status(IStatus.ERROR, plugin.getBundle().getSymbolicName(), exception.getMessage(), exception));
+	}
+
+	/**
+	 * Logs a status.
+	 *
+	 * @param status
+	 *            the status to be logged
+	 */
+	public static void log(IStatus status) {
 		try {
 			if (plugin != null) {
-				plugin.getLog().log(new Status(IStatus.ERROR, plugin.getBundle().getSymbolicName(),
-						exception.getMessage(), exception));
+				plugin.getLog().log(status);
 			} else {
 				// The plugin is not available. Just print to stderr.
-				exception.printStackTrace();
+				System.err.println(status);
 			}
 		} catch (Exception e) {
-			// Print the original error if something happened, not the one
-			// related to the log not working.
-			exception.printStackTrace();
+			// Print the original status if something happened
+			System.err.println(status);
 		}
 	}
 }
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/EMacroService.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/EMacroService.java
index 615b2f9..62e493b 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/EMacroService.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/EMacroService.java
@@ -140,8 +140,8 @@
 	void removeMacroStateListener(IMacroStateListener listener);
 
 	/**
-	 * Provides the macro record context or null if the macro engine is not
-	 * recording.
+	 * Provides the macro record context or {@code null} if the macro engine is
+	 * not recording.
 	 *
 	 * @return the macro record context created when macro record started or
 	 *         {@code null} if not currently recording.
@@ -149,34 +149,33 @@
 	IMacroRecordContext getMacroRecordContext();
 
 	/**
-	 * Provides the macro playback context or null if the macro engine is not
-	 * playing back.
+	 * Provides the macro playback context or {@code null} if the macro engine
+	 * is not playing back.
 	 *
-	 * @return the macro playback context created when the macro playback started or
-	 *         {@code null} if not currently playing back.
+	 * @return the macro playback context created when the macro playback
+	 *         started or {@code null} if not currently playing back.
 	 */
 	IMacroPlaybackContext getMacroPlaybackContext();
 
 	// Deal with managing accepted commands during macro record/playback.
-	// (by default should load the command behavior
-	// through the org.eclipse.e4.core.macros.commandHandling extension
-	// point,
-	// but it is possible to programmatically change it as needed later on).
+	// (by default should load the command behavior through the
+	// org.eclipse.e4.core.macros.commandHandling extension point, but it is
+	// possible to programmatically change it as needed later on).
 
 	/**
-	 * Returns {@code true} if the given Eclipse Core Command should be recorded and
-	 * {@code false} otherwise. This specifically means that when a given Eclipse
-	 * Core Command is executed, a macro instruction will be created for it.
-	 * Likewise, if {@code false} is returned, a macro instruction will not be
-	 * created automatically (and as such, it won't be recorded in the macro). See
-	 * the {@code org.eclipse.e4.core.macros.commandHandling} extension point for
-	 * details.
+	 * Returns {@code true} if the given Eclipse Core Command should be recorded
+	 * and {@code false} otherwise. This specifically means that when a given
+	 * Eclipse Core Command is executed, a macro instruction will be created for
+	 * it. Likewise, if {@code false} is returned, a macro instruction will not
+	 * be created automatically (and as such, it won't be recorded in the
+	 * macro). See the {@code org.eclipse.e4.core.macros.commandHandling}
+	 * extension point for details.
 	 *
 	 * @param commandId
 	 *            the id of the Eclipse Core Command.
 	 *
-	 * @return whether the given Eclipse Core Command should be recorded for
-	 *         playback when recording a macro (i.e.: an
+	 * @return {@code true} if the given Eclipse Core Command should be recorded
+	 *         for playback when recording a macro (i.e., an
 	 *         {@link org.eclipse.e4.core.macros.IMacroInstruction} will be
 	 *         automatically created to play it back when in record mode).
 	 *
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroInstruction.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroInstruction.java
index e3648aa..6c9707a 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroInstruction.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroInstruction.java
@@ -39,14 +39,13 @@
 	void execute(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException;
 
 	/**
-	 * Converts the macro instruction into a map for serialization, that can be
-	 * used to recreate the macro instruction with an
-	 * {@link IMacroInstructionFactory} registered through the
-	 * {@code org.eclipse.e4.core.macros.macroInstructionsFactory} extension
-	 * point.
+	 * Converts the macro instruction into a map for serialization, that can be used
+	 * to recreate the macro instruction with an {@link IMacroInstructionFactory}
+	 * registered through the
+	 * {@code org.eclipse.e4.core.macros.macroInstructionsFactory} extension point.
 	 *
 	 * @return a map that may be serialized and that can be used to recreate the
-	 *         macro instruction
+	 *         macro instruction.
 	 */
 	Map<String, String> toMap();
 
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroPlaybackContext.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroPlaybackContext.java
index c542eba..21e850d 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroPlaybackContext.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/IMacroPlaybackContext.java
@@ -10,9 +10,25 @@
  *******************************************************************************/
 package org.eclipse.e4.core.macros;
 
+import java.util.Map;
+
 /**
  * Context passed when playing back a macro.
  */
 public interface IMacroPlaybackContext extends IMacroContext {
 
+	/**
+	 * Runs a macro instruction given its id and parameters.
+	 *
+	 * @param macroInstructionId
+	 *            the id of the macro instruction to be run.
+	 * @param macroInstructionParameters
+	 *            the parameters to create the macro instruction.
+	 * @throws Exception
+	 *             if it was not possible to create the macro instruction with the
+	 *             given parameters.
+	 */
+	void runMacroInstruction(String macroInstructionId, Map<String, String> macroInstructionParameters)
+			throws Exception;
+
 }
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/ComposableMacro.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/ComposableMacro.java
index 0c1c13d..6ffb8fb 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/ComposableMacro.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/ComposableMacro.java
@@ -10,28 +10,62 @@
  *******************************************************************************/
 package org.eclipse.e4.core.macros.internal;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.e4.core.macros.IMacroInstruction;
 import org.eclipse.e4.core.macros.IMacroInstructionFactory;
 import org.eclipse.e4.core.macros.IMacroPlaybackContext;
 import org.eclipse.e4.core.macros.MacroPlaybackException;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 
 /**
- * This is a macro which is created from a sequence of instructions which are
- * stored in-memory (and may be persisted later on).
+ * A macro that is created from a sequence of instructions which are stored
+ * in-memory (and may be persisted later on).
  */
-/* default */ class ComposableMacro implements IMacro {
+public class ComposableMacro implements IMacro {
+
+	public static final String XML_MACRO_TAG = "macro"; //$NON-NLS-1$
+
+	public static final String XML_INSTRUCTION_TAG = "instruction"; //$NON-NLS-1$
+
+	public static final String XML_ID_ATTRIBUTE = "id"; //$NON-NLS-1$
+
+	public static final String XML_BASE64_ATTRIBUTE = "base64"; //$NON-NLS-1$
+
+	public static final String XML_BASE64_ATTRIBUTE_TRUE = "true"; //$NON-NLS-1$
+
+	public static final String XML_DEFINITION_TAG = "definition"; //$NON-NLS-1$
+
+	public static final String XML_KEY_TAG = "key"; //$NON-NLS-1$
+
+	public static final String XML_VALUE_TAG = "value"; //$NON-NLS-1$
 
 	/**
 	 * Provides the macro instruction id to an implementation which is able to
 	 * recreate it.
 	 */
-	private Map<String, IMacroInstructionFactory> fMacroInstructionIdToFactory;
+	private final Map<String, IMacroInstructionFactory> fMacroInstructionIdToFactory;
 
 	/**
 	 * The macro instructions which compose this macro.
@@ -72,12 +106,11 @@
 	 *            the macro instruction to be checked.
 	 */
 	private void checkMacroInstruction(IMacroInstruction macroInstruction) {
-		if (fMacroInstructionIdToFactory != null
-				&& !fMacroInstructionIdToFactory.containsKey(macroInstruction.getId())) {
-			throw new RuntimeException(String.format(
-					"Macro instruction: %s not properly registered through a %s extension point.", //$NON-NLS-1$
-					macroInstruction.getId(), MacroServiceImplementation.MACRO_INSTRUCTION_FACTORY_EXTENSION_POINT));
-		}
+		Assert.isTrue(
+				fMacroInstructionIdToFactory == null
+						|| fMacroInstructionIdToFactory.containsKey(macroInstruction.getId()),
+				String.format("Macro instruction: %s not properly registered through a %s extension point.", //$NON-NLS-1$
+						macroInstruction.getId(), MacroServiceImpl.MACRO_INSTRUCTION_FACTORY_EXTENSION_POINT));
 	}
 
 	/**
@@ -137,8 +170,7 @@
 	}
 
 	@Override
-	public void playback(IMacroPlaybackContext macroPlaybackContext,
-			Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) throws MacroPlaybackException {
+	public void playback(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException {
 		for (IMacroInstruction macroInstruction : fMacroInstructions) {
 			macroInstruction.execute(macroPlaybackContext);
 		}
@@ -146,31 +178,83 @@
 
 	/**
 	 * Actually returns the bytes to be written to the disk to be loaded back later
-	 * on (the actual load and playback is later done by {@link SavedJSMacro}).
+	 * on (the actual load and playback is later done by {@link SavedXMLMacro}).
 	 *
 	 * @return an UTF-8 encoded array of bytes which can be used to rerun the macro
 	 *         later on.
+	 * @throws IOException
+	 *             if some error happens converting the macro to XML.
 	 */
-	/* default */ byte[] toJSBytes() {
-		final StringBuilder buf = new StringBuilder(fMacroInstructions.size() * 60);
+	public byte[] toXMLBytes() throws IOException {
+		try {
+			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+			factory.setFeature("http://xml.org/sax/features/namespaces", false); //$NON-NLS-1$
+			factory.setFeature("http://xml.org/sax/features/validation", false); //$NON-NLS-1$
+			factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); //$NON-NLS-1$
+			factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); //$NON-NLS-1$
 
-		buf.append("// Macro generated by the Eclipse macro record engine.\n"); //$NON-NLS-1$
-		buf.append("// The runMacro() function will be later run by the macro engine.\n"); //$NON-NLS-1$
-		buf.append("function runMacro(){\n"); //$NON-NLS-1$
+			DocumentBuilder documentBuilder = factory.newDocumentBuilder();
+			Document document = documentBuilder.newDocument();
+			Element root = document.createElement(XML_MACRO_TAG);
 
-		for (IMacroInstruction macroInstruction : fMacroInstructions) {
-			Map<String, String> map = macroInstruction.toMap();
-			Assert.isNotNull(map);
+			for (IMacroInstruction macroInstruction : fMacroInstructions) {
+				Map<String, String> map = macroInstruction.toMap();
+				Iterator<Entry<String, String>> iterator = map.entrySet().iterator();
 
-			buf.append("    runMacroInstruction("); //$NON-NLS-1$
-			buf.append(JSONHelper.quote(macroInstruction.getId()));
-			buf.append(", "); //$NON-NLS-1$
-			buf.append(JSONHelper.toJSon(map));
-			buf.append(");\n"); //$NON-NLS-1$
+				Element instructionElement = document.createElement(XML_INSTRUCTION_TAG);
+				instructionElement.setAttribute(XML_ID_ATTRIBUTE, macroInstruction.getId());
+				Element instructionDefinition = document.createElement(XML_DEFINITION_TAG);
+				while (iterator.hasNext()) {
+					Entry<String, String> entry = iterator.next();
+
+					instructionDefinition
+							.appendChild(setTextContent(document.createElement(XML_KEY_TAG), entry.getKey()));
+					instructionDefinition
+							.appendChild(setTextContent(document.createElement(XML_VALUE_TAG), entry.getValue()));
+				}
+				instructionElement.appendChild(instructionDefinition);
+				root.appendChild(instructionElement);
+			}
+			document.appendChild(root);
+
+			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+			Transformer transformer = TransformerFactory.newInstance().newTransformer();
+			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
+			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
+			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
+			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //$NON-NLS-1$//$NON-NLS-2$
+
+			DOMSource source = new DOMSource(document);
+			StreamResult outputTarget = new StreamResult(outputStream);
+			transformer.transform(source, outputTarget);
+			return outputStream.toByteArray();
+		} catch (DOMException | IllegalArgumentException | ParserConfigurationException
+				| TransformerFactoryConfigurationError | TransformerException e) {
+			throw new IOException("Error converting macro to XML.", e); //$NON-NLS-1$
 		}
-		buf.append("}\n"); //$NON-NLS-1$
+	}
 
-		return buf.toString().getBytes(StandardCharsets.UTF_8);
+	private Element setTextContent(Element element, String str) {
+		if (isAsciiPrintable(str)) {
+			element.setTextContent(str);
+		} else {
+			element.setTextContent(new String(Base64.getEncoder().encode(str.getBytes(StandardCharsets.UTF_8)),
+					StandardCharsets.UTF_8));
+			element.setAttribute(XML_BASE64_ATTRIBUTE, XML_BASE64_ATTRIBUTE_TRUE);
+		}
+		return element;
+	}
+
+	private static boolean isAsciiPrintable(String str) {
+		int length = str.length();
+		for (int i = 0; i < length; i++) {
+			char c = str.charAt(i);
+			if (!(c >= 32 && c < 127)) {
+				return false;
+			}
+		}
+		return true;
 	}
 
 	/**
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/IMacro.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/IMacro.java
index c76b120..22059ad 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/IMacro.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/IMacro.java
@@ -10,8 +10,6 @@
  *******************************************************************************/
 package org.eclipse.e4.core.macros.internal;
 
-import java.util.Map;
-import org.eclipse.e4.core.macros.IMacroInstructionFactory;
 import org.eclipse.e4.core.macros.IMacroPlaybackContext;
 import org.eclipse.e4.core.macros.MacroPlaybackException;
 
@@ -30,12 +28,8 @@
 	 *
 	 * @param macroPlaybackContext
 	 *            the context to playback the macro.
-	 * @param macroInstructionIdToFactory
-	 *            a map pointing from the macro instruction id to the factory used
-	 *            to create the related macro instruction.
 	 * @throws MacroPlaybackException
 	 *             if there was some error running the macro.
 	 */
-	void playback(IMacroPlaybackContext macroPlaybackContext,
-			Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) throws MacroPlaybackException;
+	void playback(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException;
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/JSONHelper.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/JSONHelper.java
deleted file mode 100644
index cbe44ed..0000000
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/JSONHelper.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Fabio Zadrozny and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
- *******************************************************************************/
-package org.eclipse.e4.core.macros.internal;
-
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/**
- * Helper to convert a map to a JSON without adding new dependencies.
- */
-public class JSONHelper {
-
-	/**
-	 * Quotes contents of the strings so that a given string can be saved as a valid
-	 * JSON string.
-	 *
-	 * @param string
-	 *            the string to be quoted.
-	 * @return a string where the values of the input string are quoted so that it
-	 *         forms a valid JSON string.
-	 */
-	public static String quote(String string) {
-		int len = string.length();
-		if (len == 0) {
-			return "\"\""; //$NON-NLS-1$
-		}
-
-		StringBuilder sb = new StringBuilder(len + 4);
-		sb.append('"');
-
-		for (int i = 0; i < len; i += 1) {
-			char c = string.charAt(i);
-			switch (c) {
-			case '"':
-			case '\\':
-			case '/':
-				sb.append('\\');
-				sb.append(c);
-				break;
-
-			case '\b':
-				sb.append("\\b"); //$NON-NLS-1$
-				break;
-
-			case '\f':
-				sb.append("\\f"); //$NON-NLS-1$
-				break;
-
-			case '\n':
-				sb.append("\\n"); //$NON-NLS-1$
-				break;
-
-			case '\r':
-				sb.append("\\r"); //$NON-NLS-1$
-				break;
-
-			case '\t':
-				sb.append("\\t"); //$NON-NLS-1$
-				break;
-
-			default:
-				if (c < ' ') {
-					String t = "000" + Integer.toHexString(c); //$NON-NLS-1$
-					sb.append("\\u" + t.substring(t.length() - 4)); //$NON-NLS-1$
-				} else {
-					sb.append(c);
-				}
-			}
-		}
-		sb.append('"');
-		return sb.toString();
-	}
-
-	/**
-	 * Provides a JSON representation of the passed map.
-	 *
-	 * @param map
-	 *            a map to be converted to a JSON string.
-	 * @return a JSON string with the contents of the passed map.
-	 */
-	public static String toJSon(Map<String, String> map) {
-		Iterator<Entry<String, String>> iterator = map.entrySet().iterator();
-		final StringBuilder buf = new StringBuilder("{"); //$NON-NLS-1$
-
-		while (iterator.hasNext()) {
-			if (buf.length() > 1) {
-				buf.append(", "); //$NON-NLS-1$
-			}
-			Entry<String, String> entry = iterator.next();
-			buf.append(quote(entry.getKey()));
-			buf.append(": "); //$NON-NLS-1$
-			buf.append(quote(entry.getValue()));
-		}
-		buf.append('}');
-		return buf.toString();
-	}
-}
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroManager.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroManager.java
index d6f962f..afc178a 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroManager.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroManager.java
@@ -25,6 +25,7 @@
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.ListenerList;
 import org.eclipse.core.runtime.SafeRunner;
 import org.eclipse.e4.core.macros.Activator;
@@ -45,7 +46,7 @@
  */
 public class MacroManager {
 
-	private static final String JS_EXT = ".js"; //$NON-NLS-1$
+	private static final String XML_EXT = ".xml"; //$NON-NLS-1$
 
 	private static final String TEMP_MACRO_PREFIX = "temp_macro_"; //$NON-NLS-1$
 
@@ -85,8 +86,8 @@
 	private File[] fMacrosDirectories;
 
 	/**
-	 * Holds the macro currently being recorded (if we're in record mode). If not in
-	 * record mode, should be null.
+	 * Holds the macro currently being recorded (if we are in record mode). If
+	 * not in record mode, should be {@code null}.
 	 */
 	private ComposableMacro fMacroBeingRecorded;
 
@@ -96,11 +97,16 @@
 	private IMacro fLastMacro;
 
 	/**
-	 * Flag indicating whether we're playing back a macro.
+	 * Flag indicating whether we are playing back a macro.
 	 */
 	private boolean fIsPlayingBack = false;
 
 	/**
+	 * Flag indicating whether we're recording a macro.
+	 */
+	private boolean fIsRecording = false;
+
+	/**
 	 * State to be used when recording macro.
 	 */
 	private IMacroRecordContext fMacroRecordContext;
@@ -149,7 +155,7 @@
 	 * @return whether a macro is currently being recorded.
 	 */
 	public boolean isRecording() {
-		return fMacroBeingRecorded != null;
+		return fIsRecording;
 	}
 
 	/**
@@ -223,16 +229,16 @@
 
 	private static final class MacroRecordContext implements IMacroRecordContext {
 
-		private final Map<Object, Object> ctx = new HashMap<>();
+		private final Map<Object, Object> fContext = new HashMap<>();
 
 		@Override
 		public Object get(String key) {
-			return ctx.get(key);
+			return fContext.get(key);
 		}
 
 		@Override
 		public void set(String key, Object value) {
-			ctx.put(key, value);
+			fContext.put(key, value);
 		}
 	}
 
@@ -254,6 +260,7 @@
 		}
 		if (fMacroBeingRecorded == null) {
 			// Start recording
+			fIsRecording = true;
 			fMacroRecordContext = new MacroRecordContext();
 			fMacroBeingRecorded = new ComposableMacro(macroInstructionIdToFactory);
 			for (IMacroStateListener listener : fStateListeners) {
@@ -275,6 +282,10 @@
 	 */
 	private void stopRecording(final EMacroService macroService) {
 		try {
+			fIsRecording = false;
+			// Notify before saving macro (but after the flag to know whether
+			// we're recording is set).
+			notifyMacroStateChange(macroService, StateChange.RECORD_FINISHED);
 			fMacroBeingRecorded.clearCachedInfo();
 			if (fMacroBeingRecorded.getLength() > 0) {
 				// No point in saving an empty macro.
@@ -283,9 +294,6 @@
 			}
 		} finally {
 			fMacroBeingRecorded = null;
-			// Notify only after setting fMacroBeingRecorded to null (which will
-			// make isRecording return false on the notification);
-			notifyMacroStateChange(macroService, StateChange.RECORD_FINISHED);
 			fMacroRecordContext = null;
 		}
 	}
@@ -319,16 +327,16 @@
 		// temporary macros.
 		File macroDirectory = fMacrosDirectories[0];
 		if (!macroDirectory.isDirectory()) {
-			Activator.log(new RuntimeException(
-					String.format("Unable to save macro. Expected: %s to be a directory.", macroDirectory))); //$NON-NLS-1$
+			Activator.log(IStatus.ERROR,
+					String.format("Unable to save macro. Expected: %s to be a directory.", macroDirectory)); //$NON-NLS-1$
 			return;
 		}
 
 		List<StoredMacroReference> storedMacroReferences = listTemporaryMacroReferences(macroDirectory);
 
 		try {
-			Path tempFile = Files.createTempFile(Paths.get(macroDirectory.toURI()), TEMP_MACRO_PREFIX, JS_EXT);
-			Files.write(tempFile, macro.toJSBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
+			Path tempFile = Files.createTempFile(Paths.get(macroDirectory.toURI()), TEMP_MACRO_PREFIX, XML_EXT);
+			Files.write(tempFile, macro.toXMLBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
 					StandardOpenOption.TRUNCATE_EXISTING);
 		} catch (IOException e) {
 			Activator.log(e);
@@ -369,7 +377,7 @@
 					@Override
 					public boolean accept(Path entry) {
 						String name = entry.getFileName().toString().toLowerCase();
-						return name.startsWith(TEMP_MACRO_PREFIX) && name.endsWith(JS_EXT);
+						return name.startsWith(TEMP_MACRO_PREFIX) && name.endsWith(XML_EXT);
 					}
 				})) {
 			for (Path p : directoryStream) {
@@ -421,14 +429,10 @@
 	 * @param macroPlaybackContext
 	 *            a context to be used to playback the macro (passed to the macro to
 	 *            be played back).
-	 * @param macroInstructionIdToFactory
-	 *            a map pointing from the macro instruction id to the factory used
-	 *            to create the related macro instruction.
 	 * @throws MacroPlaybackException
 	 *             if some error happens when running the macro.
 	 */
-	public void playbackLastMacro(EMacroService macroService, final IMacroPlaybackContext macroPlaybackContext,
-			final Map<String, IMacroInstructionFactory> macroInstructionIdToFactory)
+	public void playbackLastMacro(EMacroService macroService, final IMacroPlaybackContext macroPlaybackContext)
 			throws MacroPlaybackException {
 		if (fLastMacro != null && !fIsPlayingBack) {
 			// Note that we can play back while recording, but we can't change
@@ -441,7 +445,7 @@
 				}
 
 				if (notifyMacroStateChange(macroService, StateChange.PLAYBACK_STARTED)) {
-					fLastMacro.playback(macroPlaybackContext, macroInstructionIdToFactory);
+					fLastMacro.playback(macroPlaybackContext);
 				}
 			} finally {
 				fIsPlayingBack = false;
@@ -496,12 +500,12 @@
 			if (macroDirectory.isDirectory()) {
 				List<StoredMacroReference> storedMacroReferences = listTemporaryMacroReferences(macroDirectory);
 				if (storedMacroReferences.size() > 0) {
-					fLastMacro = new SavedJSMacro(storedMacroReferences.get(0).fPath.toFile());
+					fLastMacro = new SavedXMLMacro(storedMacroReferences.get(0).fPath.toFile());
 					return; // Load the last from the first directory (others aren't used for the last
 							// macro).
 				}
 			} else {
-				Activator.log(new RuntimeException(String.format("Expected: %s to be a directory.", macroDirectory))); //$NON-NLS-1$
+				Activator.log(IStatus.ERROR, String.format("Expected: %s to be a directory.", macroDirectory)); //$NON-NLS-1$
 			}
 		}
 	}
@@ -530,22 +534,22 @@
 	}
 
 	/**
-	 * Provides the macro record context or null if the macro engine is not
-	 * recording.
+	 * Provides the macro record context or {@code null} if the macro engine is
+	 * not recording.
 	 *
-	 * @return the macro record context created when macro record started or null if
-	 *         it is not currently recording.
+	 * @return the macro record context created when macro record started or
+	 *         {@code null} if it is not currently recording.
 	 */
 	public IMacroRecordContext getMacroRecordContext() {
 		return fMacroRecordContext;
 	}
 
 	/**
-	 * Provides the macro playback context or null if the macro engine is not
-	 * playing back.
+	 * Provides the macro playback context or {@code null} if the macro engine
+	 * is not playing back.
 	 *
 	 * @return the macro playback context created when macro playback started or
-	 *         null if it is not currently recording.
+	 *         {@code null} if it is not currently recording.
 	 */
 	public IMacroPlaybackContext getMacroPlaybackContext() {
 		return fMacroPlaybackContext;
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroPlaybackContextImpl.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroPlaybackContextImpl.java
index 885eda2..92603d4 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroPlaybackContextImpl.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroPlaybackContextImpl.java
@@ -12,6 +12,8 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.e4.core.macros.IMacroInstruction;
+import org.eclipse.e4.core.macros.IMacroInstructionFactory;
 import org.eclipse.e4.core.macros.IMacroPlaybackContext;
 
 /**
@@ -19,15 +21,40 @@
  */
 public class MacroPlaybackContextImpl implements IMacroPlaybackContext {
 
-	private final Map<Object, Object> ctx = new HashMap<>();
+	private final Map<Object, Object> fContext = new HashMap<>();
+
+	private Map<String, IMacroInstructionFactory> fMacroInstructionIdToFactory;
+
+	/**
+	 * @param macroInstructionIdToFactory
+	 *            a map pointing from the macro instruction id to the factory used
+	 *            to create the related macro instruction.
+	 */
+	public MacroPlaybackContextImpl(Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) {
+		fMacroInstructionIdToFactory = macroInstructionIdToFactory;
+	}
 
 	@Override
 	public Object get(String key) {
-		return ctx.get(key);
+		return fContext.get(key);
 	}
 
 	@Override
 	public void set(String key, Object value) {
-		ctx.put(key, value);
+		fContext.put(key, value);
+	}
+
+	@Override
+	public void runMacroInstruction(String macroInstructionId, Map<String, String> macroInstructionParameters)
+			throws Exception {
+		IMacroInstructionFactory macroFactory = fMacroInstructionIdToFactory.get(macroInstructionId);
+		if (macroFactory == null) {
+			throw new IllegalStateException("Unable to find IMacroInstructionFactory for macro instruction: " //$NON-NLS-1$
+					+ macroInstructionId);
+		}
+
+		IMacroInstruction macroInstruction = macroFactory.create(macroInstructionParameters);
+		macroInstruction.execute(this);
+
 	}
 }
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceCreationFunction.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceCreationFunction.java
index da1fd60..8d2fdfa 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceCreationFunction.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceCreationFunction.java
@@ -17,7 +17,7 @@
 
 /**
  * Creates a
- * {@link org.eclipse.e4.core.macros.internal.MacroServiceImplementation} (to be
+ * {@link org.eclipse.e4.core.macros.internal.MacroServiceImpl} (to be
  * bound to EMacroService).
  *
  * @note internal API: users should generally just get the EMacroService as a
@@ -31,7 +31,7 @@
 	@Override
 	public Object compute(IEclipseContext context, String contextKey) {
 		if (fService == null) {
-			fService = ContextInjectionFactory.make(MacroServiceImplementation.class, context);
+			fService = ContextInjectionFactory.make(MacroServiceImpl.class, context);
 		}
 		return fService;
 	}
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImplementation.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImpl.java
similarity index 95%
rename from bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImplementation.java
rename to bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImpl.java
index 1df05f7..d7a7f74 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImplementation.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/MacroServiceImpl.java
@@ -22,6 +22,7 @@
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IExtensionRegistry;
 import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
 import org.eclipse.e4.core.contexts.ContextInjectionFactory;
 import org.eclipse.e4.core.contexts.IEclipseContext;
 import org.eclipse.e4.core.macros.Activator;
@@ -40,7 +41,7 @@
  * things to an internal MacroManager instance and sets it up properly, dealing
  * with the eclipse context and extension points).
  */
-public class MacroServiceImplementation implements EMacroService {
+public class MacroServiceImpl implements EMacroService {
 	private static final String MACRO_COMMAND_HANDLING_EXTENSION_POINT = "org.eclipse.e4.core.macros.commandHandling"; //$NON-NLS-1$
 	private static final String MACRO_COMMAND_HANDLING_ELEMENT = "command"; //$NON-NLS-1$
 	private static final String MACRO_COMMAND_HANDLING_ID = "id"; //$NON-NLS-1$
@@ -75,7 +76,7 @@
 	private Predicate<IConfigurationElement> fFilterMacroListeners;
 
 	@Inject
-	public MacroServiceImplementation(IEclipseContext eclipseContext, IExtensionRegistry extensionRegistry) {
+	public MacroServiceImpl(IEclipseContext eclipseContext, IExtensionRegistry extensionRegistry) {
 		Assert.isNotNull(eclipseContext);
 		Assert.isNotNull(extensionRegistry);
 		fEclipseContext = eclipseContext;
@@ -149,8 +150,8 @@
 						Activator.log(e);
 					}
 				} else {
-					Activator.log(new RuntimeException(
-							"Wrong definition for extension: " + MACRO_LISTENERS_EXTENSION_POINT + ": " + ce)); //$NON-NLS-1$ //$NON-NLS-2$
+					Activator.log(IStatus.ERROR,
+							"Wrong definition for extension: " + MACRO_LISTENERS_EXTENSION_POINT + ": " + ce); //$NON-NLS-1$ //$NON-NLS-2$
 				}
 			}
 		}
@@ -182,8 +183,8 @@
 						Activator.log(e);
 					}
 				} else {
-					Activator.log(new RuntimeException("Wrong definition for extension: " //$NON-NLS-1$
-							+ MACRO_INSTRUCTION_FACTORY_EXTENSION_POINT + ": " + ce)); //$NON-NLS-1$
+					Activator.log(IStatus.WARNING, "Wrong definition for extension: " //$NON-NLS-1$
+							+ MACRO_INSTRUCTION_FACTORY_EXTENSION_POINT + ": " + ce); //$NON-NLS-1$
 				}
 			}
 			fMacroInstructionIdToFactory = Collections.unmodifiableMap(validMacroInstructionIds);
@@ -250,8 +251,8 @@
 	public void playbackLastMacro() throws MacroPlaybackException {
 		loadExtensionPointsmacroStateListeners();
 		Map<String, IMacroInstructionFactory> macroInstructionIdToFactory = getMacroInstructionIdToFactory();
-		IMacroPlaybackContext macroPlaybackContext = new MacroPlaybackContextImpl();
-		getMacroManager().playbackLastMacro(this, macroPlaybackContext, macroInstructionIdToFactory);
+		IMacroPlaybackContext macroPlaybackContext = new MacroPlaybackContextImpl(macroInstructionIdToFactory);
+		getMacroManager().playbackLastMacro(this, macroPlaybackContext);
 	}
 
 	@Override
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/Messages.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/Messages.java
index 6fb60b6..5734d1d 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/Messages.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/Messages.java
@@ -18,7 +18,6 @@
 public class Messages extends NLS {
 	private static final String BUNDLE_NAME = "org.eclipse.e4.core.macros.internal.messages"; //$NON-NLS-1$
 	public static String SavedJSMacro_MacrosEvalError;
-	public static String SavedJSMacro_NoJavaScriptEngineFound;
 	static {
 		// initialize resource bundle
 		NLS.initializeMessages(BUNDLE_NAME, Messages.class);
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedJSMacro.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedJSMacro.java
deleted file mode 100644
index 24af108..0000000
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedJSMacro.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Fabio Zadrozny and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
- *******************************************************************************/
-package org.eclipse.e4.core.macros.internal;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.text.MessageFormat;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import javax.script.Bindings;
-import javax.script.ScriptContext;
-import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
-import javax.script.SimpleScriptContext;
-import org.eclipse.e4.core.macros.IMacroInstruction;
-import org.eclipse.e4.core.macros.IMacroInstructionFactory;
-import org.eclipse.e4.core.macros.IMacroPlaybackContext;
-import org.eclipse.e4.core.macros.MacroPlaybackException;
-
-/**
- * Actually loads a macro from a JS file to be played back. Works with the
- * contents saved from {@code ComposableMacro#toJSBytes()}.
- * <p>
- * Currently the saved macro is a JavaScript file to be played back again with a
- * "runMacro" function which may have multiple "runMacroInstruction" calls to
- * run a macro instruction which was previously persisted with
- * {@link IMacroInstruction#toMap()}.
- * </p>
- */
-public class SavedJSMacro implements IMacro {
-
-	/**
-	 * The file which contains the contents of the macro.
-	 */
-	private final File fFile;
-
-	/**
-	 * Creates a macro which is backed by the contents of a javascript file.
-	 *
-	 * @param file
-	 *            the file with the contents of the macro.
-	 */
-	public SavedJSMacro(File file) {
-		fFile = file;
-	}
-
-	/**
-	 * Static method to be called when playing back a macro to run a macro
-	 * instruction.
-	 *
-	 * @param macroPlaybackContext
-	 *            the context for the macro playback.
-	 * @param macroInstructionId
-	 *            the id of the macro instruction to be executed.
-	 * @param macroInstructionParameters
-	 *            the parameters to create the macro instruction.
-	 * @param macroInstructionIdToFactory
-	 *            a map pointing from the macro instruction id to the factory
-	 *            used to create the related macro instruction.
-	 * @throws Exception
-	 *             if something happened when creating the macro instruction or
-	 *             actually executing it.
-	 */
-	@SuppressWarnings({ "rawtypes" })
-	public static void runMacroInstruction(IMacroPlaybackContext macroPlaybackContext, String macroInstructionId,
-			Object macroInstructionParameters, Map<String, IMacroInstructionFactory> macroInstructionIdToFactory)
-			throws Exception {
-		Map<String, String> stringMap = new HashMap<>();
-		Map m = (Map) macroInstructionParameters;
-		Set<Map.Entry> entrySet = m.entrySet();
-		for (Map.Entry entry : entrySet) {
-			Object key = entry.getKey();
-			Object value = entry.getValue();
-			stringMap.put(key.toString(), value.toString());
-		}
-
-		IMacroInstructionFactory macroFactory = macroInstructionIdToFactory.get(macroInstructionId);
-		if (macroFactory == null) {
-			throw new IllegalStateException(
-					"Unable to find IMacroInstructionFactory for macro instruction: " + macroInstructionId); //$NON-NLS-1$
-		}
-
-		IMacroInstruction macroInstruction = macroFactory.create(stringMap);
-		macroInstruction.execute(macroPlaybackContext);
-	}
-
-	@Override
-	public void playback(IMacroPlaybackContext macroPlaybackContext,
-			Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) throws MacroPlaybackException {
-		ScriptEngineManager manager = new ScriptEngineManager();
-		ScriptEngine engine = manager.getEngineByName("JavaScript"); //$NON-NLS-1$
-		if (engine == null) {
-			throw new MacroPlaybackException(Messages.SavedJSMacro_NoJavaScriptEngineFound);
-		}
-		SimpleScriptContext context = new SimpleScriptContext();
-		context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
-		Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE);
-
-		// Setup the default context.
-		engineScope.put("__macroPlaybackContext", macroPlaybackContext); //$NON-NLS-1$
-		engineScope.put("__macroInstructionIdToFactory", macroInstructionIdToFactory); //$NON-NLS-1$
-
-		try {
-			engine.eval("" + //$NON-NLS-1$
-					"__macro = Java.type('org.eclipse.e4.core.macros.internal.SavedJSMacro');\n" //$NON-NLS-1$
-					+ "function runMacroInstruction(macroMacroInstructionId, macroInstructionParameters){" //$NON-NLS-1$
-					+ "    __macro.runMacroInstruction(__macroPlaybackContext, macroMacroInstructionId, macroInstructionParameters, __macroInstructionIdToFactory);" //$NON-NLS-1$
-					+ "}" //$NON-NLS-1$
-					+ "", context); //$NON-NLS-1$
-
-			// The contents to execute are actually built at:
-			// org.eclipse.e4.core.macros.internal.ComposableMacro.toJSBytes()
-			try (BufferedReader reader = new BufferedReader(
-					new InputStreamReader(new FileInputStream(fFile), "UTF-8"))) { //$NON-NLS-1$
-				// Let any exception running it go through.
-				// It should define a runMacro() method which we can run later on.
-				engine.eval(reader, context);
-
-				// Actually run the macro now.
-				engine.eval("runMacro();", context); //$NON-NLS-1$
-			}
-		} catch (Exception e) {
-			throw new MacroPlaybackException(
-					MessageFormat.format(Messages.SavedJSMacro_MacrosEvalError, e.getMessage()), e);
-		}
-	}
-}
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedXMLMacro.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedXMLMacro.java
new file mode 100644
index 0000000..16d91e0
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/SavedXMLMacro.java
@@ -0,0 +1,147 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Fabio Zadrozny and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
+ *******************************************************************************/
+package org.eclipse.e4.core.macros.internal;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.e4.core.macros.IMacroPlaybackContext;
+import org.eclipse.e4.core.macros.MacroPlaybackException;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * Actually loads a macro from a XML file to be played back. Works with the
+ * contents saved from {@code ComposableMacro#toXMLBytes()}.
+ */
+public class SavedXMLMacro implements IMacro {
+
+	/**
+	 * The file which contains the contents of the macro.
+	 */
+	private final File fFile;
+
+	/**
+	 * Creates a macro which is backed by the contents of a XML file.
+	 *
+	 * @param file
+	 *            the file with the contents of the macro.
+	 */
+	public SavedXMLMacro(File file) {
+		fFile = file;
+	}
+
+	@Override
+	public void playback(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException {
+		try {
+			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+			factory.setFeature("http://xml.org/sax/features/namespaces", false); //$NON-NLS-1$
+			factory.setFeature("http://xml.org/sax/features/validation", false); //$NON-NLS-1$
+			factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); //$NON-NLS-1$
+			factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); //$NON-NLS-1$
+			// The contents to execute are actually built at:
+			// org.eclipse.e4.core.macros.internal.ComposableMacro.toXMLBytes()
+			try (BufferedReader reader = new BufferedReader(
+					new InputStreamReader(new FileInputStream(fFile), "UTF-8"))) { //$NON-NLS-1$
+				DocumentBuilder parser = factory.newDocumentBuilder();
+				Document document = parser.parse(new InputSource(reader));
+				NodeList childNodes = document.getChildNodes();
+
+				int length = childNodes.getLength();
+				if (length != 1) {
+					throw new MacroPlaybackException(MessageFormat.format(Messages.SavedJSMacro_MacrosEvalError,
+							"Expected only one root in the macro XML.")); //$NON-NLS-1$
+				}
+				Node macroRoot = childNodes.item(0);
+				for (Node macroInstructionNode : getNodesAsCollection(macroRoot, ComposableMacro.XML_INSTRUCTION_TAG)) {
+					NamedNodeMap macroInstructionNodeAttributes = macroInstructionNode.getAttributes();
+					if (macroInstructionNodeAttributes == null) {
+						continue;
+					}
+					Node idAttribute = macroInstructionNodeAttributes.getNamedItem(ComposableMacro.XML_ID_ATTRIBUTE);
+					if (idAttribute == null) {
+						continue;
+					}
+					String macroInstructionId = idAttribute.getNodeValue();
+					for (Node definitionNode : getNodesAsCollection(macroInstructionNode,
+							ComposableMacro.XML_DEFINITION_TAG)) {
+						Map<String, String> macroInstructionParameters = new HashMap<>();
+						String key = null;
+						String val = null;
+						for (Node keyOrVal : getNodesAsCollection(definitionNode, ComposableMacro.XML_KEY_TAG,
+								ComposableMacro.XML_VALUE_TAG)) {
+							if (keyOrVal.getNodeName().equals(ComposableMacro.XML_KEY_TAG)) {
+								key = getTextContent(keyOrVal);
+							} else {
+								val = getTextContent(keyOrVal);
+								Assert.isNotNull(key, "XML malformed, received value without key."); //$NON-NLS-1$
+								macroInstructionParameters.put(key, val);
+								key = null;
+								val = null;
+							}
+						}
+						macroPlaybackContext.runMacroInstruction(macroInstructionId, macroInstructionParameters);
+					}
+				}
+			}
+		} catch (Exception e) {
+			throw new MacroPlaybackException(
+					MessageFormat.format(Messages.SavedJSMacro_MacrosEvalError, e.getMessage()), e);
+		}
+	}
+
+	private String getTextContent(Node keyOrVal) {
+		String str = keyOrVal.getTextContent();
+		NamedNodeMap attributes = keyOrVal.getAttributes();
+		if (attributes != null) {
+			Node base64Attribute = attributes.getNamedItem(ComposableMacro.XML_BASE64_ATTRIBUTE);
+			if (base64Attribute != null) {
+				if (ComposableMacro.XML_BASE64_ATTRIBUTE_TRUE.equals(base64Attribute.getNodeValue())) {
+					str = new String(Base64.getDecoder().decode(str.getBytes(StandardCharsets.UTF_8)),
+							StandardCharsets.UTF_8);
+				}
+			}
+		}
+		return str;
+	}
+
+	private Collection<Node> getNodesAsCollection(final Node node, String... filterNodeName) {
+		NodeList childNodes = node.getChildNodes();
+		int length = childNodes.getLength();
+		Set<String> filterNodeNames = new HashSet<>(Arrays.asList(filterNodeName));
+		List<Node> result = new ArrayList<>(length);
+		for (int i = 0; i < length; i++) {
+			Node item = childNodes.item(i);
+			if (filterNodeName.length == 0 || filterNodeNames.contains(item.getNodeName())) {
+				result.add(item);
+			}
+		}
+		return result;
+	}
+}
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/messages.properties b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/messages.properties
index dd8d464..3677ab5 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/messages.properties
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/internal/messages.properties
@@ -1,2 +1 @@
 SavedJSMacro_MacrosEvalError=Error when evaluating macro:\n\n{0}
-SavedJSMacro_NoJavaScriptEngineFound=No javascript engine available
diff --git a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/package-info.java b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/package-info.java
index 52a1127..ce4b469 100644
--- a/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/package-info.java
+++ b/bundles/org.eclipse.e4.core.macros/src/org/eclipse/e4/core/macros/package-info.java
@@ -118,7 +118,7 @@
  *
  * <p>
  * This gives an example where some state must be set for both macro and record
- * mode (i.e.: disabling code-completion) and in the record mode it also has to
+ * mode (i.e., disabling code-completion) and in the record mode it also has to
  * record keypresses to be played back later on.
  * </p>
  *
@@ -254,4 +254,4 @@
  * </p>
  *
  *
- **/
\ No newline at end of file
+ **/
diff --git a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/Activator.java b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/Activator.java
index 85d62cc..ca13b48 100644
--- a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/Activator.java
+++ b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/Activator.java
@@ -39,27 +39,45 @@
 		return plugin;
 	}
 
+	/**
+	 * Logs a message.
+	 *
+	 * @param severity
+	 *            the {@link IStatus} severity
+	 * @param message
+	 *            the status message
+	 */
+	public static void log(int severity, String message) {
+		log(new Status(severity, plugin.getBundle().getSymbolicName(), message));
+	}
 
 	/**
 	 * Logs an exception.
 	 *
 	 * @param exception
-	 *            the exception to be logged.
+	 *            the exception to be logged
 	 */
 	public static void log(Throwable exception) {
-		try {
-			if (plugin != null) {
-				plugin.getLog().log(new Status(IStatus.ERROR, plugin.getBundle().getSymbolicName(),
-						exception.getMessage(), exception));
-			} else {
-				// The plugin is not available. Just print to stderr.
-				exception.printStackTrace();
-			}
-		} catch (Exception e) {
-			// Print the original error if something happened, not the one
-			// related to the log not working.
-			exception.printStackTrace();
-		}
+		log(new Status(IStatus.ERROR, plugin.getBundle().getSymbolicName(), exception.getMessage(), exception));
 	}
 
+	/**
+	 * Logs a status.
+	 *
+	 * @param status
+	 *            the status to be logged
+	 */
+	public static void log(IStatus status) {
+		try {
+			if (plugin != null) {
+				plugin.getLog().log(status);
+			} else {
+				// The plugin is not available. Just print to stderr.
+				System.err.println(status);
+			}
+		} catch (Exception e) {
+			// Print the original status if something happened
+			System.err.println(status);
+		}
+	}
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/EditorUtils.java b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/EditorUtils.java
index fa3664d..373352b 100644
--- a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/EditorUtils.java
+++ b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/EditorUtils.java
@@ -36,14 +36,15 @@
 	private final static String TARGET_EDITOR_PART = "TARGET_EDITOR_PART"; //$NON-NLS-1$
 
 	/**
-	 * Provides the {@link StyledText} from the passed editor or {@code null} if not
-	 * available.
+	 * Provides the {@link StyledText} from the passed editor or {@code null} if
+	 * not available.
 	 *
 	 * @param editor
 	 *            the editor from where the {@link StyledText} should be gotten.
 	 *
-	 * @return the {@link StyledText} related to the current editor or null if it is
-	 *         not available (i.e.: if the editor passed is not a text editor).
+	 * @return the {@link StyledText} related to the current editor or
+	 *         {@code null} if it is not available (i.e., if the editor passed
+	 *         is not a text editor).
 	 */
 	public static StyledText getActiveEditorStyledText(IEditorPart editor) {
 		if (editor == null) {
diff --git a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/UserNotifications.java b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/UserNotifications.java
index a60170b..82ca130 100644
--- a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/UserNotifications.java
+++ b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/UserNotifications.java
@@ -52,7 +52,7 @@
 	 * Sets a given message to be shown to the user.
 	 *
 	 * @param message
-	 *            the message to be shown or null to clear it.
+	 *            the message to be shown or {@code null} to clear it.
 	 */
 	public void setMessage(String message) {
 		IStatusLineManager statusLineManager = getStatusLineManager();
@@ -69,7 +69,7 @@
 	 * Shows some error message related to the macro to the user.
 	 *
 	 * @param message
-	 *            the error message to be shown (cannot be null).
+	 *            the error message to be shown (should never be {@code null}).
 	 */
 	public void showErrorMessage(String message) {
 		Activator plugin = Activator.getDefault();
@@ -97,8 +97,8 @@
 	}
 
 	/**
-	 * Provides the status line manager to be used for notifications or null if it
-	 * is not available.
+	 * Provides the status line manager to be used for notifications or {@code null}
+	 * if it is not available.
 	 *
 	 * @return the available status line manager for the current editor.
 	 */
@@ -148,8 +148,8 @@
 	}
 
 	private void openWarningWithIgnoreToggle(String title, String message, String key) {
+		Activator.log(new Status(IStatus.WARNING, Activator.getDefault().getBundle().getSymbolicName(), message));
 		if (shell == null) {
-			System.err.println(message);
 			return;
 		}
 
diff --git a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/actions/MacroPlaybackAction.java b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/actions/MacroPlaybackAction.java
index 78fd113..ee1014e 100644
--- a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/actions/MacroPlaybackAction.java
+++ b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/actions/MacroPlaybackAction.java
@@ -12,13 +12,16 @@
 
 import org.eclipse.core.commands.AbstractHandler;
 import org.eclipse.core.commands.ExecutionEvent;
-import org.eclipse.e4.core.macros.Activator;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
 import org.eclipse.e4.core.macros.EMacroService;
 import org.eclipse.e4.core.macros.MacroPlaybackException;
-import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.e4.ui.macros.Activator;
 import org.eclipse.ui.IWorkbenchWindow;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.handlers.HandlerUtil;
+import org.eclipse.ui.statushandlers.StatusAdapter;
+import org.eclipse.ui.statushandlers.StatusManager;
 
 /**
  * Activates the playback of the last macro.
@@ -30,17 +33,12 @@
 		try {
 			PlatformUI.getWorkbench().getService(EMacroService.class).playbackLastMacro();
 		} catch (MacroPlaybackException e) {
-			Activator.log(e);
 			IWorkbenchWindow activeWorkbenchWindow = HandlerUtil.getActiveWorkbenchWindow(event);
 			if (activeWorkbenchWindow != null) {
-				// When it comes from evaluating JS it adds
-				// "org.eclipse.e4.core.macros.MacroPlaybackException: "
-				// which isn't really interesting to the user (so, just remove that part).
-				String msg = e.getMessage().replace("org.eclipse.e4.core.macros.MacroPlaybackException: ", ""); //$NON-NLS-1$//$NON-NLS-2$
-				MessageDialog.openError(activeWorkbenchWindow.getShell(),
-						Messages.MacroPlaybackAction_ErrorRunningMacro, msg);
+				StatusAdapter status = new StatusAdapter(new Status(IStatus.ERROR,
+						Activator.getDefault().getBundle().getSymbolicName(), e.getMessage(), e));
+				StatusManager.getManager().handle(status, StatusManager.SHOW | StatusManager.LOG);
 			}
-
 		}
 		return null;
 	}
diff --git a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/keybindings/CommandManagerExecutionListener.java b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/keybindings/CommandManagerExecutionListener.java
index 8920079..72b2c42 100644
--- a/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/keybindings/CommandManagerExecutionListener.java
+++ b/bundles/org.eclipse.e4.ui.macros/src/org/eclipse/e4/ui/macros/internal/keybindings/CommandManagerExecutionListener.java
@@ -114,7 +114,7 @@
 	public void postExecuteSuccess(String commandId, Object returnValue) {
 		ParameterizedCommandAndTrigger commandAndTrigger = popCommand(commandId);
 		if (commandAndTrigger == null) {
-			// Can happen if we didn't get the preExecute (i.e.: the toggle
+			// Can happen if we didn't get the preExecute (i.e., the toggle
 			// macro record is executed and post executed only (the pre execute
 			// is skipped because recording still wasn't in place).
 			//
@@ -209,7 +209,7 @@
 			if (EditorUtils.getActiveEditorStyledText(fEclipseContext) != EditorUtils
 					.getTargetStyledText(fMacroService.getMacroRecordContext())) {
 				// Note: it previously checked swtEvent.widget, but sometimes the event was
-				// generated from the wrong control (i.e.: opening a new editor and doing
+				// generated from the wrong control (i.e., opening a new editor and doing
 				// some action sometimes had the widget from a different editor).
 				return false;
 			}
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/plugin.xml b/bundles/org.eclipse.ui.workbench.texteditor.macros/plugin.xml
index f2ebe0f..7408dd6 100644
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/plugin.xml
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/plugin.xml
@@ -4,7 +4,7 @@
    <extension
          point="org.eclipse.e4.core.macros.macroStateListeners">
       <macroStateListener
-            class="org.eclipse.ui.workbench.texteditor.macros.internal.EditorPartMacroInstaller">
+            class="org.eclipse.ui.workbench.texteditor.macros.internal.NotifyMacroOnlyInCurrentEditorInstaller">
       </macroStateListener>
       <macroStateListener
             class="org.eclipse.ui.workbench.texteditor.macros.internal.MacroStyledTextInstaller">
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/AbstractSWTEventMacroInstruction.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/AbstractSWTEventMacroInstruction.java
index b37e3cf..9310ea5 100644
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/AbstractSWTEventMacroInstruction.java
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/AbstractSWTEventMacroInstruction.java
@@ -18,13 +18,14 @@
 
 /**
  * Base class for a macro instruction based on events from a given type.
- *
- * Note that it doesn't store all the information on events, only character,
- * stateMask, keyCode, keyLocation and detail (and the actual type is meant to
- * be gotten from the class which overrides it or passed when needed).
- *
- * The actual fields that it stores may grow over time (and when restored, if
- * those weren't properly saved, default values should be used).
+ * <p>
+ * Note that this macro instruction records only the {@code character},
+ * {@code stateMask}, {@code keyCode}, {@code keyLocation} and {@code detail}.
+ * The actual event {@code type} is meant to be obtained from the class which
+ * overrides it or passed when needed.
+ * <p>
+ * The actual fields stored may grow over time: subclasses should be sure to
+ * provide default values if not present.
  */
 public abstract class AbstractSWTEventMacroInstruction implements IMacroInstruction {
 
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/EditorPartMacroInstaller.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/EditorPartMacroInstaller.java
deleted file mode 100644
index 17ddb4f..0000000
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/EditorPartMacroInstaller.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Fabio Zadrozny and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
- *******************************************************************************/
-package org.eclipse.ui.workbench.texteditor.macros.internal;
-
-import javax.inject.Inject;
-import org.eclipse.e4.core.contexts.IEclipseContext;
-import org.eclipse.e4.core.macros.CancelMacroException;
-import org.eclipse.e4.core.macros.EMacroService;
-import org.eclipse.e4.core.macros.IMacroRecordContext;
-import org.eclipse.e4.core.macros.IMacroStateListener;
-
-/**
- * A listener to the macro context which will enable notifications to the user
- * regarding limitations of recording only in the current editor.
- */
-public class EditorPartMacroInstaller implements IMacroStateListener {
-
-	private static final String NOTIFY_MACRO_ONLY_IN_CURRENT_EDITOR = "NOTIFY_MACRO_ONLY_IN_CURRENT_EDITOR"; //$NON-NLS-1$
-
-	@Inject
-	private IEclipseContext fEclipseContext;
-
-	@Override
-	public void macroStateChanged(EMacroService macroService, StateChange stateChange)
-			throws CancelMacroException {
-		if (stateChange == StateChange.RECORD_STARTED) {
-			IMacroRecordContext context = macroService.getMacroRecordContext();
-			NotifyMacroOnlyInCurrentEditor notifyMacroOnlyInCurrentEditor = new NotifyMacroOnlyInCurrentEditor(
-					macroService, fEclipseContext);
-			notifyMacroOnlyInCurrentEditor.checkEditorActiveForMacroRecording();
-
-			notifyMacroOnlyInCurrentEditor.install();
-			context.set(NOTIFY_MACRO_ONLY_IN_CURRENT_EDITOR, notifyMacroOnlyInCurrentEditor);
-
-		} else if (stateChange == StateChange.RECORD_FINISHED) {
-			IMacroRecordContext context = macroService.getMacroRecordContext();
-			Object object = context.get(NOTIFY_MACRO_ONLY_IN_CURRENT_EDITOR);
-			if (object instanceof NotifyMacroOnlyInCurrentEditor) {
-				NotifyMacroOnlyInCurrentEditor notifyMacroOnlyInCurrentEditor = (NotifyMacroOnlyInCurrentEditor) object;
-				notifyMacroOnlyInCurrentEditor.uninstall();
-			}
-		} else if (stateChange == StateChange.PLAYBACK_STARTED) {
-			new NotifyMacroOnlyInCurrentEditor(macroService, fEclipseContext).checkEditorActiveForMacroPlayback();
-		}
-	}
-}
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextInstaller.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextInstaller.java
index 1b0070b..740b7f7 100644
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextInstaller.java
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextInstaller.java
@@ -19,185 +19,31 @@
 import org.eclipse.e4.core.macros.IMacroRecordContext;
 import org.eclipse.e4.core.macros.IMacroStateListener;
 import org.eclipse.e4.ui.macros.internal.EditorUtils;
-import org.eclipse.jface.action.IAction;
-import org.eclipse.jface.text.ITextOperationTarget;
-import org.eclipse.jface.text.ITextOperationTargetExtension;
-import org.eclipse.jface.text.source.ISourceViewer;
 import org.eclipse.swt.custom.StyledText;
-import org.eclipse.swt.widgets.Control;
 import org.eclipse.ui.IEditorPart;
-import org.eclipse.ui.IMemento;
 import org.eclipse.ui.ISources;
-import org.eclipse.ui.XMLMemento;
-import org.eclipse.ui.texteditor.ITextEditor;
-import org.eclipse.ui.texteditor.ITextEditorActionConstants;
-import org.eclipse.ui.texteditor.ITextEditorExtension5;
-import org.eclipse.ui.texteditor.TextEditorAction;
 
 /**
- * Helper class to deal with entering/exiting macro record/playback.
+ * Used for enabling macro mode in editors as well as starting and stopping the
+ * record of keystrokes.
  */
 public class MacroStyledTextInstaller implements IMacroStateListener {
 
 	/**
 	 * Constant used to keep memento on the macro context.
 	 */
-	private static final String MACRO_STYLED_TEXT_INSTALLER_MEMENTO = "MACRO_STYLED_TEXT_INSTALLER_MEMENTO"; //$NON-NLS-1$
+	private static final String MACRO_STYLED_TEXT_HANDLER = "MACRO_STYLED_TEXT_HANDLER"; //$NON-NLS-1$
 
 	/**
 	 * Constant used to keep macro recorder on the macro context.
 	 */
 	private static final String MACRO_STYLED_TEXT_INSTALLER_MACRO_RECORDER = "MACRO_STYLED_TEXT_INSTALLER_MACRO_RECORDER"; //$NON-NLS-1$
 
-	/**
-	 * Constant used to save whether the content assist was enabled before being
-	 * disabled in disableCodeCompletion.
-	 */
-	private static final String CONTENT_ASSIST_ENABLED = "contentAssistEnabled";//$NON-NLS-1$
-
-	/**
-	 * Constant used to save whether the quick assist was enabled before being
-	 * disabled in disableCodeCompletion.
-	 */
-	private static final String QUICK_ASSIST_ENABLED = "quickAssistEnabled";//$NON-NLS-1$
-
 	@Inject
 	@Named(ISources.ACTIVE_EDITOR_NAME)
 	@Optional
 	private IEditorPart activeEditor;
 
-	/**
-	 * Re-enables the content assist based on the state of the key
-	 * {@link #CONTENT_ASSIST_ENABLED} in the passed memento.
-	 *
-	 * @param memento
-	 *            the memento where a key with {@link #CONTENT_ASSIST_ENABLED} with
-	 *            the enabled state of the content assist to be restored.
-	 */
-	private void leaveMacroMode(IMemento memento, IMacroContext context) {
-		IEditorPart editorPart = EditorUtils.getTargetEditorPart(context);
-		if (editorPart != null) {
-			ITextOperationTarget textOperationTarget = editorPart.getAdapter(ITextOperationTarget.class);
-			if (textOperationTarget instanceof ITextOperationTargetExtension) {
-				ITextOperationTargetExtension targetExtension = (ITextOperationTargetExtension) textOperationTarget;
-				if (textOperationTarget instanceof ITextOperationTargetExtension) {
-					restore(memento, targetExtension, ISourceViewer.CONTENTASSIST_PROPOSALS, CONTENT_ASSIST_ENABLED);
-					restore(memento, targetExtension, ISourceViewer.QUICK_ASSIST, QUICK_ASSIST_ENABLED);
-				}
-			}
-
-			if (editorPart instanceof ITextEditor) {
-				ITextEditor textEditor = (ITextEditor) editorPart;
-				restore(memento, textEditor, ITextEditorActionConstants.CONTENT_ASSIST);
-				restore(memento, textEditor, ITextEditorActionConstants.QUICK_ASSIST);
-				restore(memento, textEditor, ITextEditorActionConstants.BLOCK_SELECTION_MODE);
-			}
-		}
-
-	}
-
-	/**
-	 * Disables the content assist and saves the previous state on the passed
-	 * memento (note that it's only saved if it is actually disabled here).
-	 *
-	 * @param memento
-	 *            memento where the previous state should be saved, to be properly
-	 *            restored later on in {@link #leaveMacroMode(IMemento)}.
-	 */
-	private void enterMacroMode(IMemento memento, IMacroContext context) {
-		IEditorPart editorPart = EditorUtils.getTargetEditorPart(context);
-		if (editorPart instanceof ITextEditorExtension5) {
-			ITextEditorExtension5 iTextEditorExtension5 = (ITextEditorExtension5) editorPart;
-			if (iTextEditorExtension5.isBlockSelectionModeEnabled()) {
-				// Note: macro can't deal with block selections... there's nothing really
-				// inherent to not being able to work, but given:
-				// org.eclipse.jface.text.TextViewer.verifyEventInBlockSelection(VerifyEvent)
-				// and the fact that we don't generate events through the Display (because it's
-				// too error prone -- so much that it could target the wrong IDE instance for
-				// the events because it deals with system messages and not really events
-				// inside the IDE) and the fact that we can't force a new system message time
-				// for temporary events created internally, makes it really hard to work
-				// around the hack in verifyEventInBlockSelection.
-				// So, we simply disable block selection mode as well as the action which would
-				// activate it.
-
-				// Note: ideally we'd have a way to set a new time for the time returned in
-				// org.eclipse.swt.widgets.Display.getLastEventTime()
-				// -- as it is, internally the events time will be always the same because
-				// there's no API to reset it -- if possible we should reset it when we
-				// generate our internal events at:
-				// org.eclipse.ui.workbench.texteditor.macros.internal.StyledTextKeyDownMacroInstruction.execute(IMacroPlaybackContext)
-				iTextEditorExtension5.setBlockSelectionMode(false);
-			}
-		}
-
-		if (editorPart instanceof ITextEditor) {
-			ITextEditor textEditor = (ITextEditor) editorPart;
-			disable(memento, textEditor, ITextEditorActionConstants.CONTENT_ASSIST);
-			disable(memento, textEditor, ITextEditorActionConstants.QUICK_ASSIST);
-			disable(memento, textEditor, ITextEditorActionConstants.BLOCK_SELECTION_MODE);
-		}
-
-		if (editorPart != null) {
-			ITextOperationTarget textOperationTarget = editorPart.getAdapter(ITextOperationTarget.class);
-			if (textOperationTarget instanceof ITextOperationTargetExtension) {
-				ITextOperationTargetExtension targetExtension = (ITextOperationTargetExtension) textOperationTarget;
-
-				// Disable content assist and mark it to be restored later on
-				disable(memento, textOperationTarget, targetExtension, ISourceViewer.CONTENTASSIST_PROPOSALS,
-						CONTENT_ASSIST_ENABLED);
-				disable(memento, textOperationTarget, targetExtension, ISourceViewer.QUICK_ASSIST,
-						QUICK_ASSIST_ENABLED);
-			}
-		}
-	}
-
-	private void restore(IMemento memento, ITextOperationTargetExtension targetExtension, int operation,
-			String preference) {
-		Boolean contentAssistProposalsBeforMacroMode = memento.getBoolean(preference);
-		if (contentAssistProposalsBeforMacroMode != null) {
-			if ((contentAssistProposalsBeforMacroMode).booleanValue()) {
-				targetExtension.enableOperation(operation, true);
-			} else {
-				targetExtension.enableOperation(operation, false);
-			}
-		}
-	}
-
-	private void restore(IMemento memento, ITextEditor textEditor, String actionId) {
-		Boolean b = memento.getBoolean(actionId);
-		if (b != null && b) {
-			Control control = textEditor.getAdapter(Control.class);
-			if (control != null && !control.isDisposed()) {
-				// Do nothing if already disposed.
-				IAction action = textEditor.getAction(actionId);
-				if (action instanceof TextEditorAction) {
-					TextEditorAction textEditorAction = (TextEditorAction) action;
-					textEditorAction.setEditor(textEditor);
-					textEditorAction.update();
-				}
-			}
-		}
-	}
-
-	private void disable(IMemento memento, ITextOperationTarget textOperationTarget,
-			ITextOperationTargetExtension targetExtension, int operation, String preference) {
-		if (textOperationTarget.canDoOperation(operation)) {
-			memento.putBoolean(preference, true);
-			targetExtension.enableOperation(operation, false);
-		}
-	}
-
-	private void disable(IMemento memento, ITextEditor textEditor, String actionId) {
-		IAction action = textEditor.getAction(actionId);
-		if (action != null && action instanceof TextEditorAction) {
-			TextEditorAction textEditorAction = (TextEditorAction) action;
-			memento.putBoolean(actionId, true);
-			textEditorAction.setEditor(null);
-			textEditorAction.update();
-		}
-	}
-
 	@Override
 	public void macroPlaybackContextCreated(IMacroPlaybackContext context) {
 		EditorUtils.cacheTargetEditorPart(activeEditor, context);
@@ -211,26 +57,25 @@
 	}
 
 	/**
-	 * Implemented to properly deal with macro recording/playback (i.e.: the editor
+	 * Implemented to properly deal with macro recording/playback (i.e., the editor
 	 * may need to disable content assist during macro recording and it needs to
 	 * record keystrokes to be played back afterwards).
 	 */
 	@Override
 	public void macroStateChanged(EMacroService macroService, StateChange stateChange) {
-
 		if (stateChange == StateChange.RECORD_STARTED) {
 			enterMacroMode(macroService.getMacroRecordContext(), macroService.getMacroPlaybackContext());
 			enableRecording(macroService, macroService.getMacroRecordContext());
 
 		} else if (stateChange == StateChange.RECORD_FINISHED) {
-			leaveMacroMode(macroService.getMacroRecordContext(), macroService.getMacroPlaybackContext());
+			leaveMacroMode(macroService.getMacroRecordContext());
 			disableRecording(macroService.getMacroRecordContext());
 
 		} else if (stateChange == StateChange.PLAYBACK_STARTED) {
 			enterMacroMode(macroService.getMacroPlaybackContext(), macroService.getMacroRecordContext());
 
 		} else if (stateChange == StateChange.PLAYBACK_FINISHED) {
-			leaveMacroMode(macroService.getMacroPlaybackContext(), macroService.getMacroRecordContext());
+			leaveMacroMode(macroService.getMacroPlaybackContext());
 
 		}
 	}
@@ -238,30 +83,26 @@
 	private void enterMacroMode(IMacroContext context, IMacroContext otherContext) {
 		StyledText currentStyledText = EditorUtils.getTargetStyledText(context);
 		StyledText otherStyledText = EditorUtils.getTargetStyledText(otherContext);
-		if (currentStyledText == otherStyledText) {
-			return; // If they're the same in both it means we already entered macro mode in the
-					// other before.
+		if (currentStyledText == otherStyledText || currentStyledText == null) {
+			return; // If they are the same in both it means we already entered macro mode in the
+					// other before (i.e., started record then started playback).
 		}
-		Object object = context.get(MACRO_STYLED_TEXT_INSTALLER_MEMENTO);
-		if (object == null) {
-			XMLMemento mementoStateBeforeMacro = XMLMemento.createWriteRoot("AbstractTextEditorXmlMemento"); //$NON-NLS-1$
-			enterMacroMode(mementoStateBeforeMacro, context);
-			context.set(MACRO_STYLED_TEXT_INSTALLER_MEMENTO, mementoStateBeforeMacro);
+		MacroStyledTextModeHandler handler = (MacroStyledTextModeHandler) context.get(MACRO_STYLED_TEXT_HANDLER);
+		if (handler == null) {
+			handler = new MacroStyledTextModeHandler(EditorUtils.getTargetEditorPart(context));
+			handler.enterMacroMode();
+			context.set(MACRO_STYLED_TEXT_HANDLER, handler);
 		}
 	}
 
-	private void leaveMacroMode(IMacroContext context, IMacroContext otherContext) {
-		StyledText currentStyledText = EditorUtils.getTargetStyledText(context);
-		StyledText otherStyledText = EditorUtils.getTargetStyledText(otherContext);
-		if (currentStyledText == otherStyledText) {
-			return; // If they're the same in both it means we still can't exit macro mode.
-		}
-
+	private void leaveMacroMode(IMacroContext context) {
 		// Restores content assist if it was disabled (based on the memento)
-		Object object = context.get(MACRO_STYLED_TEXT_INSTALLER_MEMENTO);
-		if (object instanceof XMLMemento) {
-			XMLMemento mementoStateBeforeMacro = (XMLMemento) object;
-			leaveMacroMode(mementoStateBeforeMacro, context);
+		// Note that it may be null if it was not created in this context
+		// (i.e.: started record, started playback, end playback).
+		Object object = context.get(MACRO_STYLED_TEXT_HANDLER);
+		if (object != null) {
+			MacroStyledTextModeHandler macroStyledTextHandler = (MacroStyledTextModeHandler) object;
+			macroStyledTextHandler.leaveMacroMode();
 		}
 	}
 
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextModeHandler.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextModeHandler.java
new file mode 100644
index 0000000..6231eca
--- /dev/null
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/MacroStyledTextModeHandler.java
@@ -0,0 +1,174 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Fabio Zadrozny and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
+ *******************************************************************************/
+package org.eclipse.ui.workbench.texteditor.macros.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.ITextOperationTarget;
+import org.eclipse.jface.text.ITextOperationTargetExtension;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorActionConstants;
+import org.eclipse.ui.texteditor.ITextEditorExtension5;
+import org.eclipse.ui.texteditor.TextEditorAction;
+
+/**
+ * Used to disable (enter macro mode) and re-enable (leave macro mode) features
+ * in an editor which are not compatible with macro record or playback.
+ */
+public class MacroStyledTextModeHandler {
+
+	/**
+	 * Constant used to save whether the content assist was enabled before being
+	 * disabled in disableCodeCompletion.
+	 */
+	private static final String CONTENT_ASSIST_ENABLED = "contentAssistEnabled";//$NON-NLS-1$
+
+	/**
+	 * Constant used to save whether the quick assist was enabled before being
+	 * disabled in disableCodeCompletion.
+	 */
+	private static final String QUICK_ASSIST_ENABLED = "quickAssistEnabled";//$NON-NLS-1$
+
+	private IEditorPart fEditorPart;
+
+	private Map<String, Boolean> fMemento = new HashMap<>();
+
+	/**
+	 * @param editorPart
+	 *            the editor where macro record or playback will take place.
+	 */
+	public MacroStyledTextModeHandler(IEditorPart editorPart) {
+		fEditorPart = editorPart;
+	}
+
+	/**
+	 * Disables features not compatible with macro record or playback.
+	 */
+	public void enterMacroMode() {
+		if (fEditorPart instanceof ITextEditorExtension5) {
+			ITextEditorExtension5 iTextEditorExtension5 = (ITextEditorExtension5) fEditorPart;
+			if (iTextEditorExtension5.isBlockSelectionModeEnabled()) {
+				// Note: macro can't deal with block selections... there's nothing really
+				// inherent to not being able to work, but given:
+				// org.eclipse.jface.text.TextViewer.verifyEventInBlockSelection(VerifyEvent)
+				// and the fact that we don't generate events through the Display (because it's
+				// too error prone -- so much that it could target the wrong IDE instance for
+				// the events because it deals with system messages and not really events
+				// inside the IDE) and the fact that we can't force a new system message time
+				// for temporary events created internally, makes it really hard to work
+				// around the hack in verifyEventInBlockSelection.
+				// So, we simply disable block selection mode as well as the action which would
+				// activate it.
+
+				// Note: ideally we'd have a way to set a new time for the time returned in
+				// org.eclipse.swt.widgets.Display.getLastEventTime()
+				// -- as it is, internally the events time will be always the same because
+				// there's no API to reset it -- if possible we should reset it when we
+				// generate our internal events at:
+				// org.eclipse.ui.workbench.texteditor.macros.internal.StyledTextKeyDownMacroInstruction.execute(IMacroPlaybackContext)
+				iTextEditorExtension5.setBlockSelectionMode(false);
+			}
+		}
+
+		if (fEditorPart instanceof ITextEditor) {
+			ITextEditor textEditor = (ITextEditor) fEditorPart;
+			disable(textEditor, ITextEditorActionConstants.CONTENT_ASSIST);
+			disable(textEditor, ITextEditorActionConstants.QUICK_ASSIST);
+			disable(textEditor, ITextEditorActionConstants.BLOCK_SELECTION_MODE);
+		}
+
+		if (fEditorPart != null) {
+			ITextOperationTarget textOperationTarget = fEditorPart.getAdapter(ITextOperationTarget.class);
+			if (textOperationTarget instanceof ITextOperationTargetExtension) {
+				ITextOperationTargetExtension targetExtension = (ITextOperationTargetExtension) textOperationTarget;
+
+				// Disable content assist and mark it to be restored later on
+				disable(textOperationTarget, targetExtension, ISourceViewer.CONTENTASSIST_PROPOSALS,
+						CONTENT_ASSIST_ENABLED);
+				disable(textOperationTarget, targetExtension, ISourceViewer.QUICK_ASSIST, QUICK_ASSIST_ENABLED);
+			}
+		}
+	}
+
+	/**
+	 * Resets the state of the editor to what it was before macro record or playback
+	 * started.
+	 */
+	public void leaveMacroMode() {
+		if (fEditorPart != null) {
+			ITextOperationTarget textOperationTarget = fEditorPart.getAdapter(ITextOperationTarget.class);
+			if (textOperationTarget instanceof ITextOperationTargetExtension) {
+				ITextOperationTargetExtension targetExtension = (ITextOperationTargetExtension) textOperationTarget;
+				if (textOperationTarget instanceof ITextOperationTargetExtension) {
+					restore(targetExtension, ISourceViewer.CONTENTASSIST_PROPOSALS, CONTENT_ASSIST_ENABLED);
+					restore(targetExtension, ISourceViewer.QUICK_ASSIST, QUICK_ASSIST_ENABLED);
+				}
+			}
+
+			if (fEditorPart instanceof ITextEditor) {
+				ITextEditor textEditor = (ITextEditor) fEditorPart;
+				restore(textEditor, ITextEditorActionConstants.CONTENT_ASSIST);
+				restore(textEditor, ITextEditorActionConstants.QUICK_ASSIST);
+				restore(textEditor, ITextEditorActionConstants.BLOCK_SELECTION_MODE);
+			}
+		}
+	}
+
+	private void restore(ITextOperationTargetExtension targetExtension, int operation, String preference) {
+		Boolean contentAssistProposalsBeforMacroMode = fMemento.get(preference);
+		if (contentAssistProposalsBeforMacroMode != null) {
+			if ((contentAssistProposalsBeforMacroMode).booleanValue()) {
+				targetExtension.enableOperation(operation, true);
+			} else {
+				targetExtension.enableOperation(operation, false);
+			}
+		}
+	}
+
+	private void restore(ITextEditor textEditor, String actionId) {
+		Boolean b = fMemento.get(actionId);
+		if (b != null && b) {
+			Control control = textEditor.getAdapter(Control.class);
+			if (control != null && !control.isDisposed()) {
+				// Do nothing if already disposed.
+				IAction action = textEditor.getAction(actionId);
+				if (action instanceof TextEditorAction) {
+					TextEditorAction textEditorAction = (TextEditorAction) action;
+					textEditorAction.setEditor(textEditor);
+					textEditorAction.update();
+				}
+			}
+		}
+	}
+
+	private void disable(ITextOperationTarget textOperationTarget, ITextOperationTargetExtension targetExtension,
+			int operation, String preference) {
+		if (textOperationTarget.canDoOperation(operation)) {
+			fMemento.put(preference, true);
+			targetExtension.enableOperation(operation, false);
+		}
+	}
+
+	private void disable(ITextEditor textEditor, String actionId) {
+		IAction action = textEditor.getAction(actionId);
+		if (action != null && action instanceof TextEditorAction) {
+			TextEditorAction textEditorAction = (TextEditorAction) action;
+			fMemento.put(actionId, true);
+			textEditorAction.setEditor(null);
+			textEditorAction.update();
+		}
+	}
+
+}
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorInstaller.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorInstaller.java
new file mode 100644
index 0000000..e9dbe11
--- /dev/null
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorInstaller.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Fabio Zadrozny and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
+ *******************************************************************************/
+package org.eclipse.ui.workbench.texteditor.macros.internal;
+
+import javax.inject.Inject;
+import org.eclipse.e4.core.contexts.ContextInjectionFactory;
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.core.macros.CancelMacroException;
+import org.eclipse.e4.core.macros.CancelMacroPlaybackException;
+import org.eclipse.e4.core.macros.CancelMacroRecordingException;
+import org.eclipse.e4.core.macros.EMacroService;
+import org.eclipse.e4.core.macros.IMacroRecordContext;
+import org.eclipse.e4.core.macros.IMacroStateListener;
+import org.eclipse.e4.ui.macros.internal.EditorUtils;
+import org.eclipse.e4.ui.macros.internal.UserNotifications;
+import org.eclipse.swt.custom.StyledText;
+
+/**
+ * A listener to the macro state which will enable notifications to the user
+ * regarding limitations of recording only in the current editor.
+ */
+public class NotifyMacroOnlyInCurrentEditorInstaller implements IMacroStateListener {
+
+	private static final String VERIFY_MACRO_ONLY_IN_CURRENT_EDITOR = "VERIFY_MACRO_ONLY_IN_CURRENT_EDITOR"; //$NON-NLS-1$
+
+	@Inject
+	private IEclipseContext fEclipseContext;
+
+	private UserNotifications fUserNotifications;
+
+	/**
+	 * Provides the class which should be used to give user notifications.
+	 *
+	 * @return the helper class for giving user notifications.
+	 */
+	private UserNotifications getUserNotifications() {
+		if (fUserNotifications == null) {
+			fUserNotifications = ContextInjectionFactory.make(UserNotifications.class, fEclipseContext);
+		}
+		return fUserNotifications;
+	}
+
+	@Override
+	public void macroStateChanged(EMacroService macroService, StateChange stateChange)
+			throws CancelMacroException {
+		if (stateChange == StateChange.RECORD_STARTED) {
+			StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
+			if (currentStyledText == null) {
+				UserNotifications userNotifications = getUserNotifications();
+				userNotifications.setMessage(Messages.NotifyMacroOnlyInCurrentEditor_NotRecording);
+				userNotifications.notifyNoEditorOnMacroRecordStartup();
+				throw new CancelMacroRecordingException();
+			}
+
+			IMacroRecordContext context = macroService.getMacroRecordContext();
+			NotifyMacroOnlyInCurrentEditorListener notifyMacroOnlyInCurrentEditor = new NotifyMacroOnlyInCurrentEditorListener(
+					macroService, fEclipseContext);
+			notifyMacroOnlyInCurrentEditor.install();
+			context.set(VERIFY_MACRO_ONLY_IN_CURRENT_EDITOR, notifyMacroOnlyInCurrentEditor);
+
+		} else if (stateChange == StateChange.RECORD_FINISHED) {
+			IMacroRecordContext context = macroService.getMacroRecordContext();
+			Object object = context.get(VERIFY_MACRO_ONLY_IN_CURRENT_EDITOR);
+			if (object instanceof NotifyMacroOnlyInCurrentEditorListener) {
+				NotifyMacroOnlyInCurrentEditorListener notifyMacroOnlyInCurrentEditor = (NotifyMacroOnlyInCurrentEditorListener) object;
+				notifyMacroOnlyInCurrentEditor.uninstall();
+			}
+		} else if (stateChange == StateChange.PLAYBACK_STARTED) {
+			StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
+			if (currentStyledText == null) {
+				getUserNotifications().notifyNoEditorOnMacroPlaybackStartup();
+				throw new CancelMacroPlaybackException();
+			}
+		}
+	}
+}
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditor.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorListener.java
similarity index 76%
rename from bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditor.java
rename to bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorListener.java
index 5c2ef1a..c9586da 100644
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditor.java
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/NotifyMacroOnlyInCurrentEditorListener.java
@@ -12,8 +12,6 @@
 
 import org.eclipse.e4.core.contexts.ContextInjectionFactory;
 import org.eclipse.e4.core.contexts.IEclipseContext;
-import org.eclipse.e4.core.macros.CancelMacroPlaybackException;
-import org.eclipse.e4.core.macros.CancelMacroRecordingException;
 import org.eclipse.e4.core.macros.EMacroService;
 import org.eclipse.e4.core.macros.IMacroRecordContext;
 import org.eclipse.e4.ui.macros.internal.EditorUtils;
@@ -26,9 +24,11 @@
 import org.eclipse.ui.PlatformUI;
 
 /**
- * Used to notify that macro is only available in the initial editor.
+ * Listens for window and part changes to notify that macro recording is not
+ * active (currently macro recording is only enabled in the editor which was
+ * active when macro recording started).
  */
-public class NotifyMacroOnlyInCurrentEditor {
+public class NotifyMacroOnlyInCurrentEditorListener {
 
 	/**
 	 * When a new window is opened/activated, add the needed listeners.
@@ -124,74 +124,11 @@
 	 * @param eclipseContext
 	 *            eclipse context for dependency injection.
 	 */
-	public NotifyMacroOnlyInCurrentEditor(EMacroService macroService, IEclipseContext eclipseContext) {
+	public NotifyMacroOnlyInCurrentEditorListener(EMacroService macroService, IEclipseContext eclipseContext) {
 		fMacroService = macroService;
 		fEclipseContext = eclipseContext;
 	}
 
-	/**
-	 * Provides the class which should be used to give user notifications.
-	 *
-	 * @return the helper class for giving user notifications.
-	 */
-	public UserNotifications getUserNotifications() {
-		if (fUserNotifications == null) {
-			fUserNotifications = ContextInjectionFactory.make(UserNotifications.class, fEclipseContext);
-		}
-		return fUserNotifications;
-	}
-
-	/**
-	 * Checks that the current editor didn't change (if it did, notify the user that
-	 * recording doesn't work in other editors).
-	 */
-	private void checkCurrentEditor() {
-		IMacroRecordContext macroRecordContext = this.fMacroService.getMacroRecordContext();
-		if (macroRecordContext != null) {
-			StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
-			StyledText targetStyledText = EditorUtils.getTargetStyledText(macroRecordContext);
-			if (targetStyledText != currentStyledText && currentStyledText != fLastEditor) {
-				getUserNotifications().setMessage(Messages.NotifyMacroOnlyInCurrentEditor_NotRecording);
-				getUserNotifications().notifyCurrentEditor();
-			} else if (targetStyledText == currentStyledText && fLastEditor != null
-					&& fLastEditor != currentStyledText) {
-				getUserNotifications().setMessage(Messages.NotifyMacroOnlyInCurrentEditor_Recording);
-			}
-			fLastEditor = currentStyledText;
-		}
-	}
-
-	/**
-	 * Check if there's some active editor when the macro recording starts.
-	 *
-	 * @throws CancelMacroRecordingException
-	 *             if there's no active editor available for the macro recording.
-	 */
-	public void checkEditorActiveForMacroRecording() throws CancelMacroRecordingException {
-		StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
-		if (currentStyledText == null) {
-			UserNotifications userNotifications = getUserNotifications();
-			userNotifications.setMessage(Messages.NotifyMacroOnlyInCurrentEditor_NotRecording);
-			userNotifications.notifyNoEditorOnMacroRecordStartup();
-			throw new CancelMacroRecordingException();
-		}
-	}
-
-	/**
-	 * Check if there's some active editor when the macro playback starts.
-	 *
-	 * @throws CancelMacroPlaybackException
-	 *             if there's no active editor available for the macro playback.
-	 */
-	public void checkEditorActiveForMacroPlayback() throws CancelMacroPlaybackException {
-		StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
-		if (currentStyledText == null) {
-			getUserNotifications().notifyNoEditorOnMacroPlaybackStartup();
-			throw new CancelMacroPlaybackException();
-		}
-	}
-
-
 	private void addListeners(IWorkbenchWindow window) {
 		window.getPartService().addPartListener(fPartListener);
 	}
@@ -221,6 +158,35 @@
 		PlatformUI.getWorkbench().removeWindowListener(fWindowsListener);
 	}
 
+	/**
+	 * Provides the class which should be used to give user notifications.
+	 *
+	 * @return the helper class for giving user notifications.
+	 */
+	private UserNotifications getUserNotifications() {
+		if (fUserNotifications == null) {
+			fUserNotifications = ContextInjectionFactory.make(UserNotifications.class, fEclipseContext);
+		}
+		return fUserNotifications;
+	}
 
-
+	/**
+	 * Checks that the current editor didn't change (if it did, notify the user that
+	 * recording doesn't work in other editors).
+	 */
+	private void checkCurrentEditor() {
+		IMacroRecordContext macroRecordContext = this.fMacroService.getMacroRecordContext();
+		if (macroRecordContext != null) {
+			StyledText currentStyledText = EditorUtils.getActiveEditorStyledText(fEclipseContext);
+			StyledText targetStyledText = EditorUtils.getTargetStyledText(macroRecordContext);
+			if (targetStyledText != currentStyledText && currentStyledText != fLastEditor) {
+				getUserNotifications().setMessage(Messages.NotifyMacroOnlyInCurrentEditor_NotRecording);
+				getUserNotifications().notifyCurrentEditor();
+			} else if (targetStyledText == currentStyledText && fLastEditor != null
+					&& fLastEditor != currentStyledText) {
+				getUserNotifications().setMessage(Messages.NotifyMacroOnlyInCurrentEditor_Recording);
+			}
+			fLastEditor = currentStyledText;
+		}
+	}
 }
diff --git a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/StyledTextKeyDownMacroInstruction.java b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/StyledTextKeyDownMacroInstruction.java
index 1f666ad..970986c 100644
--- a/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/StyledTextKeyDownMacroInstruction.java
+++ b/bundles/org.eclipse.ui.workbench.texteditor.macros/src/org/eclipse/ui/workbench/texteditor/macros/internal/StyledTextKeyDownMacroInstruction.java
@@ -12,7 +12,6 @@
 
 import java.util.Map;
 import org.eclipse.e4.core.macros.IMacroPlaybackContext;
-import org.eclipse.e4.core.macros.internal.JSONHelper;
 import org.eclipse.e4.ui.macros.internal.EditorUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.StyledText;
@@ -67,7 +66,65 @@
 
 	@Override
 	public String toString() {
-		return Messages.StyledTextKeyDownMacroInstruction_KeyDown
-				+ JSONHelper.quote(Character.toString(this.fEvent.character));
+		if (this.fEvent.keyCode == SWT.CTRL) {
+			return "Ctrl"; //$NON-NLS-1$
+		}
+		if (this.fEvent.keyCode == SWT.SHIFT) {
+			return "Shift"; //$NON-NLS-1$
+		}
+		if (this.fEvent.keyCode == SWT.ALT) {
+			return "Alt"; //$NON-NLS-1$
+		}
+		return Messages.StyledTextKeyDownMacroInstruction_KeyDown + quote(this.fEvent.character);
 	}
+
+	/**
+	 * Quotes contents of the passed char so that it can be readable by the user
+	 * when printed.
+	 *
+	 * @param c
+	 *            the char to be quoted.
+	 * @return a string with contents to be shown to the user.
+	 */
+	private String quote(char c) {
+		StringBuilder sb = new StringBuilder(4);
+		sb.append('"');
+
+		switch (c) {
+		case '"':
+			sb.append('\\');
+			sb.append(c);
+			break;
+
+		case '\b':
+			sb.append("\\b"); //$NON-NLS-1$
+			break;
+
+		case '\f':
+			sb.append("\\f"); //$NON-NLS-1$
+			break;
+
+		case '\n':
+			sb.append("\\n"); //$NON-NLS-1$
+			break;
+
+		case '\r':
+			sb.append("\\r"); //$NON-NLS-1$
+			break;
+
+		case '\t':
+			sb.append("\\t"); //$NON-NLS-1$
+			break;
+
+		default:
+			if (c < ' ') {
+				sb.append("\\u" + Integer.toHexString(c)); //$NON-NLS-1$
+			} else {
+				sb.append(c);
+			}
+		}
+		sb.append('"');
+		return sb.toString();
+	}
+
 }
\ No newline at end of file
diff --git a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/JSONHelperTest.java b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/JSONHelperTest.java
deleted file mode 100644
index c3da94d..0000000
--- a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/JSONHelperTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Fabio Zadrozny and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
- *******************************************************************************/
-package org.eclipse.e4.ui.macros.tests;
-
-import org.eclipse.e4.core.macros.internal.JSONHelper;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class JSONHelperTest {
-
-	@Test
-	public void testJSONHelper() {
-		StringBuilder builder = new StringBuilder("chars\"\\/\b\f\n\r\t");
-		builder.append(new Character((char) 5));
-		builder.append(new Character((char) 6));
-		String quoted = JSONHelper.quote(builder.toString());
-		Assert.assertEquals("\"chars\\\"\\\\\\/\\b\\f\\n\\r\\t\\u0005\\u0006\"", quoted);
-	}
-}
diff --git a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/KeyBindingDispatcherMacroIntegrationTest.java b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/KeyBindingDispatcherMacroIntegrationTest.java
index ff490da..66064a8 100644
--- a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/KeyBindingDispatcherMacroIntegrationTest.java
+++ b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/KeyBindingDispatcherMacroIntegrationTest.java
@@ -41,7 +41,7 @@
 import org.eclipse.e4.core.macros.IMacroRecordContext;
 import org.eclipse.e4.core.macros.IMacroStateListener;
 import org.eclipse.e4.core.macros.internal.MacroManager;
-import org.eclipse.e4.core.macros.internal.MacroServiceImplementation;
+import org.eclipse.e4.core.macros.internal.MacroServiceImpl;
 import org.eclipse.e4.ui.bindings.BindingServiceAddon;
 import org.eclipse.e4.ui.bindings.EBindingService;
 import org.eclipse.e4.ui.bindings.internal.BindingTable;
@@ -196,7 +196,7 @@
 		ContextInjectionFactory.inject(dispatcher, workbenchContext);
 
 		workbenchContext.set(EMacroService.class.getName(),
-				ContextInjectionFactory.make(MacroServiceImplementation.class, workbenchContext));
+				ContextInjectionFactory.make(MacroServiceImpl.class, workbenchContext));
 
 		dispatcherListener = dispatcher.getKeyDownFilter();
 		display.addFilter(SWT.KeyDown, dispatcherListener);
@@ -206,7 +206,7 @@
 
 		fMacrosDirectory = folder.getRoot();
 		EMacroService macroService = workbenchContext.get(EMacroService.class);
-		MacroServiceImplementation macroServiceImplementation = (MacroServiceImplementation) macroService;
+		MacroServiceImpl macroServiceImplementation = (MacroServiceImpl) macroService;
 		MacroManager macroManager = macroServiceImplementation.getMacroManager();
 		macroManager.setMacrosDirectories(fMacrosDirectory);
 		Predicate<IConfigurationElement> filterMacroListeners = new Predicate<IConfigurationElement>() {
@@ -218,7 +218,7 @@
 			}
 		};
 
-		Field field = MacroServiceImplementation.class.getDeclaredField("fFilterMacroListeners");
+		Field field = MacroServiceImpl.class.getDeclaredField("fFilterMacroListeners");
 		field.setAccessible(true);
 		field.set(macroServiceImplementation, filterMacroListeners);
 	}
@@ -256,7 +256,7 @@
 				Arrays.asList("org.eclipse.e4.ui.macros.internal.actions.MacroUIUpdater",
 						"org.eclipse.e4.ui.macros.internal.keybindings.CommandManagerExecutionListenerInstaller"),
 				getRegisteredClasses(macroService));
-		IMacroStateListener[] macroStateListeners = ((MacroServiceImplementation) macroService)
+		IMacroStateListener[] macroStateListeners = ((MacroServiceImpl) macroService)
 				.getMacroStateListeners();
 		boolean found = false;
 		for (IMacroStateListener iMacroStateListener : macroStateListeners) {
@@ -297,7 +297,7 @@
 
 		notifyCtrlI(styledText);
 		assertTrue(handler.q2);
-		assertEquals(((MacroServiceImplementation) macroService).getMacroManager().getLengthOfMacroBeingRecorded(), 1);
+		assertEquals(((MacroServiceImpl) macroService).getMacroManager().getLengthOfMacroBeingRecorded(), 1);
 
 		finishRecording(macroService);
 
@@ -307,7 +307,7 @@
 	}
 
 	public List<String> getRegisteredClasses(EMacroService macroService) {
-		IMacroStateListener[] macroStateListeners = ((MacroServiceImplementation) macroService)
+		IMacroStateListener[] macroStateListeners = ((MacroServiceImpl) macroService)
 				.getMacroStateListeners();
 		List<String> classes = Arrays.stream(macroStateListeners).map(m -> m.getClass().getName()).sorted()
 				.collect(Collectors.toList());
@@ -320,7 +320,7 @@
 
 			@Override
 			public boolean accept(File dir, String name) {
-				return name.endsWith(".js");
+				return name.endsWith(".xml");
 			}
 		};
 		assertEquals(0, fMacrosDirectory.list(macrosFilter).length);
@@ -332,14 +332,14 @@
 		startRecording(macroService);
 		notifyCtrlI(styledText);
 		assertTrue(handler.q2);
-		assertEquals(((MacroServiceImplementation) macroService).getMacroManager().getLengthOfMacroBeingRecorded(), 1);
+		assertEquals(((MacroServiceImpl) macroService).getMacroManager().getLengthOfMacroBeingRecorded(), 1);
 		finishRecording(macroService);
 
 		// Macro was saved in the dir.
 		assertEquals(1, fMacrosDirectory.list(macrosFilter).length);
 
 		// Check if reloading from disk and playing it back works.
-		((MacroServiceImplementation) macroService).getMacroManager().reloadMacros();
+		((MacroServiceImpl) macroService).getMacroManager().reloadMacros();
 		handler.q2 = false;
 		macroService.playbackLastMacro();
 		assertTrue(handler.q2);
diff --git a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/MacroTest.java b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/MacroTest.java
index ae9d7c3..61f1cae 100644
--- a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/MacroTest.java
+++ b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/MacroTest.java
@@ -33,7 +33,7 @@
 import org.eclipse.e4.core.macros.IMacroStateListener;
 import org.eclipse.e4.core.macros.internal.MacroManager;
 import org.eclipse.e4.core.macros.internal.MacroManager.StoredMacroReference;
-import org.eclipse.e4.core.macros.internal.MacroServiceImplementation;
+import org.eclipse.e4.core.macros.internal.MacroServiceImpl;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -47,6 +47,11 @@
 	private static class PlaybackContext implements IMacroPlaybackContext {
 
 		public StringBuffer buffer = new StringBuffer();
+		private Map<String, IMacroInstructionFactory> fMacroInstructionIdToFactory;
+
+		public PlaybackContext(Map<String, IMacroInstructionFactory> macroInstructionIdToFactory) {
+			fMacroInstructionIdToFactory = macroInstructionIdToFactory;
+		}
 
 		public void recordPlayback(String name) {
 			if (buffer.length() > 0) {
@@ -67,6 +72,12 @@
 		public void set(String key, Object value) {
 			ctx.put(key, value);
 		}
+
+		@Override
+		public void runMacroInstruction(String macroInstructionId, Map<String, String> macroInstructionParameters)
+				throws Exception {
+			fMacroInstructionIdToFactory.get(macroInstructionId).create(macroInstructionParameters).execute(this);
+		}
 	}
 
 	private static class DummyMacroInstruction implements IMacroInstruction {
@@ -108,7 +119,7 @@
 		IEclipseContext eclipseContext = EclipseContextFactory.create("testRecordingState");
 		IExtensionRegistry extensionRegistry = RegistryFactory.createRegistry(new RegistryStrategy(null, null), "foo",
 				"bar");
-		MacroServiceImplementation macroService = new MacroServiceImplementation(eclipseContext,
+		MacroServiceImpl macroService = new MacroServiceImpl(eclipseContext,
 				extensionRegistry);
 		Field field = macroService.getClass().getDeclaredField("fMacroInstructionIdToFactory");
 		field.setAccessible(true);
@@ -141,14 +152,14 @@
 		buf.setLength(0);
 		Assert.assertFalse(macroService.isRecording());
 
-		PlaybackContext playbackContext = new PlaybackContext();
-		macroService.getMacroManager().playbackLastMacro(macroService, playbackContext, macroInstructionIdToFactory);
+		PlaybackContext playbackContext = new PlaybackContext(macroInstructionIdToFactory);
+		macroService.getMacroManager().playbackLastMacro(macroService, playbackContext);
 		Assert.assertEquals("rec: false play: true\n"
 				+ "rec: false play: false\n", buf.toString());
 		buf.setLength(0);
 
 		macroService.toggleMacroRecord();
-		macroService.getMacroManager().playbackLastMacro(macroService, playbackContext, macroInstructionIdToFactory);
+		macroService.getMacroManager().playbackLastMacro(macroService, playbackContext);
 		macroService.toggleMacroRecord();
 		Assert.assertEquals(
 				"rec: true play: false\n"
@@ -162,7 +173,7 @@
 	public void testAddMacroInstructions() throws Exception {
 		MacroManager macroManager = new MacroManager();
 		Map<String, IMacroInstructionFactory> macroInstructionIdToFactory = makeMacroInstructionIdToFactory();
-		PlaybackContext playbackContext = new PlaybackContext();
+		PlaybackContext playbackContext = new PlaybackContext(macroInstructionIdToFactory);
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertTrue(macroManager.isRecording());
 		macroManager.addMacroInstruction(new DummyMacroInstruction("macro1"));
@@ -170,7 +181,7 @@
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertFalse(macroManager.isRecording());
 
-		macroManager.playbackLastMacro(null, playbackContext, macroInstructionIdToFactory);
+		macroManager.playbackLastMacro(null, playbackContext);
 		Assert.assertEquals("macro1\nmacro2", playbackContext.buffer.toString());
 	}
 
@@ -178,7 +189,7 @@
 	public void testAddMacroInstructionPriority() throws Exception {
 		MacroManager macroManager = new MacroManager();
 		Map<String, IMacroInstructionFactory> macroInstructionIdToFactory = makeMacroInstructionIdToFactory();
-		PlaybackContext playbackContext = new PlaybackContext();
+		PlaybackContext playbackContext = new PlaybackContext(macroInstructionIdToFactory);
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertTrue(macroManager.isRecording());
 		Object ev = new Integer(1);
@@ -189,7 +200,7 @@
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertFalse(macroManager.isRecording());
 
-		macroManager.playbackLastMacro(null, playbackContext, macroInstructionIdToFactory);
+		macroManager.playbackLastMacro(null, playbackContext);
 		Assert.assertEquals("macro1", playbackContext.buffer.toString());
 	}
 
@@ -197,7 +208,7 @@
 	public void testAddMacroInstructionPriority1() throws Exception {
 		MacroManager macroManager = new MacroManager();
 		Map<String, IMacroInstructionFactory> macroInstructionIdToFactory = makeMacroInstructionIdToFactory();
-		PlaybackContext playbackContext = new PlaybackContext();
+		PlaybackContext playbackContext = new PlaybackContext(macroInstructionIdToFactory);
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertTrue(macroManager.isRecording());
 		Object ev = new Integer(1);
@@ -208,7 +219,7 @@
 		macroManager.toggleMacroRecord(null, macroInstructionIdToFactory);
 		Assert.assertFalse(macroManager.isRecording());
 
-		macroManager.playbackLastMacro(null, playbackContext, macroInstructionIdToFactory);
+		macroManager.playbackLastMacro(null, playbackContext);
 		Assert.assertEquals("macro2", playbackContext.buffer.toString());
 	}
 
@@ -223,8 +234,8 @@
 
 		// Create a new macroManager (to force getting from the disk).
 		macroManager = new MacroManager(root);
-		PlaybackContext playbackContext = new PlaybackContext();
-		macroManager.playbackLastMacro(null, playbackContext, macroInstructionIdToFactory);
+		PlaybackContext playbackContext = new PlaybackContext(macroInstructionIdToFactory);
+		macroManager.playbackLastMacro(null, playbackContext);
 		Assert.assertEquals("macro1", playbackContext.buffer.toString());
 	}
 
@@ -303,7 +314,7 @@
 
 			@Override
 			public boolean accept(File dir, String name) {
-				return name.endsWith(".js") && name.startsWith("temp_macro");
+				return name.endsWith(".xml") && name.startsWith("temp_macro");
 			}
 		});
 		return files;
diff --git a/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/SavedXMLMacroTest.java b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/SavedXMLMacroTest.java
new file mode 100644
index 0000000..4b4c6fe
--- /dev/null
+++ b/tests/org.eclipse.e4.ui.macros.tests/src/org/eclipse/e4/ui/macros/tests/SavedXMLMacroTest.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Fabio Zadrozny and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Fabio Zadrozny - initial API and implementation - http://eclip.se/8519
+ *******************************************************************************/
+package org.eclipse.e4.ui.macros.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.e4.core.macros.IMacroInstruction;
+import org.eclipse.e4.core.macros.IMacroInstructionFactory;
+import org.eclipse.e4.core.macros.IMacroPlaybackContext;
+import org.eclipse.e4.core.macros.MacroPlaybackException;
+import org.eclipse.e4.core.macros.internal.ComposableMacro;
+import org.eclipse.e4.core.macros.internal.SavedXMLMacro;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class SavedXMLMacroTest {
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	private static class MyMacroInstruction implements IMacroInstruction {
+
+		private static String SPECIAL_CHARS = "!@#$%^&*()<--\n\r\b\t\0&";
+		private Map<String, String> map;
+
+		public MyMacroInstruction(Map<String, String> stringMap) {
+			this.map = stringMap;
+			Assert.isTrue(new MyMacroInstruction().toMap().equals(stringMap));
+		}
+
+		public MyMacroInstruction() {
+			map = new HashMap<>();
+			map.put("attr", "value");
+			map.put("attr1", SPECIAL_CHARS);
+			map.put(SPECIAL_CHARS, "value1");
+			map.put(SPECIAL_CHARS + "\n", SPECIAL_CHARS + "\n");
+		}
+
+		@Override
+		public String getId() {
+			return "my_macro_instruction";
+		}
+
+		@Override
+		public void execute(IMacroPlaybackContext macroPlaybackContext) throws MacroPlaybackException {
+			macroPlaybackContext.set("played", true);
+		}
+
+		@Override
+		public Map<String, String> toMap() {
+			return map;
+		}
+	}
+
+	private static class MyMacroPlaybackContext implements IMacroPlaybackContext {
+
+		private Map<String, Object> ctx = new HashMap<>();
+
+		@Override
+		public Object get(String key) {
+			return ctx.get(key);
+		}
+
+		@Override
+		public void set(String key, Object value) {
+			ctx.put(key, value);
+		}
+
+		@Override
+		public void runMacroInstruction(String macroInstructionId, Map<String, String> macroInstructionParameters)
+				throws Exception {
+			new MyMacroInstruction(macroInstructionParameters).execute(this);
+		}
+	}
+
+	private static class MyMacroInstructionFactory implements IMacroInstructionFactory {
+
+		@Override
+		public IMacroInstruction create(Map<String, String> stringMap) throws Exception {
+			return new MyMacroInstruction(stringMap);
+		}
+	}
+
+	@Test
+	public void testSavedXMLMacro() throws Exception {
+		Map<String, IMacroInstructionFactory> macroInstructionIdToFactory = new HashMap<>();
+		MyMacroInstruction macroInstruction = new MyMacroInstruction();
+		macroInstructionIdToFactory.put(macroInstruction.getId(), new MyMacroInstructionFactory());
+		ComposableMacro composable = new ComposableMacro(macroInstructionIdToFactory);
+		composable.addMacroInstruction(macroInstruction);
+		byte[] xmlBytes = composable.toXMLBytes();
+		File tempFile = folder.newFile("saved_xml.xml");
+		Files.write(tempFile.toPath(), xmlBytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
+				StandardOpenOption.TRUNCATE_EXISTING);
+
+		SavedXMLMacro macro = new SavedXMLMacro(tempFile);
+		IMacroPlaybackContext macroPlaybackContext = new MyMacroPlaybackContext();
+		macro.playback(macroPlaybackContext);
+		assertEquals(macroPlaybackContext.get("played"), true);
+	}
+}