-
-
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.
Merge pull request #74 from NixOS/utilities
Some Tools and Fundamentals
- Loading branch information
Showing
14 changed files
with
899 additions
and
7 deletions.
There are no files selected for viewing
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
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
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,95 @@ | ||
package org.nixos.idea.psi; | ||
|
||
import com.intellij.lang.ASTNode; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.psi.PsiElement; | ||
import com.intellij.psi.PsiFileFactory; | ||
import com.intellij.psi.TokenType; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
import org.nixos.idea.file.NixFile; | ||
import org.nixos.idea.file.NixFileType; | ||
|
||
import java.util.Objects; | ||
|
||
public final class NixElementFactory { | ||
|
||
private NixElementFactory() {} // Cannot be instantiated | ||
|
||
public static @NotNull NixString createString(@NotNull Project project, @NotNull String code) { | ||
return createElement(project, NixString.class, "", code, ""); | ||
} | ||
|
||
public static @NotNull NixAttr createAttr(@NotNull Project project, @NotNull String code) { | ||
return createElement(project, NixAttr.class, "x.", code, ""); | ||
} | ||
|
||
public static @NotNull NixAttrPath createAttrPath(@NotNull Project project, @NotNull String code) { | ||
return createElement(project, NixAttrPath.class, "x.", code, ""); | ||
} | ||
|
||
public static @NotNull NixBind createBind(@NotNull Project project, @NotNull String code) { | ||
return createElement(project, NixBind.class, "{", code, "}"); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
public static <T extends NixExpr> @NotNull T createExpr(@NotNull Project project, @NotNull String code) { | ||
return (T) createElement(project, NixExpr.class, "", code, ""); | ||
} | ||
|
||
public static <T extends NixPsiElement> @NotNull T createElement( | ||
@NotNull Project project, @NotNull Class<T> type, | ||
@NotNull String prefix, @NotNull String text, @NotNull String suffix) { | ||
return Objects.requireNonNull( | ||
createElementOrNull(project, type, prefix, text, suffix), | ||
"Invalid " + type.getSimpleName() + ": " + text); | ||
} | ||
|
||
private static <T extends NixPsiElement> @Nullable T createElementOrNull( | ||
@NotNull Project project, @NotNull Class<T> type, | ||
@NotNull String prefix, @NotNull String text, @NotNull String suffix) { | ||
NixFile file = createFile(project, prefix + text + suffix); | ||
ASTNode current = file.getNode().getFirstChildNode(); | ||
int offset = 0; | ||
while (current != null && offset <= prefix.length()) { | ||
int length = current.getTextLength(); | ||
// Check if we have found the right element | ||
if (offset == prefix.length() && length == text.length()) { | ||
PsiElement psi = current.getPsi(); | ||
if (type.isInstance(psi)) { | ||
return containsErrors(current) ? null : type.cast(psi); | ||
} | ||
} | ||
// Check if we should go into or over this element | ||
if (offset + length <= prefix.length()) { | ||
offset += length; | ||
current = current.getTreeNext(); | ||
} else { | ||
current = current.getFirstChildNode(); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private static boolean containsErrors(ASTNode node) { | ||
ASTNode current = node.getFirstChildNode(); | ||
while (current != null) { | ||
if (current.getElementType() == TokenType.ERROR_ELEMENT) { | ||
return true; | ||
} | ||
ASTNode next = current.getFirstChildNode(); | ||
if (next == null) { | ||
next = current.getTreeNext(); | ||
while (next == null && (current = current.getTreeParent()) != node) { | ||
next = current.getTreeNext(); | ||
} | ||
} | ||
current = next; | ||
} | ||
return false; | ||
} | ||
|
||
public static @NotNull NixFile createFile(@NotNull Project project, @NotNull String code) { | ||
return (NixFile) PsiFileFactory.getInstance(project).createFileFromText("dummy.nix", NixFileType.INSTANCE, code); | ||
} | ||
} |
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,8 @@ | ||
package org.nixos.idea.psi; | ||
|
||
import com.intellij.psi.PsiElement; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
public interface NixPsiElement extends PsiElement { | ||
<T> T accept(@NotNull NixElementVisitor<T> visitor); | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/org/nixos/idea/psi/impl/AbstractNixPsiElement.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,14 @@ | ||
package org.nixos.idea.psi.impl; | ||
|
||
import com.intellij.extapi.psi.ASTWrapperPsiElement; | ||
import com.intellij.lang.ASTNode; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.nixos.idea.psi.NixPsiElement; | ||
|
||
abstract class AbstractNixPsiElement extends ASTWrapperPsiElement implements NixPsiElement { | ||
|
||
AbstractNixPsiElement(@NotNull ASTNode node) { | ||
super(node); | ||
} | ||
|
||
} |
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,131 @@ | ||
package org.nixos.idea.util; | ||
|
||
import com.intellij.lang.ASTNode; | ||
import com.intellij.psi.tree.IElementType; | ||
import org.jetbrains.annotations.Contract; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.nixos.idea.psi.NixStringText; | ||
import org.nixos.idea.psi.NixTypes; | ||
|
||
/** | ||
* Utilities for strings in the Nix Expression Language. | ||
*/ | ||
public final class NixStringUtil { | ||
|
||
private NixStringUtil() {} // Cannot be instantiated | ||
|
||
/** | ||
* Returns the source code for a string in the Nix Expression Language. | ||
* When the returned string is evaluated by a Nix interpreter, the result matches the sting given to this method. | ||
* The returned string expression is always a double-quoted string. | ||
* | ||
* <h4>Example</h4> | ||
* <pre>{@code System.out.println(quote("This should be escaped: ${}"));}</pre> | ||
* The code above prints the following: | ||
* <pre>"This should be escaped: \${}"</pre> | ||
* | ||
* @param unescaped The raw string which shall be the result when the expression is evaluated. | ||
* @return Source code for a Nix expression which evaluates to the given string. | ||
*/ | ||
@Contract(pure = true) | ||
public static @NotNull String quote(@NotNull CharSequence unescaped) { | ||
StringBuilder builder = new StringBuilder(); | ||
builder.append('"'); | ||
escape(builder, unescaped); | ||
builder.append('"'); | ||
return builder.toString(); | ||
} | ||
|
||
/** | ||
* Escapes the given string for use in a double-quoted string expression in the Nix Expression Language. | ||
* Note that it is not safe to combine the results of two method calls with arbitrary input. | ||
* For example, the following code would generate a broken result. | ||
* <pre>{@code | ||
* StringBuilder b1 = new StringBuilder(), b2 = new StringBuilder(); | ||
* NixStringUtil.escape(b1, "$"); | ||
* NixStringUtil.escape(b2, "{''}"); | ||
* System.out.println(b1.toString() + b2.toString()); | ||
* }</pre> | ||
* The result would be the following broken Nix code. | ||
* <pre> | ||
* "${''}" | ||
* </pre> | ||
* | ||
* @param builder The target string builder. The result will be appended to the given string builder. | ||
* @param unescaped The raw string which shall be escaped. | ||
*/ | ||
public static void escape(@NotNull StringBuilder builder, @NotNull CharSequence unescaped) { | ||
for (int charIndex = 0; charIndex < unescaped.length(); charIndex++) { | ||
char nextChar = unescaped.charAt(charIndex); | ||
switch (nextChar) { | ||
case '"': | ||
case '\\': | ||
builder.append('\\').append(nextChar); | ||
break; | ||
case '{': | ||
if (builder.charAt(builder.length() - 1) == '$') { | ||
builder.setCharAt(builder.length() - 1, '\\'); | ||
builder.append('$').append('{'); | ||
} else { | ||
builder.append('{'); | ||
} | ||
break; | ||
case '\n': | ||
builder.append('\\').append('n'); | ||
break; | ||
case '\r': | ||
builder.append('\\').append('r'); | ||
break; | ||
case '\t': | ||
builder.append('\\').append('t'); | ||
break; | ||
default: | ||
builder.append(nextChar); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Returns the content of the given part of a string in the Nix Expression Language. | ||
* All escape sequences are resolved. | ||
* | ||
* @param textNode A part of a string. | ||
* @return The resulting string after resolving all escape sequences. | ||
*/ | ||
public static @NotNull String parse(@NotNull NixStringText textNode) { | ||
StringBuilder builder = new StringBuilder(); | ||
for (ASTNode child = textNode.getNode().getFirstChildNode(); child != null; child = child.getTreeNext()) { | ||
parse(builder, child); | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
private static void parse(@NotNull StringBuilder builder, @NotNull ASTNode token) { | ||
CharSequence text = token.getChars(); | ||
IElementType type = token.getElementType(); | ||
if (type == NixTypes.STR || type == NixTypes.IND_STR) { | ||
builder.append(text); | ||
} else if (type == NixTypes.STR_ESCAPE) { | ||
assert text.length() == 2 && text.charAt(0) == '\\' : text; | ||
char c = text.charAt(1); | ||
builder.append(unescape(c)); | ||
} else if (type == NixTypes.IND_STR_ESCAPE) { | ||
assert text.length() == 3 && ("''$".contentEquals(text) || "'''".contentEquals(text)) || | ||
text.length() == 4 && "''\\".contentEquals(text.subSequence(0, 3)) : text; | ||
char c = text.charAt(text.length() - 1); | ||
builder.append(unescape(c)); | ||
} else { | ||
throw new IllegalStateException("Unexpected token in string: " + token); | ||
} | ||
} | ||
|
||
private static char unescape(char c) { | ||
return switch (c) { | ||
case 'n' -> '\n'; | ||
case 'r' -> '\r'; | ||
case 't' -> '\t'; | ||
default -> c; | ||
}; | ||
} | ||
} |
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
Oops, something went wrong.