From 7a83460d2885d16a137c6f79ab449f8c6be61b2c Mon Sep 17 00:00:00 2001
From: dgelessus <dgelessus@users.noreply.github.com>
Date: Wed, 27 May 2020 16:57:33 +0200
Subject: [PATCH] Track position information when splitting arguments

---
 .../java/de/prob2/jupyter/CommandUtils.java   | 18 ++++----
 src/main/java/de/prob2/jupyter/Parameter.java | 45 ++++++++++++-------
 .../de/prob2/jupyter/PositionedString.java    | 33 ++++++++++++++
 .../java/de/prob2/jupyter/SplitArguments.java |  8 ++--
 .../java/de/prob2/jupyter/SplitResult.java    |  6 +--
 5 files changed, 80 insertions(+), 30 deletions(-)
 create mode 100644 src/main/java/de/prob2/jupyter/PositionedString.java

diff --git a/src/main/java/de/prob2/jupyter/CommandUtils.java b/src/main/java/de/prob2/jupyter/CommandUtils.java
index 4e66d06..10c9735 100644
--- a/src/main/java/de/prob2/jupyter/CommandUtils.java
+++ b/src/main/java/de/prob2/jupyter/CommandUtils.java
@@ -54,6 +54,7 @@ public final class CommandUtils {
 	
 	private static final @NotNull Logger LOGGER = LoggerFactory.getLogger(CommandUtils.class);
 	
+	private static final @NotNull Pattern BODY_SPLIT_PATTERN = Pattern.compile("\\n");
 	public static final @NotNull Pattern ARG_SPLIT_PATTERN = Pattern.compile("\\s+");
 	private static final @NotNull Pattern B_IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]*");
 	
@@ -91,18 +92,19 @@ public final class CommandUtils {
 	
 	public static @NotNull SplitResult splitArgs(final @NotNull Parameters parameters, final @NotNull String argString) {
 		final SplitArguments splitArgs = new SplitArguments(Collections.emptyMap());
-		String remainingArgs = argString;
+		PositionedString remainingArgs = new PositionedString(argString, 0);
 		if (parameters.getBodyParam().isPresent()) {
-			final String[] argsAndBody = argString.split("\n", 2);
-			if (argsAndBody.length > 1) {
-				remainingArgs = argsAndBody[0];
-				splitArgs.add(parameters.getBodyParam().get(), argsAndBody[1]);
+			final Matcher bodySplitMatcher = BODY_SPLIT_PATTERN.matcher(argString);
+			if (bodySplitMatcher.find()) {
+				remainingArgs = new PositionedString(argString.substring(0, bodySplitMatcher.start()), remainingArgs.getStartPosition());
+				final PositionedString bodyValue = new PositionedString(argString.substring(bodySplitMatcher.end()), bodySplitMatcher.end());
+				splitArgs.add(parameters.getBodyParam().get(), bodyValue);
 			}
 		}
 		
 		for (int i = 0; i < parameters.getPositionalParameters().size();) {
 			final Parameter<?> param = parameters.getPositionalParameters().get(i);
-			if (remainingArgs.isEmpty()) {
+			if (remainingArgs.getValue().isEmpty()) {
 				break;
 			}
 			
@@ -123,8 +125,8 @@ public final class CommandUtils {
 	}
 	
 	public static @NotNull ParsedArguments validateSplitArgs(final @NotNull Parameters parameters, final SplitResult split) {
-		if (!split.getRemaining().isEmpty()) {
-			throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + split.getRemaining());
+		if (!split.getRemaining().getValue().isEmpty()) {
+			throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + split.getRemaining().getValue());
 		}
 		
 		final ParsedArguments parsed = new ParsedArguments(Collections.emptyMap());
diff --git a/src/main/java/de/prob2/jupyter/Parameter.java b/src/main/java/de/prob2/jupyter/Parameter.java
index 06d2825..e13803e 100644
--- a/src/main/java/de/prob2/jupyter/Parameter.java
+++ b/src/main/java/de/prob2/jupyter/Parameter.java
@@ -2,38 +2,49 @@ package de.prob2.jupyter;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 
 import org.jetbrains.annotations.NotNull;
 
 public interface Parameter<T> {
 	public static final class SplitResult {
-		private final @NotNull String splitArg;
-		private final @NotNull String remainingArgString;
+		private final @NotNull PositionedString splitArg;
+		private final @NotNull PositionedString remainingArgString;
 		
-		public SplitResult(final @NotNull String splitArg, final @NotNull String remainingArgString) {
+		public SplitResult(final @NotNull PositionedString splitArg, final @NotNull PositionedString remainingArgString) {
 			super();
 			
 			this.splitArg = splitArg;
 			this.remainingArgString = remainingArgString;
 		}
 		
-		public @NotNull String getSplitArg() {
+		public @NotNull PositionedString getSplitArg() {
 			return this.splitArg;
 		}
 		
-		public @NotNull String getRemainingArgString() {
+		public @NotNull PositionedString getRemainingArgString() {
 			return this.remainingArgString;
 		}
 	}
 	
 	public interface Splitter {
 		public static final @NotNull Parameter.Splitter REGULAR = argString -> {
-			final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2);
-			return new SplitResult(split[0], split.length > 1 ? split[1] : "");
+			final Matcher argSplitMatcher = CommandUtils.ARG_SPLIT_PATTERN.matcher(argString.getValue());
+			final PositionedString splitArg;
+			final PositionedString remainingArgString;
+			if (argSplitMatcher.find()) {
+				splitArg = new PositionedString(argString.getValue().substring(0, argSplitMatcher.start()), argString.getStartPosition());
+				remainingArgString = new PositionedString(argString.getValue().substring(argSplitMatcher.end()), argString.getStartPosition() + argSplitMatcher.end());
+			} else {
+				splitArg = argString;
+				remainingArgString = new PositionedString("", argString.getStartPosition() + argString.getValue().length());
+			}
+			return new SplitResult(splitArg, remainingArgString);
 		};
-		public static final @NotNull Parameter.Splitter REMAINDER = argString -> new SplitResult(argString, "");
+		public static final @NotNull Parameter.Splitter REMAINDER = argString -> new SplitResult(argString, new PositionedString("", argString.getStartPosition() + argString.getValue().length()));
 		
-		public abstract Parameter.SplitResult split(final @NotNull String argString);
+		public abstract Parameter.SplitResult split(final @NotNull PositionedString argString);
 	}
 	
 	public interface Validator<T> {
@@ -44,25 +55,29 @@ public interface Parameter<T> {
 				throw new UserErrorException("Non-repeating parameter " + param.getIdentifier() + " cannot appear more than once");
 			}
 			
-			return argValues.get(0);
+			return argValues.get(0).getValue();
 		};
 		public static final @NotNull Parameter.Validator<@NotNull Optional<String>> ZERO_OR_ONE = (param, argValues) -> {
 			if (argValues.size() > 1) {
 				throw new UserErrorException("Non-repeating parameter " + param.getIdentifier() + " cannot appear more than once");
 			}
 			
-			return argValues.stream().findAny();
+			return argValues.stream().findAny().map(PositionedString::getValue);
 		};
 		public static final @NotNull Parameter.Validator<@NotNull List<@NotNull String>> ONE_OR_MORE = (param, argValues) -> {
 			if (argValues.isEmpty()) {
 				throw new UserErrorException("Missing required parameter " + param.getIdentifier());
 			}
 			
-			return argValues;
+			return argValues.stream()
+				.map(PositionedString::getValue)
+				.collect(Collectors.toList());
 		};
-		public static final @NotNull Parameter.Validator<@NotNull List<@NotNull String>> ZERO_OR_MORE = (param, argValues) -> argValues;
+		public static final @NotNull Parameter.Validator<@NotNull List<@NotNull String>> ZERO_OR_MORE = (param, argValues) -> argValues.stream()
+			.map(PositionedString::getValue)
+			.collect(Collectors.toList());
 		
-		public abstract T validate(final @NotNull Parameter<T> param, final @NotNull List<@NotNull String> argValues);
+		public abstract T validate(final @NotNull Parameter<T> param, final @NotNull List<@NotNull PositionedString> argValues);
 	}
 	
 	public interface RequiredSingle extends Parameter<@NotNull String> {}
@@ -141,7 +156,7 @@ public interface Parameter<T> {
 						throw new AssertionError("Body " + param.getIdentifier() + " appeared more than once, this should never happen!");
 					}
 					
-					return argValues.get(0);
+					return argValues.get(0).getValue();
 				};
 			}
 		};
diff --git a/src/main/java/de/prob2/jupyter/PositionedString.java b/src/main/java/de/prob2/jupyter/PositionedString.java
new file mode 100644
index 0000000..94c6b0d
--- /dev/null
+++ b/src/main/java/de/prob2/jupyter/PositionedString.java
@@ -0,0 +1,33 @@
+package de.prob2.jupyter;
+
+import com.google.common.base.MoreObjects;
+
+import org.jetbrains.annotations.NotNull;
+
+public final class PositionedString {
+	private final @NotNull String value;
+	private final int startPosition;
+	
+	public PositionedString(final @NotNull String value, final int startPosition) {
+		super();
+		
+		this.value = value;
+		this.startPosition = startPosition;
+	}
+	
+	public @NotNull String getValue() {
+		return this.value;
+	}
+	
+	public int getStartPosition() {
+		return this.startPosition;
+	}
+	
+	@Override
+	public String toString() {
+		return MoreObjects.toStringHelper(this)
+			.add("value", this.getValue())
+			.add("startPosition", this.getStartPosition())
+			.toString();
+	}
+}
diff --git a/src/main/java/de/prob2/jupyter/SplitArguments.java b/src/main/java/de/prob2/jupyter/SplitArguments.java
index 906b1fb..ed06b9a 100644
--- a/src/main/java/de/prob2/jupyter/SplitArguments.java
+++ b/src/main/java/de/prob2/jupyter/SplitArguments.java
@@ -11,9 +11,9 @@ import com.google.common.base.MoreObjects;
 import org.jetbrains.annotations.NotNull;
 
 public final class SplitArguments {
-	private final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values;
+	private final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull PositionedString>> values;
 	
-	public SplitArguments(final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values) {
+	public SplitArguments(final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull PositionedString>> values) {
 		super();
 		
 		this.values = new HashMap<>(values);
@@ -23,11 +23,11 @@ public final class SplitArguments {
 		return this.values.containsKey(parameter);
 	}
 	
-	public @NotNull List<@NotNull String> get(final @NotNull Parameter<?> parameter) {
+	public @NotNull List<@NotNull PositionedString> get(final @NotNull Parameter<?> parameter) {
 		return this.values.getOrDefault(parameter, Collections.emptyList());
 	}
 	
-	public void add(final @NotNull Parameter<?> parameter, final String value) {
+	public void add(final @NotNull Parameter<?> parameter, final PositionedString value) {
 		this.values.computeIfAbsent(parameter, p -> new ArrayList<>()).add(value);
 	}
 	
diff --git a/src/main/java/de/prob2/jupyter/SplitResult.java b/src/main/java/de/prob2/jupyter/SplitResult.java
index deed42c..5ae4694 100644
--- a/src/main/java/de/prob2/jupyter/SplitResult.java
+++ b/src/main/java/de/prob2/jupyter/SplitResult.java
@@ -4,9 +4,9 @@ import org.jetbrains.annotations.NotNull;
 
 public final class SplitResult {
 	private final @NotNull SplitArguments arguments;
-	private final @NotNull String remaining;
+	private final @NotNull PositionedString remaining;
 	
-	public SplitResult(final @NotNull SplitArguments arguments, final @NotNull String remaining) {
+	public SplitResult(final @NotNull SplitArguments arguments, final @NotNull PositionedString remaining) {
 		super();
 		
 		this.arguments = arguments;
@@ -17,7 +17,7 @@ public final class SplitResult {
 		return this.arguments;
 	}
 	
-	public @NotNull String getRemaining() {
+	public @NotNull PositionedString getRemaining() {
 		return this.remaining;
 	}
 }
-- 
GitLab