backport: 19.2.0

This commit is contained in:
FalsePattern 2024-10-26 18:26:59 +02:00
parent e7f80edfd0
commit 3d76972f80
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
14 changed files with 691 additions and 130 deletions

View file

@ -17,6 +17,19 @@ Changelog structure reference:
## [Unreleased] ## [Unreleased]
## [19.2.0]
### Added
- Zig
- Enter key handling in strings and multi line strings
- Intentions for converting between multi line and quoted strings
### Fixed
- Zig
- Multiline string language injections broke when editing the injected text
## [19.1.0] ## [19.1.0]
### Added ### Added

View file

@ -11,7 +11,7 @@ baseIDE=clion
ideaVersion=2023.2.8 ideaVersion=2023.2.8
clionVersion=2023.2.5 clionVersion=2023.2.5
pluginVersion=19.1.0 pluginVersion=19.2.0
# Gradle Releases -> https://github.com/gradle/gradle/releases # Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion=8.10.2 gradleVersion=8.10.2

View file

@ -32,6 +32,24 @@
<lang.formatter language="Zig" implementationClass="com.falsepattern.zigbrains.zig.formatter.ZigFormattingModelBuilder"/> <lang.formatter language="Zig" implementationClass="com.falsepattern.zigbrains.zig.formatter.ZigFormattingModelBuilder"/>
<enterHandlerDelegate
id="ZigEnterInTextBlockHandler"
implementation="com.falsepattern.zigbrains.zig.editing.ZigEnterInTextBlockHandler" />
<enterHandlerDelegate
id="ZigEnterInQuotedStringHandler"
implementation="com.falsepattern.zigbrains.zig.editing.ZigEnterInQuotedStringHandler" />
<intentionAction>
<language>Zig</language>
<className>com.falsepattern.zigbrains.zig.intentions.MakeStringMultiline</className>
<category>Zig intentions</category>
</intentionAction>
<intentionAction>
<language>Zig</language>
<className>com.falsepattern.zigbrains.zig.intentions.MakeStringQuoted</className>
<category>Zig intentions</category>
</intentionAction>
<postStartupActivity implementation="com.falsepattern.zigbrains.zig.lsp.ZLSStartupActivity"/> <postStartupActivity implementation="com.falsepattern.zigbrains.zig.lsp.ZLSStartupActivity"/>
<!-- LSP textDocument/signatureHelp --> <!-- LSP textDocument/signatureHelp -->

View file

@ -30,6 +30,11 @@ import static com.intellij.psi.StringEscapesTokenTypes.*;
%implements FlexLexer %implements FlexLexer
%function advance %function advance
%type IElementType %type IElementType
%{
public ZigStringLexer() {
}
%}
hex=[0-9a-fA-F] hex=[0-9a-fA-F]

View file

@ -0,0 +1,63 @@
package com.falsepattern.zigbrains.zig.editing;
import com.falsepattern.zigbrains.zig.parser.ZigFile;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.falsepattern.zigbrains.zig.psi.ZigTypes;
import com.falsepattern.zigbrains.zig.stringlexer.ZigStringLexer;
import com.falsepattern.zigbrains.zig.util.PsiTextUtil;
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.editorActions.JavaLikeQuoteHandler;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
import com.intellij.lang.ASTNode;
import com.intellij.lexer.StringLiteralLexer;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.StringEscapesTokenTypes;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.tree.IElementType;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import java.io.StringReader;
import java.util.ArrayList;
@RequiredArgsConstructor
public class ZigEnterInQuotedStringHandler extends EnterHandlerDelegateAdapter {
@Override
public Result preprocessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull Ref<Integer> caretOffsetRef,
@NotNull Ref<Integer> caretAdvanceRef,
@NotNull DataContext dataContext,
EditorActionHandler originalHandler) {
if (!(file instanceof ZigFile)) {
return Result.Continue;
}
val caretOffset = (int)caretOffsetRef.get();
var psiAtOffset = file.findElementAt(caretOffset);
if (psiAtOffset instanceof LeafPsiElement leaf) {
if ( leaf.getElementType() == ZigTypes.STRING_LITERAL_SINGLE) {
psiAtOffset = leaf.getParent();
}
}
if (psiAtOffset instanceof ZigStringLiteral str &&
!str.isMultiLine() &&
str.getTextOffset() < caretOffset) {
PsiTextUtil.splitString(editor, str, caretOffset, true);
return Result.Stop;
}
return Result.Continue;
}
}

View file

@ -0,0 +1,97 @@
package com.falsepattern.zigbrains.zig.editing;
import com.falsepattern.zigbrains.zig.parser.ZigFile;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@RequiredArgsConstructor
public class ZigEnterInTextBlockHandler extends EnterHandlerDelegateAdapter {
@Override
public Result preprocessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull Ref<Integer> caretOffsetRef,
@NotNull Ref<Integer> caretAdvanceRef,
@NotNull DataContext dataContext,
EditorActionHandler originalHandler) {
if (!(file instanceof ZigFile)) {
return Result.Continue;
}
for (val assistant: ZigMultiLineAssistant.Assistants.ASSISTANTS) {
val result = preprocessEnterWithAssistant(file, editor, assistant);
if (result != null)
return result;
}
return Result.Continue;
}
private static <T extends PsiElement> Result preprocessEnterWithAssistant(@NotNull PsiFile file,
@NotNull Editor editor,
ZigMultiLineAssistant<T> assistant) {
val offset = editor.getCaretModel().getOffset();
val textBlock = getTextBlockAt(file, offset, assistant);
if (textBlock == null) {
return null;
}
val textBlockOffset = textBlock.getTextOffset();
val document = editor.getDocument();
val textBlockLine = document.getLineNumber(textBlockOffset);
val textBlockLineStart = document.getLineStartOffset(textBlockLine);
val indentPre = textBlockOffset - textBlockLineStart;
val project = textBlock.getProject();
val lineNumber = document.getLineNumber(offset);
val lineStartOffset = document.getLineStartOffset(lineNumber);
val text = document.getText(new TextRange(lineStartOffset, offset + 1));
val parts = new ArrayList<>(StringUtil.split(text, assistant.prefix));
if (parts.size() <= 1)
return Result.Continue;
if (parts.size() > 2) {
val sb = new StringBuilder();
sb.append(parts.get(1));
while (parts.size() > 2) {
sb.append(assistant.prefix);
sb.append(parts.remove(2));
}
parts.set(1, sb.toString());
}
val indentPost = measureSpaces(parts.get(1));
val newLine = '\n' + StringUtil.repeatSymbol(' ', indentPre) + assistant.prefix + StringUtil.repeatSymbol(' ', indentPost);
document.insertString(offset, newLine);
PsiDocumentManager.getInstance(project).commitDocument(document);
editor.getCaretModel().moveToOffset(offset + newLine.length());
return Result.Stop;
}
private static int measureSpaces(String str) {
for (int i = 0; i < str.length(); i++) {
val c = str.charAt(i);
switch (c) {
case ' ':
case '\t':
continue;
default:
return i;
}
}
return str.length();
}
private static <T extends PsiElement> T getTextBlockAt(PsiFile file, int offset, ZigMultiLineAssistant<T> assistant) {
val psiAtOffset = file.findElementAt(offset);
if (psiAtOffset == null)
return null;
return assistant.acceptPSI(psiAtOffset);
}
}

View file

@ -0,0 +1,61 @@
package com.falsepattern.zigbrains.zig.editing;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.falsepattern.zigbrains.zig.psi.ZigTypes;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.tree.IElementType;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@RequiredArgsConstructor
public abstract class ZigMultiLineAssistant<T extends PsiElement> {
public static class Assistants {
private Assistants() {}
public static final List<ZigMultiLineAssistant<?>> ASSISTANTS = List.of(new StringAssistant(),
new CommentAssistant("//", ZigTypes.LINE_COMMENT),
new CommentAssistant("///", ZigTypes.DOC_COMMENT),
new CommentAssistant("//!", ZigTypes.CONTAINER_DOC_COMMENT));
}
public final String prefix;
public abstract T acceptPSI(@NotNull PsiElement element);
public static class StringAssistant extends ZigMultiLineAssistant<ZigStringLiteral> {
public StringAssistant() {
super("\\\\");
}
@Override
public ZigStringLiteral acceptPSI(final @NotNull PsiElement element) {
final PsiElement candidate;
if (element instanceof LeafPsiElement leaf) {
if (leaf.getElementType() == ZigTypes.STRING_LITERAL_MULTI) {
candidate = leaf.getParent();
} else {
return null;
}
} else {
candidate = element;
}
return candidate instanceof ZigStringLiteral str && str.isMultiLine() ? str : null;
}
}
public static class CommentAssistant extends ZigMultiLineAssistant<PsiComment> {
private final IElementType tokenType;
public CommentAssistant(String prefix, IElementType type) {
super(prefix);
tokenType = type;
}
@Override
public PsiComment acceptPSI(@NotNull PsiElement element) {
return element instanceof PsiComment comment && comment.getTokenType() == tokenType ? comment : null;
}
}
}

View file

@ -0,0 +1,44 @@
package com.falsepattern.zigbrains.zig.intentions;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.falsepattern.zigbrains.zig.psi.ZigTypes;
import com.falsepattern.zigbrains.zig.util.PsiTextUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import lombok.val;
import org.jetbrains.annotations.NotNull;
public class MakeStringMultiline extends PsiElementBaseIntentionAction implements IntentionAction {
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
val str = PsiTreeUtil.getParentOfType(element, ZigStringLiteral.class);
return str != null && !str.isMultiLine();
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element)
throws IncorrectOperationException {
val str = PsiTreeUtil.getParentOfType(element, ZigStringLiteral.class);
if (str == null)
return;
PsiTextUtil.splitString(editor, str, editor.getCaretModel().getOffset(), false);
}
@Override
public @NotNull @IntentionName String getText() {
return getFamilyName();
}
@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return "Convert to multiline";
}
}

View file

@ -0,0 +1,98 @@
package com.falsepattern.zigbrains.zig.intentions;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.falsepattern.zigbrains.zig.util.PsiTextUtil;
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.formatting.CoreFormatterUtil;
import com.intellij.formatting.FormatterEx;
import com.intellij.formatting.FormattingModel;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.MathUtil;
import lombok.val;
import org.jetbrains.annotations.NotNull;
public class MakeStringQuoted extends PsiElementBaseIntentionAction implements IntentionAction {
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
val str = PsiTreeUtil.getParentOfType(element, ZigStringLiteral.class);
return str != null && str.isMultiLine();
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element)
throws IncorrectOperationException {
val document = editor.getDocument();
val file = element.getContainingFile();
val str = PsiTreeUtil.getParentOfType(element, ZigStringLiteral.class);
if (str == null)
return;
val escaper = str.createLiteralTextEscaper();
val contentRange = escaper.getRelevantTextRange();
val contentStart = contentRange.getStartOffset();
val contentEnd = contentRange.getEndOffset();
val fullRange = str.getTextRange();
var caretOffset = editor.getCaretModel().getOffset();
val prefix = new TextRange(contentStart, Math.max(contentStart, caretOffset - fullRange.getStartOffset()));
val suffix = new TextRange(Math.min(contentEnd, caretOffset - fullRange.getStartOffset()), contentEnd);
val sb = new StringBuilder();
escaper.decode(prefix, sb);
val prefixStr = ZigStringUtil.escape(sb.toString());
sb.setLength(0);
escaper.decode(suffix, sb);
val suffixStr = ZigStringUtil.escape(sb.toString());
val stringRange = document.createRangeMarker(fullRange.getStartOffset(), fullRange.getEndOffset());
stringRange.setGreedyToRight(true);
document.deleteString(stringRange.getStartOffset(), stringRange.getEndOffset());
val documentText = document.getCharsSequence();
boolean addSpace = true;
int scanStart = stringRange.getEndOffset();
int scanEnd = scanStart;
loop:
while (scanEnd < documentText.length()) {
switch (documentText.charAt(scanEnd)) {
case ' ', '\t', '\r', '\n':
break;
case ',', ';':
addSpace = false;
default:
break loop;
}
scanEnd++;
}
if (scanEnd > scanStart) {
if (addSpace) {
document.replaceString(scanStart, scanEnd, " ");
} else {
document.deleteString(scanStart, scanEnd);
}
}
document.insertString(stringRange.getEndOffset(), "\"");
document.insertString(stringRange.getEndOffset(), prefixStr);
caretOffset = stringRange.getEndOffset();
document.insertString(stringRange.getEndOffset(), suffixStr);
document.insertString(stringRange.getEndOffset(), "\"");
stringRange.dispose();
editor.getCaretModel().moveToOffset(caretOffset);
}
@Override
public @NotNull @IntentionName String getText() {
return getFamilyName();
}
@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return "Convert to quoted";
}
}

View file

@ -2,12 +2,11 @@ package com.falsepattern.zigbrains.zig.psi;
import com.falsepattern.zigbrains.zig.ZigFileType; import com.falsepattern.zigbrains.zig.ZigFileType;
import com.falsepattern.zigbrains.zig.util.PsiTextUtil; import com.falsepattern.zigbrains.zig.util.PsiTextUtil;
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.AbstractElementManipulator; import com.intellij.psi.AbstractElementManipulator;
import com.intellij.psi.PsiFileFactory; import com.intellij.psi.PsiFileFactory;
import com.intellij.util.IncorrectOperationException; import com.intellij.util.IncorrectOperationException;
import lombok.SneakyThrows;
import lombok.val; import lombok.val;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -16,27 +15,54 @@ import java.util.Arrays;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ZigStringElementManipulator extends AbstractElementManipulator<ZigStringLiteral> { public class ZigStringElementManipulator extends AbstractElementManipulator<ZigStringLiteral> {
private enum InjectTriState {
NotYet,
Incomplete,
Complete
}
@Override @Override
public @Nullable ZigStringLiteral handleContentChange(@NotNull ZigStringLiteral element, @NotNull TextRange range, String newContent) public @Nullable ZigStringLiteral handleContentChange(@NotNull ZigStringLiteral element, @NotNull TextRange range, String newContent)
throws IncorrectOperationException { throws IncorrectOperationException {
assert (new TextRange(0, element.getTextLength())).contains(range);
val originalContext = element.getText(); val originalContext = element.getText();
val isMulti = element.isMultiLine(); val isMulti = element.isMultiLine();
val elementRange = getRangeInElement(element); final CharSequence replacement;
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) { if (isMulti) {
val column = StringUtil.offsetToLineColumn(element.getContainingFile().getText(), element.getTextOffset()).column; val contentRanges = element.getContentRanges();
val pfx = " ".repeat(Math.max(0, column)) + "\\\\"; val contentBuilder = new StringBuilder();
replacement = Arrays.stream(replacement.split("(\\r\\n|\\r|\\n)")).map(line -> pfx + line).collect( var injectState = InjectTriState.NotYet;
Collectors.joining("\n")); for (val contentRange: contentRanges) {
val intersection = injectState == InjectTriState.Complete ? null : contentRange.intersection(range);
if (intersection != null) {
if (injectState == InjectTriState.NotYet) {
contentBuilder.append(originalContext, contentRange.getStartOffset(), intersection.getStartOffset());
contentBuilder.append(newContent);
if (intersection.getEndOffset() < contentRange.getEndOffset()) {
contentBuilder.append(originalContext, intersection.getEndOffset(), contentRange.getEndOffset());
injectState = InjectTriState.Complete;
} else {
injectState = InjectTriState.Incomplete;
}
} else if (intersection.getEndOffset() < contentRange.getEndOffset()) {
contentBuilder.append(originalContext, intersection.getEndOffset(), contentRange.getEndOffset());
injectState = InjectTriState.Complete;
}
} else {
contentBuilder.append(originalContext, contentRange.getStartOffset(), contentRange.getEndOffset());
}
}
val content = contentBuilder.toString();
replacement = ZigStringUtil.prefixWithTextBlockEscape(PsiTextUtil.getIndentSize(element), "\\\\", content, false, true);
} else { } else {
replacement = "\"" + replacement + "\""; val elementRange = getRangeInElement(element);
replacement = "\"" +
originalContext.substring(elementRange.getStartOffset(),
range.getStartOffset()) +
ZigStringUtil.escape(newContent) +
originalContext.substring(range.getEndOffset(),
elementRange.getEndOffset()) +
"\"";
} }
val psiFileFactory = PsiFileFactory.getInstance(element.getProject());
val dummy = psiFileFactory.createFileFromText("dummy." + ZigFileType.INSTANCE.getDefaultExtension(), val dummy = psiFileFactory.createFileFromText("dummy." + ZigFileType.INSTANCE.getDefaultExtension(),
ZigFileType.INSTANCE, "const x = \n" + replacement + "\n;"); ZigFileType.INSTANCE, "const x = \n" + replacement + "\n;");
val stringLiteral = ((ZigPrimaryTypeExpr)((ZigContainerMembers) dummy.getFirstChild()).getContainerDeclarationsList().get(0).getDeclList().get(0).getGlobalVarDecl().getExpr()).getStringLiteral(); val stringLiteral = ((ZigPrimaryTypeExpr)((ZigContainerMembers) dummy.getFirstChild()).getContainerDeclarationsList().get(0).getDeclList().get(0).getGlobalVarDecl().getExpr()).getStringLiteral();
@ -47,25 +73,4 @@ public class ZigStringElementManipulator extends AbstractElementManipulator<ZigS
public @NotNull TextRange getRangeInElement(@NotNull ZigStringLiteral element) { public @NotNull TextRange getRangeInElement(@NotNull ZigStringLiteral element) {
return PsiTextUtil.getTextRangeBounds(element.getContentRanges()); return PsiTextUtil.getTextRangeBounds(element.getContentRanges());
} }
@SneakyThrows
public static String escape(String input) {
return input.codePoints().mapToObj(point -> switch (point) {
case '\n' -> "\\n";
case '\r' -> "\\r";
case '\t' -> "\\t";
case '\\' -> "\\\\";
case '"' -> "\\\"";
case '\'', ' ', '!' -> Character.toString(point);
default -> {
if (point >= '#' && point <= '&' ||
point >= '(' && point <= '[' ||
point >= ']' && point <= '~') {
yield Character.toString(point);
} else {
yield "\\u{" + Integer.toHexString(point).toLowerCase() + "}";
}
}
}).collect(Collectors.joining(""));
}
} }

View file

@ -2,6 +2,7 @@ package com.falsepattern.zigbrains.zig.psi.impl.mixins;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral; import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.falsepattern.zigbrains.zig.util.PsiTextUtil; import com.falsepattern.zigbrains.zig.util.PsiTextUtil;
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.extapi.psi.ASTWrapperPsiElement;
import com.intellij.lang.ASTNode; import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Pair;
@ -9,9 +10,6 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.LiteralTextEscaper; import com.intellij.psi.LiteralTextEscaper;
import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.LeafElement;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.experimental.UtilityClass;
import lombok.val; import lombok.val;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -34,24 +32,6 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
return getStringLiteralMulti() != null; return getStringLiteralMulti() != null;
} }
@Override
public @NotNull List<Pair<TextRange, String>> getDecodeReplacements(@NotNull CharSequence input) {
if (isMultiLine())
return List.of();
val result = new ArrayList<Pair<TextRange, String>>();
for (int i = 0; i + 1 < input.length(); i++) {
if (input.charAt(i) == '\\') {
val length = Escaper.findEscapementLength(input, i);
val charCode = Escaper.toUnicodeChar(input, i, length);
val range = TextRange.create(i, Math.min(i + length + 1, input.length()));
result.add(Pair.create(range, Character.toString(charCode)));
i += range.getLength() - 1;
}
}
return result;
}
@Override @Override
public @NotNull List<TextRange> getContentRanges() { public @NotNull List<TextRange> getContentRanges() {
if (!isMultiLine()) { if (!isMultiLine()) {
@ -71,19 +51,6 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
return this; return this;
} }
private static @NotNull String processReplacements(@NotNull CharSequence input,
@NotNull List<? extends Pair<TextRange, String>> replacements) throws IndexOutOfBoundsException {
StringBuilder result = new StringBuilder();
int currentOffset = 0;
for (val replacement: replacements) {
result.append(input.subSequence(currentOffset, replacement.getFirst().getStartOffset()));
result.append(replacement.getSecond());
currentOffset = replacement.getFirst().getEndOffset();
}
result.append(input.subSequence(currentOffset, input.length()));
return result.toString();
}
@Override @Override
public @NotNull LiteralTextEscaper<ZigStringLiteral> createLiteralTextEscaper() { public @NotNull LiteralTextEscaper<ZigStringLiteral> createLiteralTextEscaper() {
@ -101,7 +68,7 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
if (intersection == null) continue; if (intersection == null) continue;
decoded = true; decoded = true;
val substring = intersection.subSequence(text); val substring = intersection.subSequence(text);
outChars.append(isMultiline ? substring : processReplacements(substring, myHost.getDecodeReplacements(substring))); outChars.append(ZigStringUtil.unescape(substring, isMultiline));
} }
return decoded; return decoded;
} }
@ -126,7 +93,7 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
String curString = range.subSequence(text).toString(); String curString = range.subSequence(text).toString();
final List<Pair<TextRange, String>> replacementsForThisLine = myHost.getDecodeReplacements(curString); val replacementsForThisLine = ZigStringUtil.getDecodeReplacements(curString, myHost.isMultiLine());
int encodedOffsetInCurrentLine = 0; int encodedOffsetInCurrentLine = 0;
for (Pair<TextRange, String> replacement : replacementsForThisLine) { for (Pair<TextRange, String> replacement : replacementsForThisLine) {
final int deltaLength = replacement.getFirst().getStartOffset() - encodedOffsetInCurrentLine; final int deltaLength = replacement.getFirst().getStartOffset() - encodedOffsetInCurrentLine;
@ -153,60 +120,8 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
@Override @Override
public boolean isOneLine() { public boolean isOneLine() {
return !myHost.isMultiLine(); return true;
} }
}; };
} }
@UtilityClass
private static class Escaper {
private static final Int2IntMap ESC_TO_CODE = new Int2IntOpenHashMap();
static {
ESC_TO_CODE.put('n', '\n');
ESC_TO_CODE.put('r', '\r');
ESC_TO_CODE.put('t', '\t');
ESC_TO_CODE.put('\\', '\\');
ESC_TO_CODE.put('"', '"');
ESC_TO_CODE.put('\'', '\'');
}
static int findEscapementLength(@NotNull CharSequence text, int pos) {
if (pos + 1 < text.length() && text.charAt(pos) == '\\') {
char c = text.charAt(pos + 1);
return switch (c) {
case 'x' -> 3;
case 'u' -> {
if (pos + 3 >= text.length() || text.charAt(pos + 2) != '{') {
throw new IllegalArgumentException("Invalid unicode escape sequence");
}
int digits = 0;
while (pos + 3 + digits < text.length() && text.charAt(pos + 3 + digits) != '}') {
digits++;
}
yield 3 + digits;
}
default -> 1;
};
} else {
throw new IllegalArgumentException("This is not an escapement start");
}
}
static int toUnicodeChar(@NotNull CharSequence text, int pos, int length) {
if (length > 1) {
val s = switch (text.charAt(pos + 1)) {
case 'x' -> text.subSequence(pos + 2, Math.min(text.length(), pos + length + 1));
case 'u' -> text.subSequence(pos + 3, Math.min(text.length(), pos + length));
default -> throw new AssertionError();
};
try {
return Integer.parseInt(s.toString(), 16);
} catch (NumberFormatException e) {
return 63;
}
} else {
val c = text.charAt(pos + 1);
return ESC_TO_CODE.getOrDefault(c, c);
}
}
}
} }

View file

@ -16,5 +16,4 @@ import java.util.List;
public interface ZigStringLiteralMixin extends PsiLanguageInjectionHost { public interface ZigStringLiteralMixin extends PsiLanguageInjectionHost {
boolean isMultiLine(); boolean isMultiLine();
@NotNull List<TextRange> getContentRanges(); @NotNull List<TextRange> getContentRanges();
@NotNull List<Pair<TextRange, String>> getDecodeReplacements(@NotNull CharSequence input);
} }

View file

@ -1,9 +1,23 @@
package com.falsepattern.zigbrains.zig.util; package com.falsepattern.zigbrains.zig.util;
import com.falsepattern.zigbrains.zig.stringlexer.ZigStringLexer;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate;
import com.intellij.lang.ASTNode;
import com.intellij.lexer.FlexAdapter;
import com.intellij.lexer.FlexLexer;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.StringEscapesTokenTypes;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.MathUtil;
import lombok.SneakyThrows;
import lombok.val; import lombok.val;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.StringReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -21,6 +35,7 @@ public class PsiTextUtil {
val textLength = text.length(); val textLength = text.length();
val firstChar = startMark.charAt(0); val firstChar = startMark.charAt(0);
val extraChars = startMark.substring(1); val extraChars = startMark.substring(1);
loop:
for (int i = 0; i < textLength; i++) { for (int i = 0; i < textLength; i++) {
val cI = text.charAt(i); val cI = text.charAt(i);
if (!inBody) { if (!inBody) {
@ -28,7 +43,7 @@ public class PsiTextUtil {
i + extraChars.length() < textLength) { i + extraChars.length() < textLength) {
for (int j = 0; j < extraChars.length(); j++) { for (int j = 0; j < extraChars.length(); j++) {
if (text.charAt(i + j + 1) != startMark.charAt(j)) { if (text.charAt(i + j + 1) != startMark.charAt(j)) {
continue; continue loop;
} }
} }
i += extraChars.length(); i += extraChars.length();
@ -42,14 +57,88 @@ public class PsiTextUtil {
i++; i++;
} }
inBody = false; inBody = false;
result.add(new TextRange(stringStart, i + 1)); result.add(new TextRange(stringStart, Math.min(textLength - 1, i + 1)));
continue; continue;
} }
if (cI == '\n') { if (cI == '\n') {
inBody = false; inBody = false;
result.add(new TextRange(stringStart, i + 1)); result.add(new TextRange(stringStart, Math.min(textLength - 1, i + 1)));
} }
} }
return result; return result;
} }
public static int getIndentSize(PsiElement element) {
return StringUtil.offsetToLineColumn(element.getContainingFile().getText(), element.getTextOffset()).column;
}
public static String getIndentString(PsiElement element) {
val indent = getIndentSize(element);
return " ".repeat(Math.max(0, indent));
}
public static void splitString(@NotNull Editor editor,
@NotNull PsiElement psiAtOffset,
int caretOffset,
boolean insertNewlineAtCaret) {
val document = editor.getDocument();
ASTNode token = psiAtOffset.getNode();
val text = document.getCharsSequence();
TextRange range = token.getTextRange();
val lexer = new FlexAdapter(new ZigStringLexer());
lexer.start(text, range.getStartOffset(), range.getEndOffset());
caretOffset = skipStringLiteralEscapes(caretOffset, lexer);
caretOffset = MathUtil.clamp(caretOffset, range.getStartOffset() + 1, range.getEndOffset() - 1);
val unescapedPrefix = ZigStringUtil.unescape(text.subSequence(range.getStartOffset() + 1, caretOffset), false);
val unescapedSuffix = ZigStringUtil.unescape(text.subSequence(caretOffset, range.getEndOffset() - 1), false);
val stringRange = document.createRangeMarker(range.getStartOffset(), range.getEndOffset());
stringRange.setGreedyToRight(true);
val lineNumber = document.getLineNumber(caretOffset);
val lineOffset = document.getLineStartOffset(lineNumber);
val indent = stringRange.getStartOffset() - lineOffset;
val lineIndent = StringUtil.skipWhitespaceForward(document.getText(new TextRange(lineOffset, stringRange.getStartOffset())), 0);
boolean newLine = indent != lineIndent;
document.deleteString(stringRange.getStartOffset(), stringRange.getEndOffset());
document.insertString(stringRange.getStartOffset(),
ZigStringUtil.prefixWithTextBlockEscape(newLine ? lineIndent + 4 : lineIndent,
"\\\\",
insertNewlineAtCaret ? unescapedPrefix + "\n" : unescapedPrefix,
newLine,
true));
caretOffset = stringRange.getEndOffset();
document.insertString(caretOffset,
ZigStringUtil.prefixWithTextBlockEscape(newLine ? lineIndent + 4 : lineIndent,
"\\\\",
unescapedSuffix,
false,
false));
int end = stringRange.getEndOffset();
loop:
while (end < document.getTextLength()) {
switch (text.charAt(end)) {
case ' ', '\t':
break;
default:
break loop;
}
end++;
}
document.replaceString(stringRange.getEndOffset(), end, "\n" + " ".repeat(newLine ? lineIndent : Math.max(lineIndent - 4, 0)));
stringRange.dispose();
editor.getCaretModel().moveToOffset(caretOffset);
}
@SneakyThrows
protected static int skipStringLiteralEscapes(int caretOffset, Lexer lexer) {
while (lexer.getTokenType() != null) {
if (lexer.getTokenStart() < caretOffset && caretOffset < lexer.getTokenEnd()) {
if (StringEscapesTokenTypes.STRING_LITERAL_ESCAPES.contains(lexer.getTokenType())) {
caretOffset = lexer.getTokenEnd();
}
break;
}
lexer.advance();
}
return caretOffset;
}
} }

View file

@ -0,0 +1,154 @@
package com.falsepattern.zigbrains.zig.util;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.experimental.UtilityClass;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ZigStringUtil {
public static String escape(String input) {
return input.codePoints().mapToObj(point -> switch (point) {
case '\n' -> "\\n";
case '\r' -> "\\r";
case '\t' -> "\\t";
case '\\' -> "\\\\";
case '"' -> "\\\"";
case '\'', ' ', '!' -> Character.toString(point);
default -> {
if (point >= '#' && point <= '&' ||
point >= '(' && point <= '[' ||
point >= ']' && point <= '~') {
yield Character.toString(point);
} else {
yield "\\u{" + Integer.toHexString(point).toLowerCase() + "}";
}
}
}).collect(Collectors.joining(""));
}
public static List<Pair<TextRange, String>> getDecodeReplacements(@NotNull CharSequence input, boolean isMultiline) {
if (isMultiline) {
return List.of();
}
val result = new ArrayList<Pair<TextRange, String>>();
for (int i = 0; i + 1 < input.length(); i++) {
if (input.charAt(i) == '\\') {
val length = Escaper.findEscapementLength(input, i);
val charCode = Escaper.toUnicodeChar(input, i, length);
val range = TextRange.create(i, Math.min(i + length + 1, input.length()));
result.add(Pair.create(range, Character.toString(charCode)));
i += range.getLength() - 1;
}
}
return result;
}
public static String unescape(@NotNull CharSequence input, boolean isMultiline) {
return isMultiline ? input.toString() : processReplacements(input, getDecodeReplacements(input, false));
}
private static @NotNull String processReplacements(@NotNull CharSequence input,
@NotNull List<? extends Pair<TextRange, String>> replacements) throws IndexOutOfBoundsException {
StringBuilder result = new StringBuilder();
int currentOffset = 0;
for (val replacement: replacements) {
result.append(input.subSequence(currentOffset, replacement.getFirst().getStartOffset()));
result.append(replacement.getSecond());
currentOffset = replacement.getFirst().getEndOffset();
}
result.append(input.subSequence(currentOffset, input.length()));
return result.toString();
}
private static final Pattern NL_MATCHER = Pattern.compile("(\\r\\n|\\r|\\n)");
private static final String[] COMMON_INDENTS;
static {
val count = 32;
val sb = new StringBuilder(count);
COMMON_INDENTS = new String[count];
for (int i = 0; i < count; i++) {
COMMON_INDENTS[i] = sb.toString();
sb.append(" ");
}
}
public static CharSequence prefixWithTextBlockEscape(int indent, CharSequence marker, CharSequence content, boolean indentFirst, boolean prefixFirst) {
val indentStr = indent >= 0 ? indent < COMMON_INDENTS.length ? COMMON_INDENTS[indent] : " ".repeat(indent) : "";
val parts = Arrays.asList(NL_MATCHER.split(content, -1));
val result = new StringBuilder(content.length() + marker.length() * parts.size() + indentStr.length() * parts.size());
if (indentFirst) {
result.append('\n');
result.append(indentStr);
}
if (prefixFirst) {
result.append(marker);
}
result.append(parts.get(0));
for (val part: parts.subList(1, parts.size())) {
result.append("\n").append(indentStr).append(marker).append(part);
}
return result;
}
@UtilityClass
private static class Escaper {
private static final Int2IntMap ESC_TO_CODE = new Int2IntOpenHashMap();
static {
ESC_TO_CODE.put('n', '\n');
ESC_TO_CODE.put('r', '\r');
ESC_TO_CODE.put('t', '\t');
ESC_TO_CODE.put('\\', '\\');
ESC_TO_CODE.put('"', '"');
ESC_TO_CODE.put('\'', '\'');
}
static int findEscapementLength(@NotNull CharSequence text, int pos) {
if (pos + 1 < text.length() && text.charAt(pos) == '\\') {
char c = text.charAt(pos + 1);
return switch (c) {
case 'x' -> 3;
case 'u' -> {
if (pos + 3 >= text.length() || text.charAt(pos + 2) != '{') {
throw new IllegalArgumentException("Invalid unicode escape sequence");
}
int digits = 0;
while (pos + 3 + digits < text.length() && text.charAt(pos + 3 + digits) != '}') {
digits++;
}
yield 3 + digits;
}
default -> 1;
};
} else {
throw new IllegalArgumentException("This is not an escapement start");
}
}
static int toUnicodeChar(@NotNull CharSequence text, int pos, int length) {
if (length > 1) {
val s = switch (text.charAt(pos + 1)) {
case 'x' -> text.subSequence(pos + 2, Math.min(text.length(), pos + length + 1));
case 'u' -> text.subSequence(pos + 3, Math.min(text.length(), pos + length));
default -> throw new AssertionError();
};
try {
return Integer.parseInt(s.toString(), 16);
} catch (NumberFormatException e) {
return 63;
}
} else {
val c = text.charAt(pos + 1);
return ESC_TO_CODE.getOrDefault(c, c);
}
}
}
}