From 82c67b02792ceb3548417b8dd3810c8c0b90c8e1 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Mon, 18 Mar 2019 22:51:57 +0100
Subject: [PATCH 1/2] Add serial monitor command history
The behavior is as follows:
- Pressing the UP key will select older commands.
- Pressing the DOWN key will select newer commands, restoring the last unexecuted command if available.
- Pressing the ESC key will reset the input field to the latest unexecuted command and reset the traversal location. Pressing ESC while the latest unexecuted command is selected will clear the input field.
This fixes #4891, with the difference that the proposed solution has a command history per Arduino session and this implementation has a command history per serial monitor start.
---
app/src/processing/app/CommandHistory.java | 118 +++++++++++++++++++++
app/src/processing/app/SerialMonitor.java | 42 +++++++-
2 files changed, 158 insertions(+), 2 deletions(-)
create mode 100644 app/src/processing/app/CommandHistory.java
diff --git a/app/src/processing/app/CommandHistory.java b/app/src/processing/app/CommandHistory.java
new file mode 100644
index 00000000000..25ded6f1fb9
--- /dev/null
+++ b/app/src/processing/app/CommandHistory.java
@@ -0,0 +1,118 @@
+package processing.app;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Keeps track of command history in console-like applications.
+ * @author P.J.S. Kools
+ */
+public class CommandHistory {
+
+ private List commandHistory = new ArrayList();
+ private int selectedCommandIndex = 0;
+ private final int maxHistorySize;
+
+ /**
+ * Create a new {@link CommandHistory}.
+ * @param maxHistorySize - The max command history size.
+ */
+ public CommandHistory(int maxHistorySize) {
+ this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize);
+ this.commandHistory.add(""); // Current command placeholder.
+ }
+
+ /**
+ * Adds the given command to the history and resets the history traversal
+ * position to the latest command. If the max history size is exceeded,
+ * the oldest command will be removed from the history.
+ * @param command - The command to add.
+ */
+ public void addCommand(String command) {
+
+ // Remove the oldest command if the max history size is exceeded.
+ if(this.commandHistory.size() >= this.maxHistorySize + 1) {
+ this.commandHistory.remove(0);
+ }
+
+ // Add the new command, reset the 'current' command and reset the index.
+ this.commandHistory.set(this.commandHistory.size() - 1, command);
+ this.commandHistory.add(""); // Current command placeholder.
+ this.selectedCommandIndex = this.commandHistory.size() - 1;
+ }
+
+ /**
+ * Gets whether a next (more recent) command is available in the history.
+ * @return {@code true} if a next command is available,
+ * returns {@code false} otherwise.
+ */
+ public boolean hasNextCommand() {
+ return this.selectedCommandIndex + 1 < this.commandHistory.size();
+ }
+
+ /**
+ * Gets the next (more recent) command from the history.
+ * @return The next command or {@code null} if no next command is available.
+ */
+ public String getNextCommand() {
+ return this.hasNextCommand()
+ ? this.commandHistory.get(++this.selectedCommandIndex) : null;
+ }
+
+ /**
+ * Gets whether a previous (older) command is available in the history.
+ * @return {@code true} if a previous command is available,
+ * returns {@code false} otherwise.
+ */
+ public boolean hasPreviousCommand() {
+ return this.selectedCommandIndex > 0;
+ }
+
+ /**
+ * Gets the previous (older) command from the history.
+ * When this method is called while the most recent command in the history is
+ * selected, this will store the current command as temporary latest command
+ * so that {@link #getNextCommand()} will return it. This temporary latest
+ * command gets reset when this case occurs again or when
+ * {@link #addCommand(String)} is invoked.
+ * @param currentCommand - The current unexecuted command.
+ * @return The previous command or {@code null} if no previous command is
+ * available.
+ */
+ public String getPreviousCommand(String currentCommand) {
+
+ // Return null if there is no previous command available.
+ if (!this.hasPreviousCommand()) {
+ return null;
+ }
+
+ // Store current unexecuted command if not traversing already.
+ if (this.selectedCommandIndex == this.commandHistory.size() - 1) {
+ this.commandHistory.set(this.commandHistory.size() - 1,
+ (currentCommand == null ? "" : currentCommand));
+ }
+
+ // Return the previous command.
+ return this.commandHistory.get(--this.selectedCommandIndex);
+ }
+
+ /**
+ * Resets the history location to the most recent command.
+ * @returns The latest unexecuted command as stored by
+ * {@link #getPreviousCommand(String)} or an empty string if no such command
+ * was set.
+ */
+ public String resetHistoryLocation() {
+ this.selectedCommandIndex = this.commandHistory.size() - 1;
+ return this.commandHistory.set(this.commandHistory.size() - 1, "");
+ }
+
+ /**
+ * Clears the command history.
+ */
+ public void clear() {
+ this.commandHistory.clear();
+ this.commandHistory.add(""); // Current command placeholder.
+ this.selectedCommandIndex = 0;
+ }
+}
diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java
index d4f59019eae..1f9a8d5b139 100644
--- a/app/src/processing/app/SerialMonitor.java
+++ b/app/src/processing/app/SerialMonitor.java
@@ -23,6 +23,9 @@
import java.awt.Color;
import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
import static processing.app.I18n.tr;
@@ -32,6 +35,10 @@ public class SerialMonitor extends AbstractTextMonitor {
private Serial serial;
private int serialRate;
+ private static final int COMMAND_HISTORY_SIZE = 100;
+ private final CommandHistory commandHistory =
+ new CommandHistory(COMMAND_HISTORY_SIZE);
+
public SerialMonitor(Base base, BoardPort port) {
super(base, port);
@@ -54,11 +61,42 @@ public SerialMonitor(Base base, BoardPort port) {
});
onSendCommand((ActionEvent event) -> {
- send(textField.getText());
+ String command = textField.getText();
+ send(command);
+ commandHistory.addCommand(command);
textField.setText("");
});
-
+
onClearCommand((ActionEvent event) -> textArea.setText(""));
+
+ // Add key listener to UP, DOWN, ESC keys for command history traversal.
+ textField.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ switch (e.getKeyCode()) {
+
+ // Select previous command.
+ case KeyEvent.VK_UP:
+ if (commandHistory.hasPreviousCommand()) {
+ textField.setText(
+ commandHistory.getPreviousCommand(textField.getText()));
+ }
+ break;
+
+ // Select next command.
+ case KeyEvent.VK_DOWN:
+ if (commandHistory.hasNextCommand()) {
+ textField.setText(commandHistory.getNextCommand());
+ }
+ break;
+
+ // Reset history location, restoring the last unexecuted command.
+ case KeyEvent.VK_ESCAPE:
+ textField.setText(commandHistory.resetHistoryLocation());
+ break;
+ }
+ }
+ });
}
private void send(String s) {
From fe75b821726ade9d376d08d851a6855af853df3b Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Sat, 30 Mar 2019 23:16:19 +0100
Subject: [PATCH 2/2] CommandHistory optimization
- Use LinkedList with ListIterator to make all methods except for `clear()` run in `O(1)` (constant runtime) instead of `O(n)` (linear runtime).
- No longer store executed commands that are executed multiple times (executing {1, 1, 1, 1, 2} now only adds {1, 2} to the history).
---
app/src/processing/app/CommandHistory.java | 105 +++++++++++++++------
1 file changed, 77 insertions(+), 28 deletions(-)
diff --git a/app/src/processing/app/CommandHistory.java b/app/src/processing/app/CommandHistory.java
index 25ded6f1fb9..cae3c2fc498 100644
--- a/app/src/processing/app/CommandHistory.java
+++ b/app/src/processing/app/CommandHistory.java
@@ -1,7 +1,7 @@
package processing.app;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.LinkedList;
+import java.util.ListIterator;
/**
* Keeps track of command history in console-like applications.
@@ -9,9 +9,10 @@
*/
public class CommandHistory {
- private List commandHistory = new ArrayList();
- private int selectedCommandIndex = 0;
+ private final LinkedList commandHistory = new LinkedList();
private final int maxHistorySize;
+ private ListIterator iterator = null;
+ private boolean iteratorAsc;
/**
* Create a new {@link CommandHistory}.
@@ -19,26 +20,41 @@ public class CommandHistory {
*/
public CommandHistory(int maxHistorySize) {
this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize);
- this.commandHistory.add(""); // Current command placeholder.
+ this.commandHistory.addLast(""); // Current command placeholder.
}
/**
* Adds the given command to the history and resets the history traversal
- * position to the latest command. If the max history size is exceeded,
- * the oldest command will be removed from the history.
+ * position to the latest command. If the latest command in the history is
+ * equal to the given command, it will not be added to the history.
+ * If the max history size is exceeded, the oldest command will be removed
+ * from the history.
* @param command - The command to add.
*/
public void addCommand(String command) {
+ if (this.maxHistorySize == 0) {
+ return;
+ }
+
+ // Remove 'current' command.
+ this.commandHistory.removeLast();
+
+ // Add new command if it differs from the latest command.
+ if (this.commandHistory.isEmpty()
+ || !this.commandHistory.getLast().equals(command)) {
+
+ // Remove oldest command if max history size is exceeded.
+ if (this.commandHistory.size() >= this.maxHistorySize) {
+ this.commandHistory.removeFirst();
+ }
- // Remove the oldest command if the max history size is exceeded.
- if(this.commandHistory.size() >= this.maxHistorySize + 1) {
- this.commandHistory.remove(0);
+ // Add new command and reset 'current' command.
+ this.commandHistory.addLast(command);
}
- // Add the new command, reset the 'current' command and reset the index.
- this.commandHistory.set(this.commandHistory.size() - 1, command);
- this.commandHistory.add(""); // Current command placeholder.
- this.selectedCommandIndex = this.commandHistory.size() - 1;
+ // Re-add 'current' command and reset command iterator.
+ this.commandHistory.addLast(""); // Current command placeholder.
+ this.iterator = null;
}
/**
@@ -47,7 +63,14 @@ public void addCommand(String command) {
* returns {@code false} otherwise.
*/
public boolean hasNextCommand() {
- return this.selectedCommandIndex + 1 < this.commandHistory.size();
+ if (this.iterator == null) {
+ return false;
+ }
+ if (!this.iteratorAsc) {
+ this.iterator.next(); // Current command, ascending.
+ this.iteratorAsc = true;
+ }
+ return this.iterator.hasNext();
}
/**
@@ -55,8 +78,20 @@ public boolean hasNextCommand() {
* @return The next command or {@code null} if no next command is available.
*/
public String getNextCommand() {
- return this.hasNextCommand()
- ? this.commandHistory.get(++this.selectedCommandIndex) : null;
+
+ // Return null if there is no next command available.
+ if (!this.hasNextCommand()) {
+ return null;
+ }
+
+ // Get next command.
+ String next = this.iterator.next();
+
+ // Reset 'current' command when at the end of the list.
+ if (this.iterator.nextIndex() == this.commandHistory.size()) {
+ this.iterator.set(""); // Reset 'current' command.
+ }
+ return next;
}
/**
@@ -65,15 +100,22 @@ public String getNextCommand() {
* returns {@code false} otherwise.
*/
public boolean hasPreviousCommand() {
- return this.selectedCommandIndex > 0;
+ if (this.iterator == null) {
+ return this.commandHistory.size() > 1;
+ }
+ if (this.iteratorAsc) {
+ this.iterator.previous(); // Current command, descending.
+ this.iteratorAsc = false;
+ }
+ return this.iterator.hasPrevious();
}
/**
* Gets the previous (older) command from the history.
* When this method is called while the most recent command in the history is
* selected, this will store the current command as temporary latest command
- * so that {@link #getNextCommand()} will return it. This temporary latest
- * command gets reset when this case occurs again or when
+ * so that {@link #getNextCommand()} will return it once. This temporary
+ * latest command gets reset when this case occurs again or when
* {@link #addCommand(String)} is invoked.
* @param currentCommand - The current unexecuted command.
* @return The previous command or {@code null} if no previous command is
@@ -86,14 +128,21 @@ public String getPreviousCommand(String currentCommand) {
return null;
}
- // Store current unexecuted command if not traversing already.
- if (this.selectedCommandIndex == this.commandHistory.size() - 1) {
- this.commandHistory.set(this.commandHistory.size() - 1,
- (currentCommand == null ? "" : currentCommand));
+ // Store current unexecuted command and create iterator if not traversing.
+ if (this.iterator == null) {
+ this.iterator =
+ this.commandHistory.listIterator(this.commandHistory.size());
+ this.iterator.previous(); // Last element, descending.
+ this.iteratorAsc = false;
+ }
+
+ // Store current unexecuted command if on 'current' index.
+ if (this.iterator.nextIndex() == this.commandHistory.size() - 1) {
+ this.iterator.set(currentCommand == null ? "" : currentCommand);
}
// Return the previous command.
- return this.commandHistory.get(--this.selectedCommandIndex);
+ return this.iterator.previous();
}
/**
@@ -103,7 +152,7 @@ public String getPreviousCommand(String currentCommand) {
* was set.
*/
public String resetHistoryLocation() {
- this.selectedCommandIndex = this.commandHistory.size() - 1;
+ this.iterator = null;
return this.commandHistory.set(this.commandHistory.size() - 1, "");
}
@@ -111,8 +160,8 @@ public String resetHistoryLocation() {
* Clears the command history.
*/
public void clear() {
+ this.iterator = null;
this.commandHistory.clear();
- this.commandHistory.add(""); // Current command placeholder.
- this.selectedCommandIndex = 0;
+ this.commandHistory.addLast(""); // Current command placeholder.
}
}