Skip to content

Commit

Permalink
Detect Indentation (Prototype #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
JojOatXGME committed Jul 15, 2024
1 parent 7d2f79b commit af39db9
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 1 deletion.
149 changes: 149 additions & 0 deletions src/main/java/org/nixos/idea/util/NixStringIndentation.java
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);
}
}
}
1 change: 0 additions & 1 deletion src/main/java/org/nixos/idea/util/NixStringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.psi.NixStringPart;
import org.nixos.idea.psi.NixStringText;
import org.nixos.idea.psi.NixTypes;

Expand Down
79 changes: 79 additions & 0 deletions src/test/java/org/nixos/idea/util/NixStringIndentationTest.java
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) {
}
}

0 comments on commit af39db9

Please sign in to comment.