Skip to content

Commit

Permalink
Add the Auto Import feature for Code Completion
Browse files Browse the repository at this point in the history
- #6947
- Add `Auto Import` as an option for code completion
- Add `Prefer Import` and `Don't Import` options for global namespace items(`Don't Import` is enabled by default)
  - `File Scope` means a php file without a namespace name (e.g. `<html><p><?php echo \NS\something(); ?></p></html>`)
- Add `File Scope`(unchecked by default) and `Namespace Scope`(checked by default) options
- Don't add a use statement if use list has the same name item(Instead, the result of "Smart" or "Unqualified" CC is used)
- Add unit tests

Note: A use statement may not be added to an expected position if the existing use list is not sorted(ignore cases)
  • Loading branch information
junichi11 committed Feb 10, 2024
1 parent 92f2a29 commit c470ae1
Show file tree
Hide file tree
Showing 831 changed files with 31,017 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public final class CodeUtils {
public static final String EMPTY_STRING = ""; // NOI18N
public static final String NEW_LINE = "\n"; // NOI18N
public static final String THIS_VARIABLE = "$this"; // NOI18N
public static final String NS_SEPARATOR = "\\"; // NOI18N

public static final Pattern WHITE_SPACES_PATTERN = Pattern.compile("\\s+"); // NOI18N
public static final Pattern SPLIT_TYPES_PATTERN = Pattern.compile("[()|&]+"); // NOI18N
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/

package org.netbeans.modules.php.editor.codegen;

import java.util.ArrayList;
Expand All @@ -32,6 +31,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateInsertRequest;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateParameter;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateProcessor;
Expand All @@ -44,6 +45,7 @@
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.php.api.util.StringUtils;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.TypeScope;
Expand Down Expand Up @@ -87,6 +89,71 @@ public void updateDefaultValues() {
param.setValue(value);
}
}
updateImport();
}

private void updateImport() {
final AutoImport.Hints autoImportHints = getAutoImportHints();
if (autoImportHints != null) {
JTextComponent component = request.getComponent();
if (component == null) {
return;
}
final Document doc = component.getDocument();
if (doc == null) {
return;
}
RP.schedule(() -> {
try {
PHPParseResult[] result = new PHPParseResult[1];
ParserManager.parse(Collections.singleton(Source.create(doc)), new UserTask() {

@Override
public void run(ResultIterator resultIterator) throws Exception {
Parser.Result parserResult = resultIterator.getParserResult();
if (parserResult instanceof PHPParseResult) {
result[0] = (PHPParseResult) parserResult;
}
}
});
AutoImport.get(result[0]).insert(autoImportHints, component.getCaretPosition());
} catch (ParseException ex) {
LOGGER.log(Level.WARNING, null, ex);
}
}, 300, TimeUnit.MILLISECONDS);
}
}

@CheckForNull
private AutoImport.Hints getAutoImportHints() {
String fqName = CodeUtils.EMPTY_STRING;
String aliasName = CodeUtils.EMPTY_STRING;
String useType = CodeUtils.EMPTY_STRING;
for (CodeTemplateParameter param : request.getMasterParameters()) {
if (param.getName().equals(AutoImport.PARAM_NAME)) {
for (Entry<String, String> entry : param.getHints().entrySet()) {
String key = entry.getKey();
switch (key) {
case AutoImport.PARAM_KEY_FQ_NAME:
fqName = entry.getValue();
break;
case AutoImport.PARAM_KEY_ALIAS_NAME:
aliasName = entry.getValue();
break;
case AutoImport.PARAM_KEY_USE_TYPE:
useType = entry.getValue();
break;
default:
// noop
break;
}
}
if (!fqName.isEmpty() && !useType.isEmpty()) {
return new AutoImport.Hints(fqName, useType, aliasName);
}
}
}
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import javax.swing.ImageIcon;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.editor.completion.Completion;
Expand Down Expand Up @@ -105,7 +106,9 @@
import static org.netbeans.modules.php.editor.PredefinedSymbols.Attributes.OVERRIDE;
import org.netbeans.modules.php.editor.api.elements.EnumCaseElement;
import org.netbeans.modules.php.editor.api.elements.EnumElement;
import org.netbeans.modules.php.editor.codegen.AutoImport;
import org.netbeans.modules.php.editor.elements.ElementUtils;
import org.netbeans.modules.php.editor.options.CodeCompletionPanel;
import org.netbeans.modules.php.editor.options.CodeCompletionPanel.CodeCompletionType;
import org.netbeans.modules.php.editor.options.OptionsUtils;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
Expand All @@ -129,16 +132,23 @@ public abstract class PHPCompletionItem implements CompletionProposal {
protected static final ImageIcon KEYWORD_ICON = IconsUtils.loadKeywordIcon();
protected static final ImageIcon ENUM_CASE_ICON = IconsUtils.loadEnumCaseIcon();
private static final int TYPE_NAME_MAX_LENGTH = Integer.getInteger("nb.php.editor.ccTypeNameMaxLength", 30); // NOI18N
final CompletionRequest request;
private final ElementHandle element;
private QualifiedNameKind generateAs;
private static ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
private static final Cache<FileObject, PhpLanguageProperties> PROPERTIES_CACHE
= new Cache<>(new WeakHashMap<>());
private static final String AUTO_IMPORT_PARAM_FORMAT = "%s${php-auto-import default=\"\" fqName=%s aliasName=\"%s\" useType=%s editable=false}"; // NOI18N
private static ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
private static volatile Boolean ADD_FIRST_CLASS_CALLABLE = null; // for unit tests

final CompletionRequest request;
private final ElementHandle element;
private final boolean isPlatform;
private final boolean isDeprecated;
private QualifiedNameKind generateAs;

// for unit tests
private PhpVersion phpVersion;
private static volatile Boolean ADD_FIRST_CLASS_CALLABLE = null; // for unit tests
private CodeCompletionType codeCompletionType;
private Boolean isAutoImport = null;
private Boolean isGlobalItemImportable = null;

PHPCompletionItem(ElementHandle element, CompletionRequest request, QualifiedNameKind generateAs) {
this.request = request;
Expand Down Expand Up @@ -289,19 +299,23 @@ public String getInsertPrefix() {
}
if (props.getPhpVersion() != PhpVersion.PHP_5) {
if (generateAs == null) {
CodeCompletionType codeCompletionType = OptionsUtils.codeCompletionType();
switch (codeCompletionType) {
CodeCompletionType completionType = getCodeCompletionType();
switch (completionType) {
case FULLY_QUALIFIED:
template.append(ifq.getFullyQualifiedName());
return template.toString();
case UNQUALIFIED:
String autoImportTemplate = createAutoImportTemplate(ifq);
if (autoImportTemplate != null) {
return autoImportTemplate;
}
template.append(getName());
return template.toString();
case SMART:
generateAs = qn.getKind();
break;
default:
assert false : codeCompletionType;
assert false : completionType;
}
}
} else {
Expand Down Expand Up @@ -359,12 +373,121 @@ public String getInsertPrefix() {
assert false : "[" + tpl + "] should start with [" + extraPrefix + "]";
}
}
String autoImportTemplate = createAutoImportTemplate(ifq);
if (autoImportTemplate != null) {
return autoImportTemplate;
}
return tpl;
}

return getName();
}

@CheckForNull
private String createAutoImportTemplate(FullyQualifiedElement fullyQualifiedElement) {
if (isAutoImport()) {
String fqName = fullyQualifiedElement.getFullyQualifiedName().toString().substring(CodeUtils.NS_SEPARATOR.length());
String name = getName();
String useType = getUseType();
String aliasName = CodeUtils.EMPTY_STRING;
boolean isGlobalNamespace = !fqName.contains(CodeUtils.NS_SEPARATOR);
Model model = request.result.getModel();
NamespaceDeclaration namespaceDeclaration = findEnclosingNamespace(request.result, request.anchor);
NamespaceScope namespaceScope = ModelUtils.getNamespaceScope(namespaceDeclaration, model.getFileScope());
if (!useType.isEmpty() && isImportableScope() && !AutoImport.sameUseNameExists(name, fqName, AutoImport.getUseScopeType(useType), namespaceScope)) {
if (isAutoImportContext(request.context) && !fullyQualifiedElement.isAliased() && isGlobalItemImportable(isGlobalNamespace)) {
// note: add an empty parameter(hidden parameter) after a name to avoid filtering completion items
// if we add a default value to the parameter template, completion items are removed(filtered) from a completion list when we move the caret.
// see: org.netbeans.modules.csl.editor.completion.GsfCompletionProvider.JavaCompletionQuery.getFilteredData()
return String.format(AUTO_IMPORT_PARAM_FORMAT, name, fqName, aliasName, useType);
}
}
}
return null;
}

// for unit tests
void setAutoImport(boolean isAutoImport) {
this.isAutoImport = isAutoImport;
}

private boolean isAutoImport() {
if (isAutoImport != null) {
// for unit tests
return isAutoImport;
}
return OptionsUtils.autoImport();
}

// for unit tests
void setCodeCompletionType(CodeCompletionType codeCompletionType) {
this.codeCompletionType = codeCompletionType;
}

private CodeCompletionType getCodeCompletionType() {
if (codeCompletionType != null) {
// for unit tests
return codeCompletionType;
}
return OptionsUtils.codeCompletionType();
}

// for unit tests
void setGlobalItemImportable(boolean isGlobalItemImportable) {
this.isGlobalItemImportable = isGlobalItemImportable;
}

private boolean isGlobalItemImportable(boolean isGlobalNamespace) {
if (isGlobalNamespace) {
if (isGlobalItemImportable != null) {
// for unit tests
return isGlobalItemImportable;
}
CodeCompletionPanel.GlobalNamespaceAutoImportType globalNSImport = null;
switch (getUseType()) {
case AutoImport.USE_TYPE:
globalNSImport = OptionsUtils.globalNSImportType();
break;
case AutoImport.USE_FUNCTION:
globalNSImport = OptionsUtils.globalNSImportFunction();
break;
case AutoImport.USE_CONST:
globalNSImport = OptionsUtils.globalNSImportConst();
break;
default:
assert false : "Unknown use type: " + getUseType(); // NOI18N
}
return globalNSImport == CodeCompletionPanel.GlobalNamespaceAutoImportType.IMPORT;
}
return true;
}

private boolean isImportableScope() {
Model model = request.result.getModel();
Collection<? extends NamespaceScope> declaredNamespaces = model.getFileScope().getDeclaredNamespaces();
NamespaceDeclaration namespaceDeclaration = findEnclosingNamespace(request.result, request.anchor);
if (declaredNamespaces.size() > 1 && namespaceDeclaration == null) {
return false;
} else if (declaredNamespaces.size() == 1) {
return OptionsUtils.autoImportFileScope();
} else if (namespaceDeclaration != null) {
return OptionsUtils.autoImportNamespaceScope();
}
return false;
}

private String getUseType() {
String useType = CodeUtils.EMPTY_STRING;
if (getKind() == ElementKind.CLASS || getKind() == ElementKind.CONSTRUCTOR) {
useType = AutoImport.USE_TYPE;
} else if (this instanceof ConstantItem) {
useType = AutoImport.USE_CONST;
} else if (this instanceof FunctionElementItem) {
useType = AutoImport.USE_FUNCTION;
}
return useType;
}

@Override
public String getRhsHtml(HtmlFormatter formatter) {
if (element instanceof TypeMemberElement) {
Expand Down Expand Up @@ -478,6 +601,15 @@ private boolean isNewClassContext(CompletionContext context) {
|| context.equals(CompletionContext.ATTRIBUTE);
}

private boolean isAutoImportContext(CompletionContext context) {
return context != CompletionContext.GROUP_USE_KEYWORD
&& context != CompletionContext.GROUP_USE_FUNCTION_KEYWORD
&& context != CompletionContext.GROUP_USE_CONST_KEYWORD
&& context != CompletionContext.USE_KEYWORD
&& context != CompletionContext.USE_FUNCTION_KEYWORD
&& context != CompletionContext.USE_CONST_KEYWORD;
}

static class NewClassItem extends MethodElementItem {

/**
Expand Down Expand Up @@ -1794,6 +1926,11 @@ public String getName() {
return super.getName();
}

@Override
public String getCustomInsertTemplate() {
return super.getInsertPrefix();
}

@Override
public String getLhsHtml(HtmlFormatter formatter) {
ElementHandle element = getElement();
Expand Down Expand Up @@ -1844,6 +1981,10 @@ public ElementKind getKind() {
return ElementKind.CLASS;
}

@Override
public String getCustomInsertTemplate() {
return super.getInsertPrefix();
}
}

static class ClassItem extends PHPCompletionItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,18 @@ CodeCompletionPanel.trueFalseNullCheckBox.text="TRUE", "FALSE", "NULL" C&onstant
CodeCompletionPanel.autoCompletionCommentAsteriskLabel.text=C&omment Completion:
CodeCompletionPanel.autoCompletionCommentAsteriskCheckBox.text=&Insert " * " after a line break (Multi line comment /* */ only)
CodeCompletionPanel.codeCompletionFirstClassCallableCheckBox.text=Add First Class Callable Syntax
CodeCompletionPanel.autoImportInfoLabel.text=<html>Fix imports after completing names without namespace names if possible<br>Otherwise, the behavior is the same as "Smart" or "Unqualified"
CodeCompletionPanel.autoImportGlobalNamespaceLabel.text=Auto Import for Global Namespace Names (e.g. "\\MyClass"):
CodeCompletionPanel.autoImportGlobalNamespaceTypeLabel.text=T&ype:
CodeCompletionPanel.autoImportGlobalNamespaceTypeDoNotImportRadioButton.text=Do&n't Import
CodeCompletionPanel.autoImportGlobalNamespaceTypeImportRadioButton.text=P&refer Import
CodeCompletionPanel.autoImportGlobalNamespaceFunctionLabel.text=&Function:
CodeCompletionPanel.autoImportGlobalNamespaceFunctionImportRadioButton.text=Prefer &Import
CodeCompletionPanel.autoImportGlobalNamespaceFunctionDoNotImportRadioButton.text=Don't Impor&t
CodeCompletionPanel.autoImportGlobalNamespaceConstLabel.text=&Const:
CodeCompletionPanel.autoImportGlobalNamespaceConstImportRadioButton.text=Prefer I&mport
CodeCompletionPanel.autoImportGlobalNamespaceConstDoNotImportRadioButton.text=Don't Impo&rt
CodeCompletionPanel.autoImportForScopeLabel.text=&Auto Import for Scope:
CodeCompletionPanel.autoImportFileScopeCheckBox.text=&File Scope
CodeCompletionPanel.autoImportNamesapceScopeCheckBox.text=Namespace Sc&ope
CodeCompletionPanel.autoImportCheckBox.text=&Auto Import (only "Smart" and "Unqualified" completion)
Loading

0 comments on commit c470ae1

Please sign in to comment.