/******************************************************************************* * Copyright (c) 2016 Janko Richter and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Janko Richter - initial API and implementation * Kaloyan Raev - Bug 485550 - Improve insert variable comment quick assist *******************************************************************************/ package org.eclipse.php.internal.ui.quickassist; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.CoreException; import org.eclipse.dltk.core.IMember; import org.eclipse.dltk.core.IModelElement; import org.eclipse.dltk.core.ISourceModule; import org.eclipse.dltk.core.ModelException; import org.eclipse.dltk.core.search.IDLTKSearchConstants; import org.eclipse.dltk.core.search.IDLTKSearchScope; import org.eclipse.dltk.core.search.SearchEngine; import org.eclipse.dltk.internal.ui.dialogs.OpenTypeSelectionDialog2; import org.eclipse.dltk.ui.DLTKPluginImages; import org.eclipse.dltk.ui.DLTKUIPlugin; import org.eclipse.dltk.ui.IDLTKUILanguageToolkit; import org.eclipse.dltk.ui.text.completion.IScriptCompletionProposal; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.osgi.util.NLS; import org.eclipse.php.core.ast.nodes.ASTNode; import org.eclipse.php.core.ast.nodes.ASTNodes; import org.eclipse.php.core.ast.nodes.Variable; import org.eclipse.php.core.compiler.PHPFlags; import org.eclipse.php.internal.core.corext.dom.NodeFinder; import org.eclipse.php.internal.ui.PHPUILanguageToolkit; import org.eclipse.php.internal.ui.PHPUiPlugin; import org.eclipse.php.internal.ui.text.correction.proposals.AbstractCorrectionProposal; import org.eclipse.php.ui.text.correction.IInvocationContext; import org.eclipse.php.ui.text.correction.IProblemLocation; import org.eclipse.php.ui.text.correction.IQuickAssistProcessor; import org.eclipse.swt.widgets.Shell; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.TextEdit; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.dialogs.SelectionDialog; public class VarCommentQuickAssistProcessor implements IQuickAssistProcessor { /** * Proposal implementation */ private static class VarCommentCorrectionProposal extends AbstractCorrectionProposal { private static final String COMMAND_ID = "org.eclipse.php.ui.insertVarComment"; //$NON-NLS-1$ private static final Pattern noneSpacePattern = Pattern.compile("[^\\p{javaWhitespace}]+"); //$NON-NLS-1$ private ASTNode variableNode; private ISourceModule sourceModule; public VarCommentCorrectionProposal(ASTNode variableNode, ISourceModule sourceModule) { super(Messages.VarCommentQuickAssistProcessor_name, 0, DLTKPluginImages.get(DLTKPluginImages.IMG_OBJS_ANNOTATION), COMMAND_ID); this.variableNode = variableNode; this.sourceModule = sourceModule; } @Override public void apply(IDocument document) { try { SelectionDialog dialog = createTypeDialog(document); int result = dialog.open(); if (result != IDialogConstants.OK_ID) { return; } TextEdit textEdit = null; Object[] types = dialog.getResult(); if (types != null && types.length == 1 && types[0] instanceof IModelElement) { textEdit = createTextEditForType(document, (IModelElement) types[0]); } if (null != textEdit) { textEdit.apply(document); } } catch (BadLocationException | ModelException | MalformedTreeException e) { PHPUiPlugin.log(e); } } /** * Creates the variable comment for given type */ private TextEdit createTextEditForType(IDocument document, IModelElement selectedType) throws BadLocationException, ModelException { String typeName = selectedType.getElementName(); IModelElement parent = selectedType.getParent(); if (parent instanceof IMember && PHPFlags.isNamespace(((IMember) parent).getFlags())) { typeName = parent.getElementName() + '\\' + typeName; } typeName = typeName.trim(); if (typeName.length() > 0) { typeName = '\\' + typeName; } int varOffset = variableNode.getStart(); int varLength = variableNode.getLength(); String varName = document.get(varOffset, varLength); // inserts type hint int selectionStartLine = document.getLineOfOffset(varOffset); int selectionLineOffset = document.getLineOffset(selectionStartLine); IRegion selectionLineInfo = document.getLineInformation(selectionStartLine); int selectionLineLength = selectionLineInfo.getLength(); String selectionLineText = document.get(selectionLineOffset, selectionLineLength); Matcher nonSpaceMatch = noneSpacePattern.matcher(selectionLineText); StringBuilder varTypeHint = new StringBuilder(); // $NON-NLS-1$ if (nonSpaceMatch.find()) { varTypeHint.append(selectionLineText.substring(0, nonSpaceMatch.start())); } varTypeHint.append("/** @var "); //$NON-NLS-1$ varTypeHint.append(typeName); varTypeHint.append(" "); //$NON-NLS-1$ if (variableNode instanceof Variable && !((Variable) variableNode).isDollared()) { varTypeHint.append("$"); //$NON-NLS-1$ } varTypeHint.append(varName); varTypeHint.append(" */"); //$NON-NLS-1$ varTypeHint.append(TextUtilities.getDefaultLineDelimiter(document)); return new InsertEdit(selectionLineOffset, varTypeHint.toString()); } /** * Creates the dialog to select type * * @throws BadLocationException * if variable name cannot be determined */ private SelectionDialog createTypeDialog(IDocument document) throws BadLocationException { String varName = document.get(variableNode.getStart(), variableNode.getLength()); IDLTKUILanguageToolkit languageToolkit = PHPUILanguageToolkit.getInstance(); final Shell parent = DLTKUIPlugin.getActiveWorkbenchShell(); IDLTKSearchScope searchScope = SearchEngine.createSearchScope(sourceModule.getScriptProject()); OpenTypeSelectionDialog2 dialog = new OpenTypeSelectionDialog2(parent, true, PlatformUI.getWorkbench().getProgressService(), searchScope, IDLTKSearchConstants.TYPE, languageToolkit); dialog.setTitle(Messages.VarCommentQuickAssistProcessor_OpenTypeAction_dialogTitle); dialog.setMessage(NLS.bind(Messages.VarCommentQuickAssistProcessor_OpenTypeAction_dialogMessage, varName)); dialog.setFilter(""); //$NON-NLS-1$ return dialog; } @Override public String getAdditionalProposalInfo() { return Messages.VarCommentQuickAssistProcessor_AdditionalProposalInfo; } } /** * Returns the variable for selected node * * @param selectedNode * @return Variable */ private static ASTNode getVariableNode(ASTNode selectedNode) { // state machine to find PHP variable ASTNode variableNode = null; final int START = 1; final int PROCESS_PARENT = 2; final int PROCESS_EXPRESSION = 3; final int FINISH = 4; int state = START; while (selectedNode != null && state != FINISH) { switch (selectedNode.getType()) { case ASTNode.VARIABLE: if (((Variable) selectedNode).isDollared() || ASTNodes.isQuotedDollaredCurlied((Variable) selectedNode)) { ASTNode parent = selectedNode.getParent(); if (parent != null && (parent.getType() == ASTNode.SINGLE_FIELD_DECLARATION || parent.getType() == ASTNode.FORMAL_PARAMETER)) { state = FINISH; break; } // variable found! state = FINISH; variableNode = selectedNode; } else if (PROCESS_EXPRESSION == state) { // expression doesn't start with a variable state = FINISH; } else { state = PROCESS_PARENT; selectedNode = selectedNode.getParent(); } break; case ASTNode.EXPRESSION_STATEMENT: if (PROCESS_EXPRESSION == state) { // prevent endless loop state = FINISH; } else { // does expression start with a variable? selectedNode = NodeFinder.perform(selectedNode, selectedNode.getStart(), 0); state = PROCESS_EXPRESSION; } break; case ASTNode.FIELD_ACCESS: case ASTNode.IDENTIFIER: case ASTNode.FUNCTION_NAME: case ASTNode.FUNCTION_INVOCATION: case ASTNode.METHOD_INVOCATION: if (PROCESS_EXPRESSION == state) { // expression doesn't start with a variable state = FINISH; } else { state = PROCESS_PARENT; selectedNode = selectedNode.getParent(); } break; default: // not expected node; stop here state = FINISH; } } return variableNode; } /** * @see IQuickAssistProcessor#getAssists(IInvocationContext, * IProblemLocation[]) */ @Override public IScriptCompletionProposal[] getAssists(IInvocationContext context, IProblemLocation[] locations) throws CoreException { ASTNode variableNode = getVariableNode(context.getCoveringNode()); if (null != variableNode) { return new IScriptCompletionProposal[] { new VarCommentCorrectionProposal(variableNode, context.getCompilationUnit()) }; } return null; } /** * @see IQuickAssistProcessor#hasAssists(IInvocationContext) */ @Override public boolean hasAssists(IInvocationContext context) throws CoreException { return (null != getVariableNode(context.getCoveringNode())); } }