/** * Copyright (c) 2005-2013 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 Apr 12, 2005 * * @author Fabio Zadrozny */ package org.python.pydev.editor.correctionassist.docstrings; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.swt.graphics.Image; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.PySelection.DocstringInfo; 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.correctionassist.heuristics.IAssistProps; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_ui.ImageCache; import org.python.pydev.shared_ui.UIConstants; import org.python.pydev.shared_ui.proposals.IPyCompletionProposal; import org.python.pydev.shared_ui.proposals.PyCompletionProposal; public class AssistDocString implements IAssistProps { private final String docStringStyle; public AssistDocString() { this(null); } /** * @param docStringStyle the doc string prefix to be used (i.e.: '@' or ':'). If null, it's gotten from the preferences. */ public AssistDocString(String docStringStyle) { this.docStringStyle = docStringStyle; } /** * @see org.python.pydev.editor.correctionassist.heuristics.IAssistProps#getProps(org.python.pydev.core.docutils.PySelection, * org.python.pydev.shared_ui.ImageCache) */ @Override public List<ICompletionProposal> getProps(PySelection ps, ImageCache imageCache, File f, IPythonNature nature, PyEdit edit, int offset) throws BadLocationException { ArrayList<ICompletionProposal> l = new ArrayList<ICompletionProposal>(); Tuple<List<String>, Integer> tuple = ps.getInsideParentesisToks(false); if (tuple == null) { if (ps.isInClassLine()) { tuple = new Tuple<List<String>, Integer>(new ArrayList<String>(), offset); } else { return l; } } List<String> params = tuple.o1; int lineOfOffset = ps.getLineOfOffset(tuple.o2); // Calculate only the initial part of the docstring here (everything else should be lazily computed on apply). String initial = PySelection.getIndentationFromLine(ps.getCursorLineContents()); String delimiter = PyAction.getDelimiter(ps.getDoc()); String indentation = edit != null ? edit.getIndentPrefs().getIndentationString() : DefaultIndentPrefs.get( nature).getIndentationString(); String delimiterAndIndent = delimiter + initial + indentation; FastStringBuffer buf = new FastStringBuffer(); String docStringMarker = DocstringsPrefPage.getDocstringMarker(); buf.append(delimiterAndIndent + docStringMarker); buf.append(delimiterAndIndent); int newOffset = buf.length(); int offsetPosToAdd = ps.getEndLineOffset(lineOfOffset); Image image = null; //may be null (testing) if (imageCache != null) { image = imageCache.get(UIConstants.ASSIST_DOCSTRING); } final boolean inFunctionLine = ps.isInFunctionLine(true); DocstringInfo docstringFromFunction = null; if (inFunctionLine) { int currLine = ps.getLineOfOffset(); docstringFromFunction = ps.getDocstringFromLine(currLine + 1); } final DocstringInfo finalDocstringFromFunction = docstringFromFunction; l.add(new PyCompletionProposal("", offsetPosToAdd, 0, newOffset, image, finalDocstringFromFunction != null ? "Update docstring" : "Make docstring", null, null, IPyCompletionProposal.PRIORITY_DEFAULT, null) { @Override public void apply(IDocument document) { if (inFunctionLine) { String preferredDocstringStyle = AssistDocString.this.docStringStyle; if (preferredDocstringStyle == null) { preferredDocstringStyle = DocstringsPrefPage.getPreferredDocstringStyle(); } // Let's check if this function already has a docstring (if it does, update the current docstring // instead of creating a new one). String updatedDocstring = null; if (finalDocstringFromFunction != null) { updatedDocstring = updatedDocstring(finalDocstringFromFunction.string, params, delimiter, initial + indentation, preferredDocstringStyle); } if (updatedDocstring != null) { fReplacementOffset = finalDocstringFromFunction.startLiteralOffset; fReplacementLength = finalDocstringFromFunction.endLiteralOffset - finalDocstringFromFunction.startLiteralOffset; int initialLen = buf.length(); buf.clear(); fCursorPosition -= initialLen - buf.length(); buf.append(updatedDocstring); } else { // Create the docstrings for (String paramName : params) { if (!PySelection.isIdentifier(paramName)) { continue; } buf.append(delimiterAndIndent).append(preferredDocstringStyle).append("param ") .append(paramName) .append(":"); if (DocstringsPrefPage.getTypeTagShouldBeGenerated(paramName)) { buf.append(delimiterAndIndent).append(preferredDocstringStyle).append("type ") .append(paramName) .append(":"); } } } } else { // It's a class declaration - do nothing. } if (finalDocstringFromFunction == null) { buf.append(delimiterAndIndent).append(docStringMarker); } String comp = buf.toString(); this.fReplacementString = comp; //remove the next line if it is a pass... if (finalDocstringFromFunction == null) { PySelection ps = new PySelection(document, fReplacementOffset); int iNextLine = ps.getCursorLine() + 1; String nextLine = ps.getLine(iNextLine); if (nextLine.trim().equals("pass")) { ps.deleteLine(iNextLine); } } super.apply(document); } }); return l; } private static class ParamInfo { public int paramLine = -1; public int typeLine = -1; public ParamInfo() { } } /** * @see org.python.pydev.editor.correctionassist.heuristics.IAssistProps#isValid(org.python.pydev.core.docutils.PySelection, * java.lang.String) */ @Override public boolean isValid(PySelection ps, String sel, PyEdit edit, int offset) { return ps.isInFunctionLine(true) || ps.isInClassLine(); } /** * @param baseDocstring this is the existing docstring (starting with ''', ', """, "). * @param params * @param delimiterAndIndent * @param docstringStyle * @return null if there was some issue updating the existing docstring. */ public static String updatedDocstring(String baseDocstring, List<String> params, String delimiter, String indent, String docstringStyle) { String docStringStartEnd; if (baseDocstring.startsWith("\"\"\"") && baseDocstring.endsWith("\"\"\"")) { docStringStartEnd = "\"\"\""; } else if (baseDocstring.startsWith("'''") && baseDocstring.endsWith("'''")) { docStringStartEnd = "'''"; } else if (baseDocstring.startsWith("'") && baseDocstring.endsWith("'")) { docStringStartEnd = "'"; } else if (baseDocstring.startsWith("\"") && baseDocstring.endsWith("\"")) { docStringStartEnd = "\""; } else { return null; } // Get it without the initial char quotes. baseDocstring = baseDocstring.substring(docStringStartEnd.length(), baseDocstring.length() - docStringStartEnd.length()); // Parse the existing string to find existing param and type declarations. // param xxx: Pattern paramPattern = Pattern .compile("\\s*(" + Pattern.quote(docstringStyle) + "param(\\s)+)(\\w+)(\\s)*" + Pattern.quote(":")); // param dict(foo->`bar`) xxx: Pattern paramPatternWithTypeOnSphinx = Pattern .compile("\\s*(" + Pattern.quote(docstringStyle) + "param(\\s)+.*\\s+)(\\w+)(\\s)*" + Pattern.quote(":")); // type xxx: Pattern typePattern = Pattern .compile("\\s*(" + Pattern.quote(docstringStyle) + "type(\\s)+)(\\w+)(\\s)*" + Pattern.quote(":")); // @test // :test Pattern otherPattern = Pattern .compile("\\s*" + Pattern.quote(docstringStyle) + "(\\w)+(\\b)"); Map<String, ParamInfo> paramInfos = new HashMap<>(); List<String> splitted = StringUtils.splitInLines(baseDocstring, false); String firstLine = splitted.get(0).trim(); if (firstLine.length() > 0) { // First line must have only the delimiter. splitted.add(0, ""); splitted.set(1, indent + splitted.get(1)); } Set<String> paramsWithTypeInline = new HashSet<>(); // Any :xxx tag that is not param nor type. Set<Integer> otherMatches = new HashSet<>(); int size = splitted.size(); for (int i = 0; i < size; i++) { String s = splitted.get(i); Matcher paramMatcher = paramPattern.matcher(s); if (paramMatcher.lookingAt()) { // Ok, we found some existing parameter docstring. String paramName = paramMatcher.group(3); getParamInfo(paramInfos, paramName).paramLine = i; } else { Matcher matcher = paramPatternWithTypeOnSphinx.matcher(s); if (matcher.lookingAt()) { // Ok, we found some existing parameter docstring. String paramName = matcher.group(3); getParamInfo(paramInfos, paramName).paramLine = i; paramsWithTypeInline.add(paramName); } else { Matcher typeMatcher = typePattern.matcher(s); if (typeMatcher.lookingAt()) { // Ok, we found some existing parameter type docstring. String paramName = typeMatcher.group(3); getParamInfo(paramInfos, paramName).typeLine = i; } else { Matcher otherMatcher = otherPattern.matcher(s); if (otherMatcher.lookingAt()) { otherMatches.add(i); } } } } } // Now, actually go on and insert the new strings. FastStringBuffer buf = new FastStringBuffer(); int paramsSize = params.size(); for (int paramI = 0; paramI < paramsSize; paramI++) { String paramName = params.get(paramI); if (!PySelection.isIdentifier(paramName)) { continue; } ParamInfo existingInfo = paramInfos.get(paramName); boolean hasParam = existingInfo != null && existingInfo.paramLine != -1; boolean addTypeForParam = DocstringsPrefPage.getTypeTagShouldBeGenerated(paramName); if (addTypeForParam) { if (paramsWithTypeInline.contains(paramName)) { addTypeForParam = false; } } boolean hasType = existingInfo != null && existingInfo.typeLine != -1; if (hasParam && hasType) { // Both there (keep on going, nothing to do). } else if (!hasParam && !hasType) { ParamInfo newParamInfo = null; if (existingInfo == null) { newParamInfo = new ParamInfo(); } int addIndex = getAddIndex(paramName, params, paramI, paramInfos, otherMatches, splitted); // neither is present, so, add both at a given location (if needed). splitted.add(addIndex, buf.append(indent).append(docstringStyle).append("param ") .append(paramName).append(":").toString()); buf.clear(); incrementExistingLines(paramInfos, otherMatches, addIndex); if (newParamInfo != null) { newParamInfo.paramLine = addIndex; } if (addTypeForParam) { splitted.add(addIndex + 1, buf.append(indent).append(docstringStyle).append("type ") .append(paramName).append(":").toString()); buf.clear(); incrementExistingLines(paramInfos, otherMatches, addIndex); if (newParamInfo != null) { newParamInfo.typeLine = addIndex + 1; } } paramInfos.put(paramName, newParamInfo); } else { if (hasParam) { // Add only the type after the existing param (if it has to be added) if (addTypeForParam) { int addIndex = existingInfo.paramLine + 1; splitted.add(addIndex, buf.append(indent).append(docstringStyle).append("type ") .append(paramName).append(":").toString()); buf.clear(); incrementExistingLines(paramInfos, otherMatches, addIndex); existingInfo.typeLine = addIndex; } } else { // Add only the param before the existing type. int addIndex = existingInfo.typeLine; splitted.add(addIndex, buf.append(indent).append(docstringStyle).append("param ") .append(paramName).append(":").toString()); buf.clear(); incrementExistingLines(paramInfos, otherMatches, addIndex); existingInfo.paramLine = addIndex; } } } String lastLine = splitted.get(splitted.size() - 1).trim(); if (lastLine.length() > 0) { // Last line must have only the indentation (compute after all new tags are added). splitted.add(indent); } buf.append(docStringStartEnd).append(StringUtils.join(delimiter, splitted)).append(docStringStartEnd); return buf.toString(); } private static void incrementExistingLines(Map<String, ParamInfo> paramInfos, Set<Integer> otherMatches, int index) { Set<Entry<String, ParamInfo>> entrySet = paramInfos.entrySet(); for (Entry<String, ParamInfo> entry : entrySet) { ParamInfo paramInfo = entry.getValue(); if (paramInfo.paramLine >= index) { paramInfo.paramLine++; } if (paramInfo.typeLine >= index) { paramInfo.typeLine++; } } HashSet<Integer> newOtherMatches = new HashSet<>(); for (Iterator<Integer> it = otherMatches.iterator(); it.hasNext();) { Integer i = it.next(); if (i >= index) { newOtherMatches.add(i + 1); } else { newOtherMatches.add(i); } } otherMatches.clear(); otherMatches.addAll(newOtherMatches); } private static int getAddIndex(String paramName, List<String> params, int paramI, Map<String, ParamInfo> paramInfos, Set<Integer> otherMatches, List<String> splitted) { if (paramI == 0) { int min = splitted.size(); // Last pos (after header) // See if there's some other tag (if there is, this one should be before it. if (paramInfos.size() > 0) { Set<Entry<String, ParamInfo>> entrySet = paramInfos.entrySet(); for (Entry<String, ParamInfo> entry : entrySet) { ParamInfo paramInfo = entry.getValue(); if (paramInfo.paramLine != -1) { min = Math.min(paramInfo.paramLine, min); } if (paramInfo.typeLine != -1) { min = Math.min(paramInfo.typeLine, min); } } } for (Integer i : otherMatches) { min = Math.min(i, min); } return min; } else { // We should add it after some existing param. String prevParam = params.get(paramI - 1); ParamInfo paramInfo = paramInfos.get(prevParam); // At this point, paramInfo from the previous parameter *must* be valid (we should've // filled it when adding the previous parameter if it still wasn't there). if (paramInfo.typeLine != -1) { return paramInfo.typeLine + 1; } else { return paramInfo.paramLine + 1; } } } private static ParamInfo getParamInfo(Map<String, ParamInfo> paramInfos, String paramName) { ParamInfo paramInfo = paramInfos.get(paramName); if (paramInfo == null) { paramInfo = new ParamInfo(); paramInfos.put(paramName, paramInfo); } return paramInfo; } }