backport: 19.2.0
This commit is contained in:
parent
92f8fa2feb
commit
8e0b2206a5
14 changed files with 691 additions and 130 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -17,6 +17,19 @@ Changelog structure reference:
|
|||
|
||||
## [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]
|
||||
|
||||
### Added
|
||||
|
|
|
@ -11,7 +11,7 @@ baseIDE=clion
|
|||
ideaVersion=2024.2.2
|
||||
clionVersion=2024.2.2
|
||||
|
||||
pluginVersion=19.1.0
|
||||
pluginVersion=19.2.0
|
||||
|
||||
# Gradle Releases -> https://github.com/gradle/gradle/releases
|
||||
gradleVersion=8.10.2
|
||||
|
|
|
@ -32,6 +32,24 @@
|
|||
|
||||
<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"/>
|
||||
|
||||
<!-- LSP textDocument/signatureHelp -->
|
||||
|
|
|
@ -30,6 +30,11 @@ import static com.intellij.psi.StringEscapesTokenTypes.*;
|
|||
%implements FlexLexer
|
||||
%function advance
|
||||
%type IElementType
|
||||
%{
|
||||
public ZigStringLexer() {
|
||||
|
||||
}
|
||||
%}
|
||||
|
||||
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.util.PsiTextUtil;
|
||||
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
|
||||
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;
|
||||
|
@ -16,27 +15,54 @@ import java.util.Arrays;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
public class ZigStringElementManipulator extends AbstractElementManipulator<ZigStringLiteral> {
|
||||
private enum InjectTriState {
|
||||
NotYet,
|
||||
Incomplete,
|
||||
Complete
|
||||
}
|
||||
@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.isMultiLine();
|
||||
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());
|
||||
final CharSequence replacement;
|
||||
if (isMulti) {
|
||||
val column = StringUtil.offsetToLineColumn(element.getContainingFile().getText(), element.getTextOffset()).column;
|
||||
val pfx = " ".repeat(Math.max(0, column)) + "\\\\";
|
||||
replacement = Arrays.stream(replacement.split("(\\r\\n|\\r|\\n)")).map(line -> pfx + line).collect(
|
||||
Collectors.joining("\n"));
|
||||
val contentRanges = element.getContentRanges();
|
||||
val contentBuilder = new StringBuilder();
|
||||
var injectState = InjectTriState.NotYet;
|
||||
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 {
|
||||
replacement = "\"" + replacement + "\"";
|
||||
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 {
|
||||
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(),
|
||||
ZigFileType.INSTANCE, "const x = \n" + replacement + "\n;");
|
||||
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) {
|
||||
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.util.PsiTextUtil;
|
||||
import com.falsepattern.zigbrains.zig.util.ZigStringUtil;
|
||||
import com.intellij.extapi.psi.ASTWrapperPsiElement;
|
||||
import com.intellij.lang.ASTNode;
|
||||
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.PsiLanguageInjectionHost;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
@ -34,24 +32,6 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
|
|||
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
|
||||
public @NotNull List<TextRange> getContentRanges() {
|
||||
if (!isMultiLine()) {
|
||||
|
@ -71,19 +51,6 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
|
|||
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
|
||||
public @NotNull LiteralTextEscaper<ZigStringLiteral> createLiteralTextEscaper() {
|
||||
|
||||
|
@ -101,7 +68,7 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
|
|||
if (intersection == null) continue;
|
||||
decoded = true;
|
||||
val substring = intersection.subSequence(text);
|
||||
outChars.append(isMultiline ? substring : processReplacements(substring, myHost.getDecodeReplacements(substring)));
|
||||
outChars.append(ZigStringUtil.unescape(substring, isMultiline));
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
@ -126,7 +93,7 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
|
|||
|
||||
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;
|
||||
for (Pair<TextRange, String> replacement : replacementsForThisLine) {
|
||||
final int deltaLength = replacement.getFirst().getStartOffset() - encodedOffsetInCurrentLine;
|
||||
|
@ -153,60 +120,8 @@ public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement imp
|
|||
|
||||
@Override
|
||||
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 {
|
||||
boolean isMultiLine();
|
||||
@NotNull List<TextRange> getContentRanges();
|
||||
@NotNull List<Pair<TextRange, String>> getDecodeReplacements(@NotNull CharSequence input);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
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.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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -21,6 +35,7 @@ public class PsiTextUtil {
|
|||
val textLength = text.length();
|
||||
val firstChar = startMark.charAt(0);
|
||||
val extraChars = startMark.substring(1);
|
||||
loop:
|
||||
for (int i = 0; i < textLength; i++) {
|
||||
val cI = text.charAt(i);
|
||||
if (!inBody) {
|
||||
|
@ -28,7 +43,7 @@ public class PsiTextUtil {
|
|||
i + extraChars.length() < textLength) {
|
||||
for (int j = 0; j < extraChars.length(); j++) {
|
||||
if (text.charAt(i + j + 1) != startMark.charAt(j)) {
|
||||
continue;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
i += extraChars.length();
|
||||
|
@ -42,14 +57,88 @@ public class PsiTextUtil {
|
|||
i++;
|
||||
}
|
||||
inBody = false;
|
||||
result.add(new TextRange(stringStart, i + 1));
|
||||
result.add(new TextRange(stringStart, Math.min(textLength - 1, i + 1)));
|
||||
continue;
|
||||
}
|
||||
if (cI == '\n') {
|
||||
inBody = false;
|
||||
result.add(new TextRange(stringStart, i + 1));
|
||||
result.add(new TextRange(stringStart, Math.min(textLength - 1, i + 1)));
|
||||
}
|
||||
}
|
||||
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.getFirst());
|
||||
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