/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ /* * Created on 16/09/2005 */ package com.python.pydev.analysis; import java.util.List; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.python.pydev.core.docutils.ImportHandle; import org.python.pydev.core.docutils.ImportHandle.ImportHandleInfo; import org.python.pydev.core.docutils.ImportNotRecognizedException; import org.python.pydev.core.docutils.PyImportsHandling; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.PySelection.LineStartingScope; import org.python.pydev.core.log.Log; import org.python.pydev.editor.PyEdit; import org.python.pydev.editor.actions.PyAction; import org.python.pydev.editor.autoedit.DefaultIndentPrefs; import org.python.pydev.editor.codecompletion.AbstractPyCompletionProposalExtension2; import org.python.pydev.editor.codecompletion.IPyCompletionProposal2; import org.python.pydev.editor.codefolding.PySourceViewer; import org.python.pydev.plugin.PydevPlugin; import org.python.pydev.plugin.preferences.PydevPrefs; import org.python.pydev.ui.importsconf.ImportsPreferencesPage; /** * This is the proposal that should be used to do a completion that can have a related import. * * @author Fabio */ public class CtxInsensitiveImportComplProposal extends AbstractPyCompletionProposalExtension2 implements ICompletionProposalExtension, IPyCompletionProposal2 { /** * If empty, act as a regular completion */ public String realImportRep; /** * This is the indentation string that should be used */ public String indentString; /** * Determines if the import was added or if only the completion was applied. */ private int importLen = 0; /** * Offset forced to be returned (only valid if >= 0) */ private int newForcedOffset = -1; /** * Indicates if the completion was applied with a trigger char that should be considered * (meaning that the resulting position should be summed with 1) */ private boolean appliedWithTrigger = false; /** * If the import should be added locally or globally. */ private boolean addLocalImport = false; public CtxInsensitiveImportComplProposal(String replacementString, int replacementOffset, int replacementLength, int cursorPosition, Image image, String displayString, IContextInformation contextInformation, String additionalProposalInfo, int priority, String realImportRep) { super(replacementString, replacementOffset, replacementLength, cursorPosition, image, displayString, contextInformation, additionalProposalInfo, priority, ON_APPLY_DEFAULT, ""); this.realImportRep = realImportRep; } public void setAddLocalImport(boolean b) { this.addLocalImport = b; } /** * This is the apply that should actually be called! */ public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { IDocument document = viewer.getDocument(); if (viewer instanceof PySourceViewer) { PySourceViewer pySourceViewer = (PySourceViewer) viewer; PyEdit pyEdit = pySourceViewer.getEdit(); this.indentString = pyEdit.getIndentPrefs().getIndentationString(); } else { //happens on compare editor this.indentString = new DefaultIndentPrefs().getIndentationString(); } //If the completion is applied with shift pressed, do a local import. Note that the user is only actually //able to do that if the popup menu is focused (i.e.: request completion and do a tab to focus it, instead //of having the focus on the editor and just pressing up/down). if ((stateMask & SWT.SHIFT) != 0) { this.setAddLocalImport(true); } apply(document, trigger, stateMask, offset); } /** * Note: This apply is not directly called (it's called through * {@link CtxInsensitiveImportComplProposal#apply(ITextViewer, char, int, int)}) * * This is the point where the completion is written. It has to be written and if some import is also available * it should be inserted at this point. * * We have to be careful to only add an import if that's really needed (e.g.: there's no other import that * equals the import that should be added). * * Also, we have to check if this import should actually be grouped with another import that already exists. * (and it could be a multi-line import) */ public void apply(IDocument document, char trigger, int stateMask, int offset) { if (this.indentString == null) { throw new RuntimeException("Indent string not set (not called with a PyEdit as viewer?)"); } if (!triggerCharAppliesCurrentCompletion(trigger, document, offset)) { newForcedOffset = offset + 1; //+1 because that's the len of the trigger return; } try { PySelection selection = new PySelection(document); int lineToAddImport = -1; ImportHandleInfo groupInto = null; ImportHandleInfo realImportHandleInfo = null; boolean groupImports = ImportsPreferencesPage.getGroupImports(); LineStartingScope previousLineThatStartsScope = null; PySelection ps = null; if (this.addLocalImport) { ps = new PySelection(document, offset); int startLineIndex = ps.getStartLineIndex(); if (startLineIndex == 0) { this.addLocalImport = false; } else { previousLineThatStartsScope = ps.getPreviousLineThatStartsScope(PySelection.INDENT_TOKENS, startLineIndex - 1, PySelection.getFirstCharPosition(ps.getCursorLineContents())); if (previousLineThatStartsScope == null) { //note that if we have no previous scope, it means we're actually on the global scope, so, //proceed as usual... this.addLocalImport = false; } } } if (realImportRep.length() > 0 && !this.addLocalImport) { //Workaround for: https://sourceforge.net/tracker/?func=detail&aid=2697165&group_id=85796&atid=577329 //when importing from __future__ import with_statement, we actually want to add a 'with' token, not //with_statement token. boolean isWithStatement = realImportRep.equals("from __future__ import with_statement"); if (isWithStatement) { this.fReplacementString = "with"; } if (groupImports) { try { realImportHandleInfo = new ImportHandleInfo(realImportRep); PyImportsHandling importsHandling = new PyImportsHandling(document); for (ImportHandle handle : importsHandling) { if (handle.contains(realImportHandleInfo)) { lineToAddImport = -2; //signal that there's no need to find a line available to add the import break; } else if (groupInto == null && realImportHandleInfo.getFromImportStr() != null) { List<ImportHandleInfo> handleImportInfo = handle.getImportInfo(); for (ImportHandleInfo importHandleInfo : handleImportInfo) { if (realImportHandleInfo.getFromImportStr().equals( importHandleInfo.getFromImportStr())) { List<String> commentsForImports = importHandleInfo.getCommentsForImports(); if (commentsForImports.size() > 0 && commentsForImports.get(commentsForImports.size() - 1).length() == 0) { groupInto = importHandleInfo; break; } } } } } } catch (ImportNotRecognizedException e1) { Log.log(e1);//that should not happen at this point } } if (lineToAddImport == -1) { boolean isFutureImport = PySelection.isFutureImportLine(this.realImportRep); lineToAddImport = selection.getLineAvailableForImport(isFutureImport); } } else { lineToAddImport = -1; } String delimiter = PyAction.getDelimiter(document); appliedWithTrigger = trigger == '.' || trigger == '('; String appendForTrigger = ""; if (appliedWithTrigger) { if (trigger == '(') { appendForTrigger = "()"; } else if (trigger == '.') { appendForTrigger = "."; } } //if the trigger is ')', just let it apply regularly -- so, ')' will only be added if it's already in the completion. //first do the completion if (fReplacementString.length() > 0) { int dif = offset - fReplacementOffset; document.replace(offset - dif, dif + this.fLen, fReplacementString + appendForTrigger); } if (this.addLocalImport) { //All the code below is because we don't want to work with a generated AST (as it may not be there), //so, we go to the previous scope, find out the valid indent inside it and then got backwards //from the position we're in now to find the closer location to where we're now where we're //actually able to add the import. try { int iLineStartingScope; if (previousLineThatStartsScope != null) { iLineStartingScope = previousLineThatStartsScope.iLineStartingScope; //Go to a non-empty line from the line we have and the line we're currently in. int iLine = iLineStartingScope + 1; String line = ps.getLine(iLine); int startLineIndex = ps.getStartLineIndex(); while (iLine < startLineIndex && (line.startsWith("#") || line.trim().length() == 0)) { iLine++; line = ps.getLine(iLine); } if (iLine >= startLineIndex) { //Sanity check! iLine = startLineIndex; line = ps.getLine(iLine); } int firstCharPos = PySelection.getFirstCharPosition(line); //Ok, all good so far, now, this would add the line to the beginning of //the element (after the if statement, def, etc.), let's try to put //it closer to where we're now (but still in a valid position). int j = startLineIndex; while (j >= 0) { String line2 = ps.getLine(j); if (PySelection.getFirstCharPosition(line2) == firstCharPos) { iLine = j; break; } if (j == iLineStartingScope) { break; } j--; } String indent = line.substring(0, firstCharPos); String strToAdd = indent + realImportRep + delimiter; ps.addLine(strToAdd, iLine - 1); //Will add it just after the line passed as a parameter. importLen = strToAdd.length(); return; } } catch (Exception e) { Log.log(e); //Something went wrong, add it as global (i.e.: BUG) } } if (groupInto != null && realImportHandleInfo != null) { //let's try to group it int maxCols = 80; if (PydevPlugin.getDefault() != null) { IPreferenceStore chainedPrefStore = PydevPrefs.getChainedPrefStore(); maxCols = chainedPrefStore .getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN); } int endLine = groupInto.getEndLine(); IRegion lineInformation = document.getLineInformation(endLine); String strToAdd = ", " + realImportHandleInfo.getImportedStr().get(0); String line = PySelection.getLine(document, endLine); if (line.length() + strToAdd.length() > maxCols) { if (line.indexOf('#') == -1) { //no comments: just add it in the next line int len = line.length(); if (line.trim().endsWith(")")) { len = line.indexOf(")"); strToAdd = "," + delimiter + indentString + realImportHandleInfo.getImportedStr().get(0); } else { strToAdd = ",\\" + delimiter + indentString + realImportHandleInfo.getImportedStr().get(0); } int end = lineInformation.getOffset() + len; importLen = strToAdd.length(); document.replace(end, 0, strToAdd); return; } } else { //regular addition (it won't pass the number of columns expected). line = PySelection.getLineWithoutCommentsOrLiterals(line); int len = line.length(); if (line.trim().endsWith(")")) { len = line.indexOf(")"); } int end = lineInformation.getOffset() + len; importLen = strToAdd.length(); document.replace(end, 0, strToAdd); return; } } //if we got here, it hasn't been added in a grouped way, so, let's add it in a new import if (lineToAddImport >= 0 && lineToAddImport <= document.getNumberOfLines()) { IRegion lineInformation = document.getLineInformation(lineToAddImport); String strToAdd = realImportRep + delimiter; importLen = strToAdd.length(); document.replace(lineInformation.getOffset(), 0, strToAdd); return; } } catch (BadLocationException x) { Log.log(x); } } @Override public Point getSelection(IDocument document) { if (newForcedOffset >= 0) { return new Point(newForcedOffset, 0); } int pos = fReplacementOffset + fReplacementString.length() + importLen; if (appliedWithTrigger) { pos += 1; } return new Point(pos, 0); } public final String getInternalDisplayStringRepresentation() { return fReplacementString; } /** * If another proposal with the same name exists, this method will be called to determine if * both completions should coexist or if one of them should be removed. */ @Override public int getOverrideBehavior(ICompletionProposal curr) { if (curr instanceof CtxInsensitiveImportComplProposal) { if (curr.getDisplayString().equals(getDisplayString())) { return BEHAVIOR_IS_OVERRIDEN; } else { return BEHAVIOR_COEXISTS; } } else { return BEHAVIOR_IS_OVERRIDEN; } } }