-
-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7d2f79b
commit af39db9
Showing
3 changed files
with
228 additions
and
1 deletion.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
src/main/java/org/nixos/idea/util/NixStringIndentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package org.nixos.idea.util; | ||
|
||
import com.intellij.lang.ASTNode; | ||
import com.intellij.psi.tree.IElementType; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.nixos.idea.psi.NixAntiquotation; | ||
import org.nixos.idea.psi.NixIndString; | ||
import org.nixos.idea.psi.NixStdString; | ||
import org.nixos.idea.psi.NixString; | ||
import org.nixos.idea.psi.NixStringPart; | ||
import org.nixos.idea.psi.NixStringText; | ||
import org.nixos.idea.psi.NixTypes; | ||
|
||
/** | ||
* Represents the common indentation in indented strings. | ||
*/ | ||
public final class NixStringIndentation { | ||
|
||
/** | ||
* Instance which represents “no indentation”, and is returned for non-indented strings. | ||
*/ | ||
private static final NixStringIndentation NONE = new NixStringIndentation(0, 0); | ||
|
||
/** | ||
* The maximal amount of spaces removed by {@link #trim(CharSequence, boolean)} from each line. | ||
*/ | ||
private final int myTrimIndentation; | ||
/** | ||
* The amount of spaces prepended by {@link #indent(CharSequence, boolean)} to each line. | ||
*/ | ||
private final int myInsertIndentation; | ||
|
||
private NixStringIndentation(int trimIndentation, int insertIndentation) { | ||
myTrimIndentation = trimIndentation; | ||
myInsertIndentation = insertIndentation; | ||
} | ||
|
||
/** | ||
* Detects the common indentation within the given string. | ||
* The returned indentation needs to be removed during decoding of the string, | ||
* and added during encoding of the string. | ||
* | ||
* @param string the string from which to get the indentation | ||
* @return the detected indentation | ||
*/ | ||
public static @NotNull NixStringIndentation detect(@NotNull NixString string) { | ||
if (string instanceof NixStdString) { | ||
return NONE; | ||
} else if (string instanceof NixIndString) { | ||
enum State {PASS_INDENT, SEARCH_LINE_FEED} | ||
State state = State.PASS_INDENT; | ||
int commonIndentation = Integer.MAX_VALUE; | ||
int currentIndentation = 0; | ||
for (NixStringPart part : string.getStringParts()) { | ||
assert part instanceof NixStringText || part instanceof NixAntiquotation : part.getClass(); | ||
if (part instanceof NixStringText textNode) { | ||
for (ASTNode token = textNode.getNode().getFirstChildNode(); token != null; token = token.getTreeNext()) { | ||
IElementType type = token.getElementType(); | ||
assert type == NixTypes.IND_STR || type == NixTypes.IND_STR_ESCAPE : type; | ||
if (type == NixTypes.IND_STR) { | ||
CharSequence text = token.getChars(); | ||
for (int i = 0; i < text.length(); i++) { | ||
char c = text.charAt(i); | ||
if (c == '\n') { | ||
currentIndentation = 0; | ||
state = State.PASS_INDENT; | ||
} else if (state == State.PASS_INDENT && c == ' ') { | ||
currentIndentation += 1; | ||
} else if (state == State.PASS_INDENT) { | ||
commonIndentation = Math.min(commonIndentation, currentIndentation); | ||
state = State.SEARCH_LINE_FEED; | ||
} | ||
} | ||
} else if (state == State.PASS_INDENT) { | ||
commonIndentation = Math.min(commonIndentation, currentIndentation); | ||
state = State.SEARCH_LINE_FEED; | ||
} | ||
} | ||
} else if (state == State.PASS_INDENT) { | ||
commonIndentation = Math.min(commonIndentation, currentIndentation); | ||
state = State.SEARCH_LINE_FEED; | ||
} | ||
} | ||
if (commonIndentation == Integer.MAX_VALUE) { | ||
int suggestedIndentation = 8; // TODO | ||
return new NixStringIndentation(Integer.MAX_VALUE, suggestedIndentation); | ||
} else { | ||
return new NixStringIndentation(commonIndentation, commonIndentation); | ||
} | ||
} else { | ||
throw new IllegalStateException("Unexpected subclass of NixString: " + string.getClass()); | ||
} | ||
} | ||
|
||
/** | ||
* Remove indentation from given string. | ||
* | ||
* @param text the indented text | ||
* @param firstLine whether the first line of the given text is indented | ||
* @return the given text with the indentation removed | ||
*/ | ||
public @NotNull String trim(@NotNull CharSequence text, boolean firstLine) { | ||
StringBuilder result = new StringBuilder(); | ||
int indentation = firstLine ? 0 : myTrimIndentation; | ||
for (int i = 0; i < text.length(); i++) { | ||
char c = text.charAt(i); | ||
if (c == ' ' && indentation++ < myTrimIndentation) { | ||
continue; | ||
} | ||
indentation = myTrimIndentation; | ||
result.append(c); | ||
} | ||
return result.toString(); | ||
} | ||
|
||
/** | ||
* Indent the given text. | ||
* | ||
* @param text the text which shall be indented | ||
* @param firstLine whether the first line shall be indented | ||
* @return indented version of the given string | ||
*/ | ||
public @NotNull String indent(@NotNull CharSequence text, boolean firstLine) { | ||
StringBuilder result = new StringBuilder(); | ||
String indentation = " ".repeat(myInsertIndentation); | ||
boolean startOfLine = firstLine; | ||
for (int i = 0; i < text.length(); i++) { | ||
char c = text.charAt(i); | ||
if (c == '\n') { | ||
startOfLine = true; | ||
} else if (startOfLine) { | ||
result.append(indentation); | ||
startOfLine = false; | ||
} | ||
result.append(c); | ||
} | ||
return result.toString(); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
if (myTrimIndentation == myInsertIndentation) { | ||
return String.format("detected_indentation(%d)", myInsertIndentation); | ||
} else { | ||
assert myTrimIndentation == Integer.MAX_VALUE : myTrimIndentation; | ||
return String.format("suggested_indentation(%d)", myInsertIndentation); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
src/test/java/org/nixos/idea/util/NixStringIndentationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package org.nixos.idea.util; | ||
|
||
import com.intellij.openapi.project.Project; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.CsvSource; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
import org.nixos.idea._testutil.WithIdeaPlatform; | ||
import org.nixos.idea.psi.NixElementFactory; | ||
import org.nixos.idea.psi.NixString; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
@WithIdeaPlatform.OnEdt | ||
final class NixStringIndentationTest { | ||
|
||
private final Project myProject; | ||
|
||
NixStringIndentationTest(Project project) { | ||
myProject = project; | ||
} | ||
|
||
@ParameterizedTest(name = "[{index}] {0} -> {1}") | ||
@CsvSource(quoteCharacter = '|', textBlock = """ | ||
# Non-indented strings always return the empty string | ||
|""| , detected_indentation(0) | ||
|" a"| , detected_indentation(0) | ||
|" a\n b"| , detected_indentation(0) | ||
# When there are only spaces, we don't know the correct indentation | ||
|''''| , suggested_indentation(0) | ||
|'' ''| , suggested_indentation(0) | ||
|''\n \n ''| , suggested_indentation(0) | ||
# The smallest indentation counts | ||
|''\n a\n b''| , detected_indentation(1) | ||
|''\n a\n b''| , detected_indentation(1) | ||
|''\n a\n b''| , detected_indentation(2) | ||
|''\n a\n ${b}''| , detected_indentation(1) | ||
|''\n a\n ''\\b''| , detected_indentation(1) | ||
# First line counts | ||
|''a\n b''| , detected_indentation(0) | ||
|''${a}\n b''| , detected_indentation(0) | ||
|''''\\a\n b''| , detected_indentation(0) | ||
# But only the first token in a line counts | ||
|'' a${b}''| , detected_indentation(2) | ||
|'' a''\\b''| , detected_indentation(2) | ||
|'' ${a}b''| , detected_indentation(2) | ||
|'' ${a}${b}''| , detected_indentation(2) | ||
|'' ${a}''\\b''| , detected_indentation(2) | ||
|'' ''\\ab''| , detected_indentation(2) | ||
|'' ''\\a${b}''| , detected_indentation(2) | ||
|'' ''\\a''\\b''| , detected_indentation(2) | ||
# Tab and CR are treated as normal characters, not as spaces | ||
# See NixOS/nix#2911 and NixOS/nix#3759 | ||
|''\t''| , detected_indentation(0) | ||
|''\n \t''| , detected_indentation(2) | ||
|''\r\n''| , detected_indentation(0) | ||
|''\n \r\n''| , detected_indentation(2) | ||
# Indentation within interpolations is ignored | ||
|'' ${\n"a"}''| , detected_indentation(2) | ||
|'' ${\n''a''}''| , detected_indentation(2) | ||
""") | ||
void detect(String code, String expectedResult) { | ||
NixString string = NixElementFactory.createString(myProject, code); | ||
assertEquals(expectedResult, NixStringIndentation.detect(string).toString()); | ||
} | ||
|
||
@Test | ||
void indent() { | ||
} | ||
|
||
@Test | ||
void indent_ignores_empty_lines() { | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {false, true}) | ||
void indent_first_line(boolean indentFirstLine) { | ||
} | ||
} |