backport: 19.2.0
This commit is contained in:
parent
3983a03beb
commit
81efb0624c
14 changed files with 691 additions and 130 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -11,7 +11,7 @@ baseIDE=clion
|
||||||
ideaVersion=2024.1.6
|
ideaVersion=2024.1.6
|
||||||
clionVersion=2024.1.5
|
clionVersion=2024.1.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
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(""));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue