backport: 18.1.0

This commit is contained in:
FalsePattern 2024-10-17 11:29:50 +02:00
parent b054009ee0
commit 71993d4c85
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
14 changed files with 478 additions and 29 deletions

View file

@ -17,6 +17,18 @@ Changelog structure reference:
## [Unreleased]
## [18.1.0]
### Added
- Zig
- Basic language injections in strings
### Fixed
- LSP
- No more error spam when zig or zls binary is missing.
## [18.0.0]
### Added

View file

@ -15,6 +15,7 @@ through the built-in plugin browser:
1. Go to `Settings -> Plugins`
2. To the right of the `Installed` button at the top, click on the `...` dropdown menu, then select `Manage Plugin Repositories...`
3. Click the add button, and then enter the ZigBrains updater URL, based on your IDE version:
- `2024.3.*`: https://falsepattern.com/zigbrains/updatePlugins-243.xml
- `2024.2.*`: https://falsepattern.com/zigbrains/updatePlugins-242.xml
- `2024.1.*`: https://falsepattern.com/zigbrains/updatePlugins-241.xml
- `2023.3.*`: https://falsepattern.com/zigbrains/updatePlugins-233.xml

View file

@ -202,6 +202,7 @@ project(":zig") {
lsp4ijDep()
intellijPlatform {
plugin(lsp4ijPluginString)
bundledPlugin("org.intellij.intelliLang")
}
}
tasks {

View file

@ -11,7 +11,7 @@ baseIDE=clion
ideaVersion=2024.2.2
clionVersion=2024.2.2
pluginVersion=18.0.0
pluginVersion=18.1.0
# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion=8.10.2

View file

@ -51,6 +51,9 @@
bundle="zigbrains.Bundle"
key="notif-zls-error"
id="ZigBrains.ZLS"/>
<lang.elementManipulator forClass="com.falsepattern.zigbrains.zig.psi.ZigStringLiteral"
implementationClass="com.falsepattern.zigbrains.zig.psi.ZigStringElementManipulator"/>
</extensions>
<extensions defaultExtensionNs="com.falsepattern.zigbrains">
@ -181,6 +184,7 @@ The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, via ZigBr
<depends optional="true" config-file="zigbrains-zig-cidr-workspace.xml">com.intellij.cidr.base</depends>
<depends optional="true" config-file="zigbrains-zig-debugger.xml">com.intellij.modules.cidr.debugger</depends>
<depends optional="true" config-file="zigbrains-zig-clion.xml">com.intellij.modules.clion</depends>
<depends optional="true" config-file="zigbrains-zig-intellilang.xml">org.intellij.intelliLang</depends>
<extensions defaultExtensionNs="com.intellij">
<notificationGroup displayType="BALLOON"

View file

@ -163,6 +163,10 @@
IDENTIFIER='identifier'
BUILTINIDENTIFIER='builtin identifier'
]
//Mixins
mixin("StringLiteral")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigStringLiteralMixinImpl"
implements("StringLiteral")="com.falsepattern.zigbrains.zig.psi.mixins.ZigStringLiteralMixin"
}
Root ::= CONTAINER_DOC_COMMENT? ContainerMembers?

View file

@ -96,7 +96,6 @@ string_char= {char_escape}
CONTAINER_DOC_COMMENT=("//!" [^\n]* [ \n]*)+
DOC_COMMENT=("///" [^\n]* [ \n]*)+
LINE_COMMENT="//" [^\n]* | "////" [^\n]*
line_string=("\\\\" [^\n]* [ \n]*)+
FLOAT= "0x" {hex_int} "." {hex_int} ([pP] [-+]? {dec_int})?
| {dec_int} "." {dec_int} ([eE] [-+]? {dec_int})?
@ -112,6 +111,7 @@ IDENTIFIER_PLAIN=[A-Za-z_][A-Za-z0-9_]*
BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
%state STR_LIT
%state STR_MULT_LINE
%state CHAR_LIT
%state ID_QUOT
@ -261,7 +261,9 @@ BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
<YYINITIAL> "\"" { yybegin(STR_LIT); }
<STR_LIT> {string_char}*"\"" { yybegin(YYINITIAL); return STRING_LITERAL_SINGLE; }
<STR_LIT> [^] { yypushback(1); yybegin(UNT_QUOT); }
<YYINITIAL> {line_string}+ { return STRING_LITERAL_MULTI; }
<YYINITIAL> "\\\\" { yypushback(2); yybegin(STR_MULT_LINE); }
<STR_MULT_LINE> [^\n]* [ \n]* "\\\\" { }
<STR_MULT_LINE> [^\n]* \n { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
<YYINITIAL> {IDENTIFIER_PLAIN} { return IDENTIFIER; }
<YYINITIAL> "@\"" { yybegin(ID_QUOT); }

View file

@ -0,0 +1,23 @@
package com.falsepattern.zigbrains.zig.intellilang;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.intellij.psi.PsiLanguageInjectionHost;
import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport;
import org.jetbrains.annotations.NotNull;
public class ZigLanguageInjectionSupport extends AbstractLanguageInjectionSupport {
@Override
public @NotNull String getId() {
return "zig";
}
@Override
public Class<?> @NotNull [] getPatternClasses() {
return new Class[0];
}
@Override
public boolean isApplicableTo(PsiLanguageInjectionHost host) {
return host instanceof ZigStringLiteral;
}
}

View file

@ -41,7 +41,7 @@ public class ZLSLanguageServerFactory implements LanguageServerFactory, Language
@Override
public boolean isEnabled(@NotNull Project project) {
return enabled;
return enabled && ZLSStreamConnectionProvider.doGetCommand(project, false) != null;
}
@Override

View file

@ -34,40 +34,48 @@ public class ZLSStreamConnectionProvider extends OSProcessStreamConnectionProvid
public ZLSStreamConnectionProvider(Project project) {
val command = getCommand(project);
val projectDir = ProjectUtil.guessProjectDir(project);
GeneralCommandLine commandLine;
GeneralCommandLine commandLine = null;
try {
val cmd = command.get();
if (cmd != null) {
commandLine = new GeneralCommandLine(command.get());
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
if (projectDir != null) {
if (commandLine != null && projectDir != null) {
commandLine.setWorkDirectory(projectDir.getPath());
}
setCommandLine(commandLine);
}
private static List<String> doGetCommand(Project project) {
public static List<String> doGetCommand(Project project, boolean warn) {
var svc = ZLSProjectSettingsService.getInstance(project);
val state = svc.getState();
var zlsPath = state.zlsPath;
if (StringUtil.isEmpty(zlsPath)) {
zlsPath = com.falsepattern.zigbrains.common.util.FileUtil.findExecutableOnPATH("zls").map(Path::toString).orElse(null);
if (zlsPath == null) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "Could not detect ZLS binary! Please configure it!",
if (warn) {
Notifications.Bus.notify(
new Notification("ZigBrains.ZLS", "Could not detect ZLS binary! Please configure it!",
NotificationType.ERROR));
}
return null;
}
state.setZlsPath(zlsPath);
}
if (!validatePath("ZLS Binary", zlsPath, false)) {
if (!validatePath("ZLS Binary", zlsPath, false, warn)) {
return null;
}
var configPath = state.zlsConfigPath;
boolean configOK = true;
if (!configPath.isBlank() && !validatePath("ZLS Config", configPath, false)) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "Using default config path.",
NotificationType.INFORMATION));
if (!configPath.isBlank() && !validatePath("ZLS Config", configPath, false, warn)) {
if (warn) {
Notifications.Bus.notify(
new Notification("ZigBrains.ZLS", "Using default config path.", NotificationType.INFORMATION));
}
configPath = null;
}
if (configPath == null || configPath.isBlank()) {
@ -87,8 +95,11 @@ public class ZLSStreamConnectionProvider extends OSProcessStreamConnectionProvid
}
configPath = tmpFile.toAbsolutePath().toString();
} catch (IOException e) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "Failed to create automatic zls config file",
if (warn) {
Notifications.Bus.notify(
new Notification("ZigBrains.ZLS", "Failed to create automatic zls config file",
NotificationType.WARNING));
}
LOG.warn(e);
configOK = false;
}
@ -121,7 +132,7 @@ public class ZLSStreamConnectionProvider extends OSProcessStreamConnectionProvid
val future = new CompletableFuture<List<String>>();
ApplicationManager.getApplication().executeOnPooledThread(() -> {
try {
future.complete(doGetCommand(project));
future.complete(doGetCommand(project, true));
} catch (Throwable t) {
future.completeExceptionally(t);
}
@ -129,7 +140,7 @@ public class ZLSStreamConnectionProvider extends OSProcessStreamConnectionProvid
return future;
}
private static boolean validatePath(String name, String pathTxt, boolean dir) {
private static boolean validatePath(String name, String pathTxt, boolean dir, boolean warn) {
if (pathTxt == null || pathTxt.isBlank()) {
return false;
}
@ -137,23 +148,29 @@ public class ZLSStreamConnectionProvider extends OSProcessStreamConnectionProvid
try {
path = Path.of(pathTxt);
} catch (InvalidPathException e) {
Notifications.Bus.notify(
new Notification("ZigBrains.ZLS", "No " + name, "Invalid " + name + " at path \"" + pathTxt + "\"",
if (warn) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "No " + name,
"Invalid " + name + " at path \"" + pathTxt + "\"",
NotificationType.ERROR));
}
return false;
}
if (!Files.exists(path)) {
if (warn) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "No " + name,
"The " + name + " at \"" + pathTxt + "\" doesn't exist!",
NotificationType.ERROR));
}
return false;
}
if (Files.isDirectory(path) != dir) {
if (warn) {
Notifications.Bus.notify(new Notification("ZigBrains.ZLS", "No " + name,
"The " + name + " at \"" + pathTxt + "\" is a " +
(Files.isDirectory(path) ? "directory" : "file") +
", expected a " + (dir ? "directory" : "file"),
NotificationType.ERROR));
}
return false;
}
return true;

View file

@ -0,0 +1,194 @@
package com.falsepattern.zigbrains.zig.psi;
import com.falsepattern.zigbrains.zig.ZigFileType;
import com.falsepattern.zigbrains.zig.util.PsiUtil;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.AbstractElementManipulator;
import com.intellij.psi.PsiFileFactory;
import com.intellij.util.IncorrectOperationException;
import lombok.SneakyThrows;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ZigStringElementManipulator extends AbstractElementManipulator<ZigStringLiteral> {
@Override
public @Nullable ZigStringLiteral handleContentChange(@NotNull ZigStringLiteral element, @NotNull TextRange range, String newContent)
throws IncorrectOperationException {
assert (new TextRange(0, element.getTextLength())).contains(range);
val originalContext = element.getText();
val isMulti = element.getStringLiteralMulti() != null;
val elementRange = getRangeInElement(element);
var replacement = originalContext.substring(elementRange.getStartOffset(),
range.getStartOffset()) +
(isMulti ? newContent : escape(newContent)) +
originalContext.substring(range.getEndOffset(),
elementRange.getEndOffset());
val psiFileFactory = PsiFileFactory.getInstance(element.getProject());
if (isMulti) {
val column = StringUtil.offsetToLineColumn(element.getContainingFile().getText(), element.getTextOffset()).column;
val pfxB = new StringBuilder(column + 2);
for (int i = 0; i < column; i++) {
pfxB.append(' ');
}
pfxB.append("\\\\");
val pfx = pfxB.toString();
replacement = Arrays.stream(replacement.split("(\\r\\n|\\r|\\n)")).map(line -> pfx + line).collect(
Collectors.joining("\n"));
} else {
replacement = "\"" + replacement + "\"";
}
val dummy = psiFileFactory.createFileFromText("dummy." + ZigFileType.INSTANCE.getDefaultExtension(),
ZigFileType.INSTANCE, "const x = \n" + replacement + "\n;");
val stringLiteral = ((ZigPrimaryTypeExpr)((ZigContainerMembers) dummy.getFirstChild()).getContainerDeclarationsList().get(0).getDeclList().get(0).getGlobalVarDecl().getExpr()).getStringLiteral();
return (ZigStringLiteral) element.replace(stringLiteral);
}
@Override
public @NotNull TextRange getRangeInElement(@NotNull ZigStringLiteral element) {
if (element.getStringLiteralSingle() != null) {
return new TextRange(1, element.getTextLength() - 1);
}
return super.getRangeInElement(element);
}
@SneakyThrows
public static String escape(String input) {
val bytes = input.getBytes(StandardCharsets.UTF_8);
val result = new ByteArrayOutputStream();
for (int i = 0; i < bytes.length; i++) {
byte c = bytes[i];
switch (c) {
case '\n' -> result.write("\\n".getBytes(StandardCharsets.UTF_8));
case '\r' -> result.write("\\r".getBytes(StandardCharsets.UTF_8));
case '\t' -> result.write("\\t".getBytes(StandardCharsets.UTF_8));
case '\\' -> result.write("\\\\".getBytes(StandardCharsets.UTF_8));
case '"' -> result.write("\\\"".getBytes(StandardCharsets.UTF_8));
case '\'', ' ', '!' -> result.write(c);
default -> {
if (c >= '#' && c <= '&' ||
c >= '(' && c <= '[' ||
c >= ']' && c <= '~') {
result.write(c);
} else {
result.write("\\x".getBytes(StandardCharsets.UTF_8));
result.write(String.format("%02x", c).getBytes(StandardCharsets.UTF_8));
}
}
}
}
return result.toString(StandardCharsets.UTF_8);
}
@SneakyThrows
public static String unescape(String input, boolean[] noErrors) {
noErrors[0] = true;
val result = new ByteArrayOutputStream();
val bytes = input.getBytes(StandardCharsets.UTF_8);
val len = bytes.length;
loop:
for (int i = 0; i < len; i++) {
byte c = bytes[i];
switch (c) {
case '\\' -> {
i++;
if (i < len) {
switch (input.charAt(i)) {
case 'n' -> result.write('\n');
case 'r' -> result.write('\r');
case 't' -> result.write('\t');
case '\\' -> result.write('\\');
case '"' -> result.write('"');
case 'x' -> {
if (i + 2 < len) {
try {
int b1 = decodeHex(bytes[i + 1]);
int b2 = decodeHex(bytes[i + 2]);
result.write((b1 << 4) | b2);
} catch (NumberFormatException ignored) {
noErrors[0] = false;
break loop;
}
i += 2;
}
}
case 'u' -> {
i++;
if (i >= len || bytes[i] != '{') {
noErrors[0] = false;
break loop;
}
int codePoint = 0;
try {
while (i < len && bytes[i] != '}') {
codePoint <<= 4;
codePoint |= decodeHex(bytes[i + 1]);
i++;
}
} catch (NumberFormatException ignored) {
noErrors[0] = false;
break loop;
}
if (i >= len) {
noErrors[0] = false;
break loop;
}
result.write(Character.toString(codePoint).getBytes(StandardCharsets.UTF_8));
}
default -> {
noErrors[0] = false;
break loop;
}
}
} else {
noErrors[0] = false;
break loop;
}
}
default -> result.write(c);
}
}
return result.toString(StandardCharsets.UTF_8);
}
public static String unescapeWithLengthMappings(String input, List<Integer> inputOffsets, boolean[] noErrors) {
String output = "";
int lastOutputLength = 0;
int inputOffset = 0;
for (int i = 0; i < input.length(); i++) {
output = unescape(input.substring(0, i + 1), noErrors);
val outputLength = output.length();
if (noErrors[0]) {
inputOffset = i;
}
while (lastOutputLength < outputLength) {
inputOffsets.add(inputOffset);
lastOutputLength++;
inputOffset = i + 1;
}
}
return output;
}
private static int decodeHex(int b) {
if (b >= '0' && b <= '9') {
return b - '0';
}
if (b >= 'A' && b <= 'F') {
return b - 'A' + 10;
}
if (b >= 'a' && b <= 'f') {
return b - 'a' + 10;
}
throw new NumberFormatException();
}
}

View file

@ -0,0 +1,156 @@
package com.falsepattern.zigbrains.zig.psi.impl.mixins;
import com.falsepattern.zigbrains.zig.psi.ZigStringElementManipulator;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.intellij.extapi.psi.ASTWrapperPsiElement;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.LiteralTextEscaper;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.impl.source.tree.LeafElement;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public abstract class ZigStringLiteralMixinImpl extends ASTWrapperPsiElement implements ZigStringLiteral {
public ZigStringLiteralMixinImpl(@NotNull ASTNode node) {
super(node);
}
@Override
public boolean isValidHost() {
return true;
}
@Override
public PsiLanguageInjectionHost updateText(@NotNull String text) {
if (this.getStringLiteralSingle() instanceof LeafElement leaf) {
leaf.replaceWithText(text);
} else if (this.getStringLiteralMulti() instanceof LeafElement leaf) {
leaf.replaceWithText(text);
}
return this;
}
@Override
public @NotNull LiteralTextEscaper<ZigStringLiteral> createLiteralTextEscaper() {
if (this.getStringLiteralSingle() != null) {
return new LiteralTextEscaper<>(this) {
private final List<Integer> inputOffsets = new ArrayList<>();
@Override
public boolean decode(@NotNull TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
boolean[] noErrors = new boolean[] {true};
outChars.append(ZigStringElementManipulator.unescapeWithLengthMappings(rangeInsideHost.substring(myHost.getText()), inputOffsets, noErrors));
return noErrors[0];
}
@Override
public int getOffsetInHost(int offsetInDecoded, @NotNull TextRange rangeInsideHost) {
int size = inputOffsets.size();
int realOffset = 0;
if (size == 0) {
realOffset = rangeInsideHost.getStartOffset() + offsetInDecoded;
} else if (offsetInDecoded >= size) {
realOffset = rangeInsideHost.getStartOffset() + inputOffsets.get(size - 1) +
(offsetInDecoded - (size - 1));
} else {
realOffset = rangeInsideHost.getStartOffset() + inputOffsets.get(offsetInDecoded);
}
return realOffset;
}
@Override
public @NotNull TextRange getRelevantTextRange() {
return new TextRange(1, myHost.getTextLength() - 1);
}
@Override
public boolean isOneLine() {
return true;
}
};
} else if (this.getStringLiteralMulti() != null) {
return new LiteralTextEscaper<>(this) {
@Override
public boolean decode(@NotNull TextRange rangeInsideHost, @NotNull StringBuilder outChars) {
val str = myHost.getText();
boolean inMultiLineString = false;
for (int i = 0; i < str.length(); i++) {
val cI = str.charAt(i);
if (!inMultiLineString) {
if (cI == '\\' &&
i + 1 < str.length() &&
str.charAt(i + 1) == '\\') {
i++;
inMultiLineString = true;
}
continue;
}
if (cI == '\r') {
outChars.append('\n');
if (i + 1 < str.length() && str.charAt(i + 1) == '\n') {
i++;
}
inMultiLineString = false;
continue;
}
if (cI == '\n') {
outChars.append('\n');
inMultiLineString = false;
continue;
}
outChars.append(cI);
}
return true;
}
@Override
public int getOffsetInHost(int offsetInDecoded, @NotNull TextRange rangeInsideHost) {
val str = myHost.getText();
boolean inMultiLineString = false;
int i = rangeInsideHost.getStartOffset();
for (; i < rangeInsideHost.getEndOffset() && offsetInDecoded > 0; i++) {
val cI = str.charAt(i);
if (!inMultiLineString) {
if (cI == '\\' &&
i + 1 < str.length() &&
str.charAt(i + 1) == '\\') {
i++;
inMultiLineString = true;
}
continue;
}
if (cI == '\r') {
offsetInDecoded--;
if (i + 1 < str.length() && str.charAt(i + 1) == '\n') {
i++;
}
inMultiLineString = false;
continue;
}
if (cI == '\n') {
offsetInDecoded--;
inMultiLineString = false;
continue;
}
offsetInDecoded--;
}
if (offsetInDecoded != 0)
return -1;
return i;
}
@Override
public boolean isOneLine() {
return false;
}
};
} else {
throw new AssertionError();
}
}
}

View file

@ -0,0 +1,13 @@
package com.falsepattern.zigbrains.zig.psi.mixins;
import com.falsepattern.zigbrains.zig.psi.ZigStringLiteral;
import com.intellij.extapi.psi.ASTWrapperPsiElement;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.LiteralTextEscaper;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.impl.source.tree.LeafElement;
import org.jetbrains.annotations.NotNull;
public interface ZigStringLiteralMixin extends PsiLanguageInjectionHost {
}

View file

@ -0,0 +1,22 @@
<!--
~ Copyright 2023-2024 FalsePattern
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<idea-plugin package="com.falsepattern.zigbrains.zig.intellilang">
<depends>org.intellij.intelliLang</depends>
<extensions defaultExtensionNs="org.intellij.intelliLang">
<languageSupport implementation="com.falsepattern.zigbrains.zig.intellilang.ZigLanguageInjectionSupport"/>
</extensions>
</idea-plugin>