Skip to content

Commit

Permalink
Added completion and doc provider for printf/scanf format specifiers (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
King2500 committed Mar 1, 2020
1 parent 36b4157 commit e3a5b80
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 7 deletions.
2 changes: 2 additions & 0 deletions META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@
<completion.confidence language="PHP" implementationClass="net.king2500.plugins.PhpAdvancedAutoComplete.PhpParameterStringCompletionConfidence"
id="asTrue" order="first"/>
<lang.documentationProvider language="PHP" order="first" implementationClass="net.king2500.plugins.PhpAdvancedAutoComplete.PhpHeaderDocumentationProvider"/>
<lang.documentationProvider language="PHP" order="first" implementationClass="net.king2500.plugins.PhpAdvancedAutoComplete.PhpFormatDocumentationProvider"/>

<!--<typedHandler implementation="net.king2500.plugins.PhpAdvancedAutoComplete.PhpAutoPopupSpaceTypedHandler"/>-->
<typedHandler implementation="net.king2500.plugins.PhpAdvancedAutoComplete.PhpAutoPopupTypedHandler"/>

<highlightUsagesHandlerFactory implementation="net.king2500.plugins.PhpAdvancedAutoComplete.reference.PhpHighlightPackParametersUsagesHandlerFactory"/>
</extensions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package net.king2500.plugins.PhpAdvancedAutoComplete;

import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.ObjectUtils;
import com.jetbrains.php.completion.PhpCompletionUtil;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.FunctionReference;
import com.jetbrains.php.lang.psi.elements.ParameterList;
import com.jetbrains.php.lang.psi.elements.Statement;
import net.king2500.plugins.PhpAdvancedAutoComplete.utils.PhpElementsUtil;
import net.king2500.plugins.PhpAdvancedAutoComplete.utils.StringUtil;
import org.jetbrains.annotations.NotNull;

/**
* @author Thomas Schulz <[email protected]>
*/
public class PhpAutoPopupTypedHandler extends TypedHandlerDelegate {
@NotNull
@Override
public Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {

if (!(file instanceof PhpFile)) {
return Result.CONTINUE;
}

int offset = editor.getCaretModel().getOffset();
PsiElement psiElement = file.findElementAt(offset);

ParameterList parameterList = PhpPsiUtil.getParentByCondition(psiElement, true, ParameterList.INSTANCEOF, Statement.INSTANCEOF);
if (parameterList != null) {
FunctionReference functionCall = ObjectUtils.tryCast(parameterList.getParent(), FunctionReference.class);
String fqn = PhpElementsUtil.resolveFqn(functionCall);

if (PhpElementsUtil.isFormatFunction(fqn) && charTyped == '%') {
if (StringUtil.getPrecedingCharNum(editor.getDocument().getCharsSequence(), offset, '%') % 2 == 0) {
PhpCompletionUtil.showCompletion(editor);
}
}
}

return Result.CONTINUE;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,60 @@ public class PhpCompletionTokens {
"NUL-padded string",
"NUL-fill to absolute position"
};

public static String[] formatFuncs = { "printf:0", "sprintf:0", "fprintf:1", "vprintf:0", "vsprintf:0", "vfprintf:1" };
public static String[] formatTokens = { "%", "b", "c", "d", "e", "E", "f", "F", "g", "G", "o", "s", "u", "x", "X" };
public static String[] formatInfos = { "", "integer", "integer", "integer", "double", "double", "double", "double", "double", "double", "integer", "string", "integer", "integer", "integer" };
public static String[] formatFlags = { "-", "+", "0", "'" };

public static String[] formatFuncFqns = { "\\printf", "\\sprintf", "\\fprintf", "\\vprintf", "\\vsprintf", "\\vfprintf", "\\sscanf", "\\fscanf" };

public final static HashMap<String, String> formatTokensDoc = new HashMap<String, String>() {{
put("-", "Left-justify within the given field width; Right justification is the default.");
put("+", "Prefix positive numbers with a plus sign +; Default only negative are prefixed with a negative sign.");
put(" ", "Pads the result with spaces. This is the default.");
put("0", "Only left-pads numbers with zeros. With s specifiers this can also right-pad with zeros.");
put("'", "Pads the result with the following character.");
put("%", "A literal percent character. No argument is required.");
put("b", "The argument is treated as an integer and presented as a binary number.");
put("c", "The argument is treated as an integer and presented as the character with that ASCII.");
put("d", "The argument is treated as an integer and presented as a (signed) decimal number.");
put("e", "The argument is treated as scientific notation (e.g. 1.2e+2). The precision specifier stands for the number of digits after the decimal point since PHP 5.2.1. In earlier versions, it was taken as number of significant digits (one less).");
put("E", "Like the e specifier but uses uppercase letter (e.g. 1.2E+2).");
put("f", "The argument is treated as a float and presented as a floating-point number (locale aware).");
put("F", "The argument is treated as a float and presented as a floating-point number (non-locale aware). Available as of PHP 5.0.3.");
put("g", "General format.<br><br>Let P equal the precision if nonzero, 6 if the precision is omitted, or 1 if the precision is zero. Then, if a conversion with style E would have an exponent of X:<br><br>If P &gt; X ≥ −4, the conversion is with style f and precision P − (X + 1). Otherwise, the conversion is with style e and precision P − 1.");
put("G", "Like the g specifier but uses E and F.");
put("o", "The argument is treated as an integer and presented as an octal number.");
put("s", "The argument is treated and presented as a string.");
put("u", "The argument is treated as an integer and presented as an unsigned decimal number.");
put("x", "The argument is treated as an integer and presented as a hexadecimal number (with lowercase letters).");
put("X", "The argument is treated as an integer and presented as a hexadecimal number (with uppercase letters).");
}};

public static String[] scanFormatFuncs = { "sscanf:1", "fscanf:1" };
public static String[] scanFormatTokens = { "%", "c", "d", "D", "e", "E", "f", "i", "n", "o", "s", "u", "x", "X" };
public static String[] scanFormatInfos = { "", "integer", "integer", "integer", "double", "double", "double", "integer", "integer", "integer", "string", "integer", "integer", "integer" };

public final static HashMap<String, String> scanFormatTokensDoc = new HashMap<String, String>() {{
put("-", "Left-justify within the given field width; Right justification is the default.");
put("+", "Prefix positive numbers with a plus sign +; Default only negative are prefixed with a negative sign.");
put(" ", "Pads the result with spaces. This is the default.");
put("0", "Only left-pads numbers with zeros. With s specifiers this can also right-pad with zeros.");
put("'", "Pads the result with the following character.");
put("%", "A literal percent character.");
put("c", "The text is interpreted as the character with that ASCII and returned as an integer.");
put("d", "The text is interpreted as a (signed) decimal number and returned as an integer.");
put("D", "The text is interpreted as a decimal number and returned as an integer.");
put("e", "The text is interpreted as scientific notation (e.g. 1.2e+2). The precision specifier stands for the number of digits after the decimal point since PHP 5.2.1. In earlier versions, it was taken as number of significant digits (one less).");
put("E", "Like the e specifier but uses uppercase letter (e.g. 1.2E+2).");
put("f", "The text is interpreted as a floating-point number (not locale aware) and returned as a float.");
put("i", "The text is interpreted as an integer with base detection.");
put("n", "Returns the number of characters processed so far.");
put("o", "The text is interpreted as an octal number and returned as an integer.");
put("s", "The text is interpreted as a string.<br>Note: Stops reading at any whitespace character.");
put("u", "The text is interpreted as an unsigned decimal number and returned as an integer.");
put("x", "The text is interpreted as a hexadecimal number (with lowercase letters) and returned as an integer.");
put("X", "The text is interpreted as a hexadecimal number (with uppercase letters) and returned as an integer.");
}};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package net.king2500.plugins.PhpAdvancedAutoComplete;

import com.intellij.lang.Language;
import com.intellij.lang.documentation.DocumentationProviderEx;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.light.LightElement;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.FunctionReference;
import com.jetbrains.php.lang.psi.elements.Statement;
import io.netty.util.internal.StringUtil;
import net.king2500.plugins.PhpAdvancedAutoComplete.utils.PhpElementsUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* @author Thomas Schulz <[email protected]>
*/
public class PhpFormatDocumentationProvider extends DocumentationProviderEx {

private static final String PHP_FORMAT_URL = "https://www.php.net/manual/en/function.{functionName}.php";

@Override
public @Nullable List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
if (!(element instanceof FormatTokenDocElement)) {
return null;
}
String functionName = ((FormatTokenDocElement)element).getFunctionName();
return Collections.singletonList(PHP_FORMAT_URL.replace("{functionName}", functionName));
}

@Override
public @Nullable String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
if (!(element instanceof FormatTokenDocElement)) {
return null;
}

String tokenText = ((FormatTokenDocElement)element).getTokenText();
String functionName = ((FormatTokenDocElement)element).getFunctionName();

if (Arrays.asList(PhpCompletionTokens.scanFormatFuncs).contains(functionName + ":1")) {
return PhpCompletionTokens.scanFormatTokensDoc.getOrDefault(tokenText, "");
}
else {
return PhpCompletionTokens.formatTokensDoc.getOrDefault(tokenText, "");
}
}

@Override
public @Nullable PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement psiElement) {
if (!(object instanceof String)) {
return null;
}

String fqn = getCallToFormatFunc(psiElement);
if (StringUtil.isNullOrEmpty(fqn)) {
return null;
}

String tokenText = (String)object;

if ("%%".equals(tokenText)) {
tokenText = "%";
}
else if (!"%".equals(tokenText)) {
tokenText = StringUtils.strip((String)object, "%");
}
String functionName = StringUtils.strip(fqn, "\\");
return new FormatTokenDocElement(psiManager, psiElement.getLanguage(), tokenText, functionName);
}

private String getCallToFormatFunc(PsiElement psiElement) {
FunctionReference function = PhpPsiUtil.getParentByCondition(psiElement, true, FunctionReference.INSTANCEOF, Statement.INSTANCEOF);
if (function == null) {
return null;
}
return PhpElementsUtil.resolveFqn(function);
}

private static class FormatTokenDocElement extends LightElement {

private final String tokenText;
private final String functionName;

protected FormatTokenDocElement(@NotNull final PsiManager manager, @NotNull final Language language, @NotNull final String tokenText, @NotNull final String functionName) {
super(manager, language);
this.tokenText = tokenText;
this.functionName = functionName;
}

public String getTokenText() {
return tokenText;
}

@Override
public String getText() {
return functionName;
}

public String getFunctionName() {
return functionName;
}

@Override
public String toString() {
return "FormatTokenDocElement for " + tokenText;
}
}
}
Loading

0 comments on commit e3a5b80

Please sign in to comment.