feat: Language injections
This commit is contained in:
parent
dd2ccf2387
commit
de2384e1b4
10 changed files with 426 additions and 2 deletions
|
@ -17,6 +17,11 @@ Changelog structure reference:
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Zig
|
||||
- Basic language injections in strings
|
||||
|
||||
### Fixed
|
||||
|
||||
- LSP
|
||||
|
|
|
@ -195,6 +195,7 @@ project(":zig") {
|
|||
lsp4ijDep()
|
||||
intellijPlatform {
|
||||
plugin(lsp4ijPluginString)
|
||||
bundledPlugin("org.intellij.intelliLang")
|
||||
}
|
||||
}
|
||||
tasks {
|
||||
|
|
|
@ -51,6 +51,9 @@
|
|||
bundle="zigbrains.Bundle"
|
||||
key="notif-zls-error"
|
||||
id="ZigBrains.ZLS"/>
|
||||
|
||||
<lang.elementManipulator forClass="com.falsepattern.zigbrains.zig.psi.ZigStringLiteral"
|
||||
implementationClass="com.falsepattern.zigbrains.zig.psi.ZigStringElementManipulator"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="com.falsepattern.zigbrains">
|
||||
|
@ -181,6 +184,7 @@ The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, via ZigBr
|
|||
<depends optional="true" config-file="zigbrains-zig-cidr-workspace.xml">com.intellij.cidr.base</depends>
|
||||
<depends optional="true" config-file="zigbrains-zig-debugger.xml">com.intellij.modules.cidr.debugger</depends>
|
||||
<depends optional="true" config-file="zigbrains-zig-clion.xml">com.intellij.modules.clion</depends>
|
||||
<depends optional="true" config-file="zigbrains-zig-intellilang.xml">org.intellij.intelliLang</depends>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<notificationGroup displayType="BALLOON"
|
||||
|
|
|
@ -163,6 +163,10 @@
|
|||
IDENTIFIER='identifier'
|
||||
BUILTINIDENTIFIER='builtin identifier'
|
||||
]
|
||||
|
||||
//Mixins
|
||||
mixin("StringLiteral")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigStringLiteralMixinImpl"
|
||||
implements("StringLiteral")="com.falsepattern.zigbrains.zig.psi.mixins.ZigStringLiteralMixin"
|
||||
}
|
||||
|
||||
Root ::= CONTAINER_DOC_COMMENT? ContainerMembers?
|
||||
|
|
|
@ -96,7 +96,6 @@ string_char= {char_escape}
|
|||
CONTAINER_DOC_COMMENT=("//!" [^\n]* [ \n]*)+
|
||||
DOC_COMMENT=("///" [^\n]* [ \n]*)+
|
||||
LINE_COMMENT="//" [^\n]* | "////" [^\n]*
|
||||
line_string=("\\\\" [^\n]* [ \n]*)+
|
||||
|
||||
FLOAT= "0x" {hex_int} "." {hex_int} ([pP] [-+]? {dec_int})?
|
||||
| {dec_int} "." {dec_int} ([eE] [-+]? {dec_int})?
|
||||
|
@ -112,6 +111,7 @@ IDENTIFIER_PLAIN=[A-Za-z_][A-Za-z0-9_]*
|
|||
BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
|
||||
|
||||
%state STR_LIT
|
||||
%state STR_MULT_LINE
|
||||
%state CHAR_LIT
|
||||
|
||||
%state ID_QUOT
|
||||
|
@ -261,7 +261,9 @@ BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
|
|||
<YYINITIAL> "\"" { yybegin(STR_LIT); }
|
||||
<STR_LIT> {string_char}*"\"" { yybegin(YYINITIAL); return STRING_LITERAL_SINGLE; }
|
||||
<STR_LIT> [^] { yypushback(1); yybegin(UNT_QUOT); }
|
||||
<YYINITIAL> {line_string}+ { return STRING_LITERAL_MULTI; }
|
||||
<YYINITIAL> "\\\\" { yypushback(2); yybegin(STR_MULT_LINE); }
|
||||
<STR_MULT_LINE> [^\n]* [ \n]* "\\\\" { }
|
||||
<STR_MULT_LINE> [^\n]* \n { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
|
||||
|
||||
<YYINITIAL> {IDENTIFIER_PLAIN} { return IDENTIFIER; }
|
||||
<YYINITIAL> "@\"" { yybegin(ID_QUOT); }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.falsepattern.zigbrains.zig.intellilang;
|
||||
|
||||
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
|
||||
import com.intellij.psi.PsiLanguageInjectionHost;
|
||||
import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ZigLanguageInjectionSupport extends AbstractLanguageInjectionSupport {
|
||||
@Override
|
||||
public @NotNull String getId() {
|
||||
return "zig";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> @NotNull [] getPatternClasses() {
|
||||
return new Class[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicableTo(PsiLanguageInjectionHost host) {
|
||||
return host instanceof ZigStringLiteral;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package com.falsepattern.zigbrains.zig.psi;
|
||||
|
||||
import com.falsepattern.zigbrains.zig.ZigFileType;
|
||||
import com.falsepattern.zigbrains.zig.util.PsiUtil;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.AbstractElementManipulator;
|
||||
import com.intellij.psi.PsiFileFactory;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ZigStringElementManipulator extends AbstractElementManipulator<ZigStringLiteral> {
|
||||
|
||||
|
||||
@Override
|
||||
public @Nullable ZigStringLiteral handleContentChange(@NotNull ZigStringLiteral element, @NotNull TextRange range, String newContent)
|
||||
throws IncorrectOperationException {
|
||||
assert (new TextRange(0, element.getTextLength())).contains(range);
|
||||
val originalContext = element.getText();
|
||||
val isMulti = element.getStringLiteralMulti() != null;
|
||||
val elementRange = getRangeInElement(element);
|
||||
var replacement = originalContext.substring(elementRange.getStartOffset(),
|
||||
range.getStartOffset()) +
|
||||
(isMulti ? newContent : escape(newContent)) +
|
||||
originalContext.substring(range.getEndOffset(),
|
||||
elementRange.getEndOffset());
|
||||
val psiFileFactory = PsiFileFactory.getInstance(element.getProject());
|
||||
if (isMulti) {
|
||||
val column = StringUtil.offsetToLineColumn(element.getContainingFile().getText(), element.getTextOffset()).column;
|
||||
val pfxB = new StringBuilder(column + 2);
|
||||
for (int i = 0; i < column; i++) {
|
||||
pfxB.append(' ');
|
||||
}
|
||||
pfxB.append("\\\\");
|
||||
val pfx = pfxB.toString();
|
||||
replacement = Arrays.stream(replacement.split("(\\r\\n|\\r|\\n)")).map(line -> pfx + line).collect(
|
||||
Collectors.joining("\n"));
|
||||
} else {
|
||||
replacement = "\"" + replacement + "\"";
|
||||
}
|
||||
val dummy = psiFileFactory.createFileFromText("dummy." + ZigFileType.INSTANCE.getDefaultExtension(),
|
||||
ZigFileType.INSTANCE, "const x = \n" + replacement + "\n;");
|
||||
val stringLiteral = ((ZigPrimaryTypeExpr)((ZigContainerMembers) dummy.getFirstChild()).getContainerDeclarationsList().get(0).getDeclList().get(0).getGlobalVarDecl().getExpr()).getStringLiteral();
|
||||
return (ZigStringLiteral) element.replace(stringLiteral);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull TextRange getRangeInElement(@NotNull ZigStringLiteral element) {
|
||||
if (element.getStringLiteralSingle() != null) {
|
||||
return new TextRange(1, element.getTextLength() - 1);
|
||||
}
|
||||
return super.getRangeInElement(element);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static String escape(String input) {
|
||||
val bytes = input.getBytes(StandardCharsets.UTF_8);
|
||||
val result = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
byte c = bytes[i];
|
||||
switch (c) {
|
||||
case '\n' -> result.write("\\n".getBytes(StandardCharsets.UTF_8));
|
||||
case '\r' -> result.write("\\r".getBytes(StandardCharsets.UTF_8));
|
||||
case '\t' -> result.write("\\t".getBytes(StandardCharsets.UTF_8));
|
||||
case '\\' -> result.write("\\\\".getBytes(StandardCharsets.UTF_8));
|
||||
case '"' -> result.write("\\\"".getBytes(StandardCharsets.UTF_8));
|
||||
case '\'', ' ', '!' -> result.write(c);
|
||||
default -> {
|
||||
if (c >= '#' && c <= '&' ||
|
||||
c >= '(' && c <= '[' ||
|
||||
c >= ']' && c <= '~') {
|
||||
result.write(c);
|
||||
} else {
|
||||
result.write("\\x".getBytes(StandardCharsets.UTF_8));
|
||||
result.write(String.format("%02x", c).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static String unescape(String input, boolean[] noErrors) {
|
||||
noErrors[0] = true;
|
||||
val result = new ByteArrayOutputStream();
|
||||
val bytes = input.getBytes(StandardCharsets.UTF_8);
|
||||
val len = bytes.length;
|
||||
loop:
|
||||
for (int i = 0; i < len; i++) {
|
||||
byte c = bytes[i];
|
||||
switch (c) {
|
||||
case '\\' -> {
|
||||
i++;
|
||||
if (i < len) {
|
||||
switch (input.charAt(i)) {
|
||||
case 'n' -> result.write('\n');
|
||||
case 'r' -> result.write('\r');
|
||||
case 't' -> result.write('\t');
|
||||
case '\\' -> result.write('\\');
|
||||
case '"' -> result.write('"');
|
||||
case 'x' -> {
|
||||
if (i + 2 < len) {
|
||||
try {
|
||||
int b1 = decodeHex(bytes[i + 1]);
|
||||
int b2 = decodeHex(bytes[i + 2]);
|
||||
result.write((b1 << 4) | b2);
|
||||
} catch (NumberFormatException ignored) {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
case 'u' -> {
|
||||
i++;
|
||||
if (i >= len || bytes[i] != '{') {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
int codePoint = 0;
|
||||
try {
|
||||
while (i < len && bytes[i] != '}') {
|
||||
codePoint <<= 4;
|
||||
codePoint |= decodeHex(bytes[i + 1]);
|
||||
i++;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
if (i >= len) {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
result.write(Character.toString(codePoint).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
default -> {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
noErrors[0] = false;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
default -> result.write(c);
|
||||
}
|
||||
}
|
||||
return result.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static String unescapeWithLengthMappings(String input, List<Integer> inputOffsets, boolean[] noErrors) {
|
||||
String output = "";
|
||||
int lastOutputLength = 0;
|
||||
int inputOffset = 0;
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
output = unescape(input.substring(0, i + 1), noErrors);
|
||||
val outputLength = output.length();
|
||||
if (noErrors[0]) {
|
||||
inputOffset = i;
|
||||
}
|
||||
while (lastOutputLength < outputLength) {
|
||||
inputOffsets.add(inputOffset);
|
||||
lastOutputLength++;
|
||||
inputOffset = i + 1;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private static int decodeHex(int b) {
|
||||
if (b >= '0' && b <= '9') {
|
||||
return b - '0';
|
||||
}
|
||||
if (b >= 'A' && b <= 'F') {
|
||||
return b - 'A' + 10;
|
||||
}
|
||||
if (b >= 'a' && b <= 'f') {
|
||||
return b - 'a' + 10;
|
||||
}
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package com.falsepattern.zigbrains.zig.psi.impl.mixins;
|
||||
|
||||
import com.falsepattern.zigbrains.zig.psi.ZigStringElementManipulator;
|
||||
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
|
||||
import com.intellij.extapi.psi.ASTWrapperPsiElement;
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.LiteralTextEscaper;
|
||||
import com.intellij.psi.PsiLanguageInjectionHost;
|
||||
import com.intellij.psi.impl.source.tree.LeafElement;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement implements ZigStringLiteral {
|
||||
public ZigStringLiteralMixinImpl(@NotNull ASTNode node) {
|
||||
super(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidHost() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public PsiLanguageInjectionHost updateText(@NotNull String text) {
|
||||
if (this.getStringLiteralSingle() instanceof LeafElement leaf) {
|
||||
leaf.replaceWithText(text);
|
||||
} else if (this.getStringLiteralMulti() instanceof LeafElement leaf) {
|
||||
leaf.replaceWithText(text);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull LiteralTextEscaper<ZigStringLiteral> createLiteralTextEscaper() {
|
||||
if (this.getStringLiteralSingle() != null) {
|
||||
return new LiteralTextEscaper<>(this) {
|
||||
private final List<Integer> inputOffsets = new ArrayList<>();
|
||||
@Override
|
||||
public boolean decode(@NotNull TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
|
||||
boolean[] noErrors = new boolean[] {true};
|
||||
outChars.append(ZigStringElementManipulator.unescapeWithLengthMappings(rangeInsideHost.substring(myHost.getText()), inputOffsets, noErrors));
|
||||
return noErrors[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOffsetInHost(int offsetInDecoded, @NotNull TextRange rangeInsideHost) {
|
||||
int size = inputOffsets.size();
|
||||
int realOffset = 0;
|
||||
if (size == 0) {
|
||||
realOffset = rangeInsideHost.getStartOffset() + offsetInDecoded;
|
||||
} else if (offsetInDecoded >= size) {
|
||||
realOffset = rangeInsideHost.getStartOffset() + inputOffsets.get(size - 1) +
|
||||
(offsetInDecoded - (size - 1));
|
||||
} else {
|
||||
realOffset = rangeInsideHost.getStartOffset() + inputOffsets.get(offsetInDecoded);
|
||||
}
|
||||
return realOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull TextRange getRelevantTextRange() {
|
||||
return new TextRange(1, myHost.getTextLength() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOneLine() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} else if (this.getStringLiteralMulti() != null) {
|
||||
return new LiteralTextEscaper<>(this) {
|
||||
@Override
|
||||
public boolean decode(@NotNull TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
|
||||
val str = myHost.getText();
|
||||
boolean inMultiLineString = false;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
val cI = str.charAt(i);
|
||||
if (!inMultiLineString) {
|
||||
if (cI == '\\' &&
|
||||
i + 1 < str.length() &&
|
||||
str.charAt(i + 1) == '\\') {
|
||||
i++;
|
||||
inMultiLineString = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (cI == '\r') {
|
||||
outChars.append('\n');
|
||||
if (i + 1 < str.length() && str.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
inMultiLineString = false;
|
||||
continue;
|
||||
}
|
||||
if (cI == '\n') {
|
||||
outChars.append('\n');
|
||||
inMultiLineString = false;
|
||||
continue;
|
||||
}
|
||||
outChars.append(cI);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOffsetInHost(int offsetInDecoded, @NotNull TextRange rangeInsideHost) {
|
||||
val str = myHost.getText();
|
||||
boolean inMultiLineString = false;
|
||||
int i = rangeInsideHost.getStartOffset();
|
||||
for (; i < rangeInsideHost.getEndOffset() && offsetInDecoded > 0; i++) {
|
||||
val cI = str.charAt(i);
|
||||
if (!inMultiLineString) {
|
||||
if (cI == '\\' &&
|
||||
i + 1 < str.length() &&
|
||||
str.charAt(i + 1) == '\\') {
|
||||
i++;
|
||||
inMultiLineString = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (cI == '\r') {
|
||||
offsetInDecoded--;
|
||||
if (i + 1 < str.length() && str.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
inMultiLineString = false;
|
||||
continue;
|
||||
}
|
||||
if (cI == '\n') {
|
||||
offsetInDecoded--;
|
||||
inMultiLineString = false;
|
||||
continue;
|
||||
}
|
||||
offsetInDecoded--;
|
||||
}
|
||||
if (offsetInDecoded != 0)
|
||||
return -1;
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOneLine() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.falsepattern.zigbrains.zig.psi.mixins;
|
||||
|
||||
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
|
||||
import com.intellij.extapi.psi.ASTWrapperPsiElement;
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.LiteralTextEscaper;
|
||||
import com.intellij.psi.PsiLanguageInjectionHost;
|
||||
import com.intellij.psi.impl.source.tree.LeafElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface ZigStringLiteralMixin extends PsiLanguageInjectionHost {
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
~ Copyright 2023-2024 FalsePattern
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<idea-plugin package="com.falsepattern.zigbrains.zig.intellilang">
|
||||
<depends>org.intellij.intelliLang</depends>
|
||||
<extensions defaultExtensionNs="org.intellij.intelliLang">
|
||||
<languageSupport implementation="com.falsepattern.zigbrains.zig.intellilang.ZigLanguageInjectionSupport"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
Loading…
Add table
Reference in a new issue