~vil/pydev/upstream

1 by Vladimír Lapáček
Initial import of upstream
1
/*
2
 * Created on Aug 11, 2004
3
 *
4
 * @author Fabio Zadrozny
5
 */
6
package org.python.pydev.editor.codecompletion;
7
8
import java.io.File;
9
import java.util.ArrayList;
10
import java.util.Arrays;
11
import java.util.Collection;
12
import java.util.HashSet;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.StringTokenizer;
16
17
import org.eclipse.core.runtime.CoreException;
18
import org.eclipse.jface.text.BadLocationException;
19
import org.eclipse.jface.text.IDocument;
20
import org.eclipse.jface.text.IRegion;
21
import org.eclipse.jface.text.ITextViewer;
22
import org.eclipse.jface.text.contentassist.CompletionProposal;
23
import org.eclipse.jface.text.contentassist.ICompletionProposal;
24
import org.eclipse.swt.graphics.Image;
25
import org.python.pydev.core.ExtensionHelper;
26
import org.python.pydev.core.ICodeCompletionASTManager;
27
import org.python.pydev.core.ICompletionState;
28
import org.python.pydev.core.IModule;
29
import org.python.pydev.core.IPythonNature;
30
import org.python.pydev.core.IToken;
31
import org.python.pydev.core.bundle.ImageCache;
32
import org.python.pydev.core.docutils.DocUtils;
33
import org.python.pydev.editor.codecompletion.revisited.ASTManager;
34
import org.python.pydev.editor.codecompletion.revisited.CompletionRecursionException;
35
import org.python.pydev.editor.codecompletion.revisited.CompletionState;
36
import org.python.pydev.editor.codecompletion.revisited.modules.AbstractModule;
37
import org.python.pydev.editor.codecompletion.revisited.modules.CompiledModule;
38
import org.python.pydev.editor.codecompletion.revisited.visitors.FindScopeVisitor;
39
import org.python.pydev.editor.codecompletion.revisited.visitors.Scope;
40
import org.python.pydev.editor.codecompletion.shell.AbstractShell;
41
import org.python.pydev.parser.PyParser;
42
import org.python.pydev.parser.jython.SimpleNode;
43
import org.python.pydev.parser.jython.ast.ClassDef;
44
import org.python.pydev.parser.jython.ast.Name;
45
import org.python.pydev.parser.jython.ast.NameTok;
46
import org.python.pydev.parser.visitors.NodeUtils;
47
import org.python.pydev.plugin.PydevPlugin;
48
import org.python.pydev.ui.UIConstants;
49
50
/**
51
 * @author Dmoore
52
 * @author Fabio Zadrozny
53
 */
54
public class PyCodeCompletion {
55
56
    /**
57
     * Type for unknown.
58
     */
59
    public static final int TYPE_UNKNOWN = -1;
60
61
    /**
62
     * Type for import (used to decide the icon)
63
     */
64
    public static final int TYPE_IMPORT = 0;
65
    
66
    /**
67
     * Type for class (used to decide the icon)
68
     */
69
    public static final int TYPE_CLASS = 1;
70
    
71
    /**
72
     * Type for function (used to decide the icon)
73
     */
74
    public static final int TYPE_FUNCTION = 2;
75
    
76
    /**
77
     * Type for attr (used to decide the icon)
78
     */
79
    public static final int TYPE_ATTR = 3;
80
    
81
    /**
82
     * Type for attr (used to decide the icon)
83
     */
84
    public static final int TYPE_BUILTIN = 4;
85
    
86
    /**
87
     * Type for parameter (used to decide the icon)
88
     */
89
    public static final int TYPE_PARAM = 5;
90
    
91
    /**
92
     * Type for package (used to decide the icon)
93
     */
94
    public static final int TYPE_PACKAGE = 6;
95
96
    /**
97
     * Type for relative import
98
     */
99
    public static final int TYPE_RELATIVE_IMPORT = 7;
100
    
101
102
    /**
103
     * Returns an image for the given type
104
     * @param type
105
     * @return
106
     */
107
    public static Image getImageForType(int type){
108
        try {
109
            ImageCache imageCache = PydevPlugin.getImageCache();
110
            if (imageCache == null)
111
                return null;
112
113
            switch (type) {
114
            case PyCodeCompletion.TYPE_IMPORT:
115
                return imageCache.get(UIConstants.COMPLETION_IMPORT_ICON);
116
117
            case PyCodeCompletion.TYPE_CLASS:
118
                return imageCache.get(UIConstants.COMPLETION_CLASS_ICON);
119
120
            case PyCodeCompletion.TYPE_FUNCTION:
121
                return imageCache.get(UIConstants.PUBLIC_METHOD_ICON);
122
123
            case PyCodeCompletion.TYPE_ATTR:
124
                return imageCache.get(UIConstants.PUBLIC_ATTR_ICON);
125
126
            case PyCodeCompletion.TYPE_BUILTIN:
127
                return imageCache.get(UIConstants.BUILTINS_ICON);
128
129
            case PyCodeCompletion.TYPE_PARAM:
130
                return imageCache.get(UIConstants.COMPLETION_PARAMETERS_ICON);
131
132
            case PyCodeCompletion.TYPE_PACKAGE:
133
                return imageCache.get(UIConstants.COMPLETION_PACKAGE_ICON);
134
                
135
            case PyCodeCompletion.TYPE_RELATIVE_IMPORT:
136
                return imageCache.get(UIConstants.COMPLETION_RELATIVE_IMPORT_ICON);
137
138
            default:
139
                return null;
140
            }
141
            
142
        } catch (Exception e) {
143
            PydevPlugin.log(e, false);
144
            return null;
145
        }
146
    }
147
148
    /**
149
     * Returns a list with the tokens to use for autocompletion.
150
     * 
151
     * The list is composed from tuples containing the following:
152
     * 
153
     * 0 - String  - token name
154
     * 1 - String  - token description
155
     * 2 - Integer - token type (see constants)
156
     * @param viewer 
157
     * 
158
     * @return list of IToken.
159
     * 
160
     * (This is where we do the "REAL" work).
161
     * @throws BadLocationException
162
     */
163
    public List getCodeCompletionProposals(ITextViewer viewer, CompletionRequest request) throws CoreException, BadLocationException {
164
        
165
        ArrayList ret = new ArrayList();
166
        try {
167
        	IPythonNature pythonNature = request.nature;
168
            if (pythonNature == null) {
169
                throw new RuntimeException("Unable to get python nature.");
170
            }
171
            ICodeCompletionASTManager astManager = pythonNature.getAstManager();
172
            if (astManager == null) { //we're probably still loading it.
173
                return new ArrayList();
174
            }
175
176
            List theList = new ArrayList();
177
            try {
178
                if (CompiledModule.COMPILED_MODULES_ENABLED) {
179
                    AbstractShell.getServerShell(request.nature, AbstractShell.COMPLETION_SHELL); //just start it
180
                }
181
            } catch (Exception e) {
182
                throw new RuntimeException(e);
183
            }
184
185
            String trimmed = request.activationToken.replace('.', ' ').trim();
186
187
            String importsTipper = getImportsTipperStr(request);
188
189
            int line = request.doc.getLineOfOffset(request.documentOffset);
190
            IRegion region = request.doc.getLineInformation(line);
191
192
            CompletionState state = new CompletionState(line, request.documentOffset - region.getOffset(), null, request.nature);
193
194
            boolean importsTip = false;
195
            //code completion in imports 
196
            if (importsTipper.length() != 0) {
197
198
                //get the project and make the code completion!!
199
                //so, we want to do a code completion for imports...
200
                //let's see what we have...
201
202
                importsTip = true;
203
                importsTipper = importsTipper.trim();
204
                IToken[] imports = astManager.getCompletionsForImport(importsTipper, request);
205
                theList.addAll(Arrays.asList(imports));
206
207
                //code completion for a token
208
            } else if (trimmed.equals("") == false && request.activationToken.indexOf('.') != -1) {
209
210
                if (request.activationToken.endsWith(".")) {
211
                    request.activationToken = request.activationToken.substring(0, request.activationToken.length() - 1);
212
                }
213
                
214
                List completions = new ArrayList();
215
                if (trimmed.equals("self") || trimmed.startsWith("self")) {
216
                    getSelfCompletions(request, theList, state);
217
218
                } else {
219
220
                    state.activationToken = request.activationToken;
221
222
                    //Ok, looking for a token in globals.
223
                    IToken[] comps = astManager.getCompletionsForToken(request.editorFile, request.doc, state);
224
                    theList.addAll(Arrays.asList(comps));
225
                }
226
                theList.addAll(completions);
227
228
            } else { //go to globals
229
230
                state.activationToken = request.activationToken;
231
                IToken[] comps = astManager.getCompletionsForToken(request.editorFile, request.doc, state);
232
233
                theList.addAll(Arrays.asList(comps));
234
                
235
                theList.addAll(getGlobalsFromParticipants(request, state));
236
            }
237
238
            changeItokenToCompletionPropostal(viewer, request, ret, theList, importsTip);
239
        } catch (CompletionRecursionException e) {
240
            ret.add(new CompletionProposal("",request.documentOffset,0,0,null,e.getMessage(), null,null));
241
        }
242
243
        return ret;
244
    }
245
246
    private Collection getGlobalsFromParticipants(CompletionRequest request, ICompletionState state) {
247
        ArrayList ret = new ArrayList();
248
        
249
        List participants = ExtensionHelper.getParticipants(ExtensionHelper.PYDEV_COMPLETION);
250
        for (Iterator iter = participants.iterator(); iter.hasNext();) {
251
            IPyDevCompletionParticipant participant = (IPyDevCompletionParticipant) iter.next();
252
            ret.addAll(participant.getGlobalCompletions(request, state));
253
        }
254
        return ret;
255
    }
256
257
    /**
258
     * @param request
259
     * @param pythonNature
260
     * @param astManager
261
     * @param theList OUT - returned completions are added here. (IToken instances)
262
     * @param line 
263
     * @param state
264
     * @return the same tokens added in theList
265
     */
266
    public static IToken[] getSelfCompletions(CompletionRequest request, List theList, CompletionState state) {
267
        return getSelfCompletions(request, theList, state, false);
268
    }
269
    
270
    /**
271
     * @param request
272
     * @param pythonNature
273
     * @param astManager
274
     * @param theList OUT - returned completions are added here. (IToken instances)
275
     * @param line 
276
     * @param state
277
     * @return the same tokens added in theList
278
     */
279
    public static IToken[] getSelfCompletions(CompletionRequest request, List theList, CompletionState state, boolean getOnlySupers) {
280
    	IToken[] comps = new IToken[0];
281
        SimpleNode s = PyParser.reparseDocument(new PyParser.ParserInfo(request.doc, true, request.nature, state.line)).o1;
282
        if(s != null){
283
            FindScopeVisitor visitor = new FindScopeVisitor(state.line, 0);
284
            try {
285
                s.accept(visitor);
286
                comps = getSelfCompletions(visitor.scope, request, theList, state, getOnlySupers);
287
            } catch (Exception e1) {
288
                e1.printStackTrace();
289
            }
290
        }
291
        return comps;
292
    }
293
    
294
    /**
295
     * Get self completions when you already have a scope
296
     */
297
    public static IToken[] getSelfCompletions(Scope scope, CompletionRequest request, List theList, CompletionState state, boolean getOnlySupers) throws BadLocationException {
298
    	IToken[] comps = new IToken[0];
299
        while(scope.scope.size() > 0){
300
            SimpleNode node = (SimpleNode) scope.scope.pop();
301
            if(node instanceof ClassDef){
302
                ClassDef d = (ClassDef) node;
303
                
304
                if(getOnlySupers){
305
                    List gottenComps = new ArrayList();
306
                    for (int i = 0; i < d.bases.length; i++) {
307
                        if(d.bases[i] instanceof Name){
308
                            Name n = (Name) d.bases[i];
309
	                        state.activationToken = n.id;
310
	        	            IToken[] completions = request.nature.getAstManager().getCompletionsForToken(request.editorFile, request.doc, state);
311
	        	            gottenComps.addAll(Arrays.asList(completions));
312
                        }
313
                    }
314
                    comps = (IToken[]) gottenComps.toArray(new IToken[0]);
315
                }else{
316
                    //ok, get the completions for the class, only thing we have to take care now is that we may 
317
                    //not have only 'self' for completion, but somthing lile self.foo.
318
                    //so, let's analyze our activation token to see what should we do.
319
                    
320
                    String trimmed = request.activationToken.replace('.', ' ').trim();
321
                    String[] actTokStrs = trimmed.split(" ");
322
                    if(actTokStrs.length == 0 || actTokStrs[0].equals("self") == false){
323
                        throw new AssertionError("We need to have at least one token (self) for doing completions in the class.");
324
                    }
325
                    
326
                    if(actTokStrs.length == 1){
327
                        //ok, it's just really self, let's get on to get the completions
328
                        state.activationToken = NodeUtils.getNameFromNameTok((NameTok) d.name);
329
        	            comps = request.nature.getAstManager().getCompletionsForToken(request.editorFile, request.doc, state);
330
        	            
331
                    }else{
332
                        //it's not only self, so, first we have to get the definition of the token
333
                        //the first one is self, so, just discard it, and go on, token by token to know what is the last 
334
                        //one we are completing (e.g.: self.foo.bar)
335
                        int line = request.doc.getLineOfOffset(request.documentOffset);
336
                        IRegion region = request.doc.getLineInformationOfOffset(request.documentOffset);
337
                        int col =  request.documentOffset - region.getOffset();
338
                        IModule module = AbstractModule.createModuleFromDoc("", null, request.doc, request.nature, line);
339
                      
340
                        ASTManager astMan = ((ASTManager)request.nature.getAstManager());
341
                        comps = astMan.getAssignCompletions(module, new CompletionState(line, col, request.activationToken, request.nature));
342
343
                    }
344
                }
345
	            theList.addAll(Arrays.asList(comps));
346
            }
347
        }
348
        return comps;
349
350
    }
351
352
    /**
353
     * @param viewer 
354
     * @param request
355
     * @param convertedProposals
356
     * @param iTokenList
357
     * @param importsTip
358
     */
359
    private void changeItokenToCompletionPropostal(ITextViewer viewer, CompletionRequest request, List convertedProposals, List iTokenList, boolean importsTip) {
360
        //TODO: check org.eclipse.jface.text.templates.TemplateCompletionProcessor to see how to do custom 'selections' in completions
361
//        int offset = request.documentOffset;
362
//        ITextSelection selection= (ITextSelection) viewer.getSelectionProvider().getSelection();
363
//
364
//        // adjust offset to end of normalized selection
365
//        if (selection.getOffset() == offset)
366
//            offset= selection.getOffset() + selection.getLength();
367
//
368
//        String prefix= extractPrefix(viewer, offset);
369
//        Region region= new Region(offset - prefix.length(), prefix.length());
370
//
371
//        TemplateContextType contextType= getContextType(viewer, region);
372
//        if (contextType != null) {
373
//            IDocument document= viewer.getDocument();
374
//            new DocumentTemplateContext(contextType, document, region.getOffset(), region.getLength());
375
//        }
376
377
        
378
        for (Iterator iter = iTokenList.iterator(); iter.hasNext();) {
379
            
380
            Object obj = iter.next();
381
            
382
            if(obj instanceof IToken){
383
                IToken element =  (IToken) obj;
384
                
385
                String name = element.getRepresentation();
386
                
387
                //GET the ARGS
388
                int l = name.length();
389
                
390
                String args = "";
391
                if(! importsTip){
392
	                args = getArgs(element);                
393
	                if(args.length()>0){
394
	                    l++; //cursor position is name + '('
395
	                }
396
                }
397
                //END
398
                
399
                String docStr = element.getDocStr();
400
                int type = element.getType();
401
                
402
                int priority = IPyCompletionProposal.PRIORITY_DEFAULT;
403
                if(type == PyCodeCompletion.TYPE_PARAM){
404
                    priority = IPyCompletionProposal.PRIORITY_LOCALS;
405
                }
406
                    
407
                PyCompletionProposal proposal = new PyCompletionProposal(name+args,
408
                        request.documentOffset - request.qlen, request.qlen, l, getImageForType(type), null, null, docStr, priority);
409
                
410
                convertedProposals.add(proposal);
411
            
412
            }else if(obj instanceof Object[]){
413
                Object element[] = (Object[]) obj;
414
                
415
                String name = (String) element[0];
416
                String docStr = (String) element [1];
417
                int type = -1;
418
                if(element.length > 2){
419
                    type = ((Integer) element [2]).intValue();
420
                }
421
422
                int priority = IPyCompletionProposal.PRIORITY_DEFAULT;
423
                if(type == PyCodeCompletion.TYPE_PARAM){
424
                    priority = IPyCompletionProposal.PRIORITY_LOCALS;
425
                }
426
                
427
                PyCompletionProposal proposal = new PyCompletionProposal(name,
428
                        request.documentOffset - request.qlen, request.qlen, name.length(), getImageForType(type), null, null, docStr, priority);
429
                
430
                convertedProposals.add(proposal);
431
                
432
            }else if(obj instanceof ICompletionProposal){
433
                //no need to convert
434
                convertedProposals.add(obj);
435
            }
436
            
437
        }
438
    }
439
440
    
441
    
442
    /**
443
     * @param element
444
     * @param args
445
     * @return
446
     */
447
    private String getArgs(IToken element) {
448
        String args = "";
449
        if(element.getArgs().trim().length() > 0){
450
            StringBuffer buffer = new StringBuffer("(");
451
            StringTokenizer strTok = new StringTokenizer(element.getArgs(), "( ,)");
452
453
            while(strTok.hasMoreTokens()){
454
                String tok = strTok.nextToken();
455
                if(tok.equals("self") == false){
456
                    if(buffer.length() > 1){
457
                        buffer.append(", ");
458
                    }
459
                    buffer.append(tok);
460
                }
461
            }
462
            buffer.append(")");
463
            args = buffer.toString();
464
        } else if (element.getType() == PyCodeCompletion.TYPE_FUNCTION){
465
            args = "()";
466
        }
467
        
468
        return args;
469
    }
470
471
472
473
    /**
474
     * Returns non empty string if we are in imports section 
475
     * 
476
     * @param theActivationToken
477
     * @param edit
478
     * @param doc
479
     * @param documentOffset
480
     * @return single space string if we are in imports but without any module
481
     *         string with current module (e.g. foo.bar.
482
     */
483
    public String getImportsTipperStr(CompletionRequest request) {
484
        
485
        IDocument doc = request.doc;
486
        int documentOffset = request.documentOffset;
487
        
488
        return getImportsTipperStr(doc, documentOffset);
489
    }
490
491
    public static String getImportsTipperStr(IDocument doc, int documentOffset) {
492
        IRegion region;
493
        try {
494
            region = doc.getLineInformationOfOffset(documentOffset);
495
            String trimmedLine = doc.get(region.getOffset(), documentOffset-region.getOffset());
496
            trimmedLine = trimmedLine.trim();
497
            return getImportsTipperStr(trimmedLine, true);
498
        } catch (BadLocationException e) {
499
            throw new RuntimeException(e);
500
        }
501
    }
502
    
503
    /**
504
     * @param doc
505
     * @param documentOffset
506
     * @return
507
     */
508
    public static String getImportsTipperStr(String trimmedLine, boolean returnEvenEmpty) {
509
        String importMsg = "";
510
        
511
        if(!trimmedLine.startsWith("from") && !trimmedLine.startsWith("import")){
512
            return "";
513
        }
514
        
515
        int fromIndex = trimmedLine.indexOf("from");
516
        int importIndex = trimmedLine.indexOf("import");
517
518
        //check if we have a from or an import.
519
        if(fromIndex  != -1 || importIndex != -1){
520
            trimmedLine = trimmedLine.replaceAll("#.*", ""); //remove comments 
521
            String[] strings = trimmedLine.split(" ");
522
            
523
            if(fromIndex != -1 && importIndex == -1){
524
                if(strings.length > 2){
525
                    //user has spaces as in  'from xxx uuu'
526
                    return "";
527
                }
528
            }
529
            
530
            
531
            for (int i = 0; i < strings.length; i++) {
532
                if(strings[i].equals("from")==false && strings[i].equals("import")==false){
533
                    if(importMsg.length() != 0){
534
                        importMsg += '.';
535
                    }
536
                    importMsg += strings[i];
537
                }
538
            }
539
            
540
            if(fromIndex  != -1 && importIndex != -1){
541
                if(strings.length == 3){
542
                    importMsg += '.';
543
                }
544
            }
545
        }else{
546
            return "";
547
        }
548
        if (importMsg.indexOf(".") == -1){
549
            if(returnEvenEmpty || importMsg.trim().length() > 0){
550
                return " "; //we have only import fff or from iii (so, we're going for all imports).
551
            }else{
552
                return ""; //we have only import fff or from iii (so, we're going for all imports).
553
            }
554
        }
555
556
        //now, we may still have something like 'unittest.test,' or 'unittest.test.,'
557
        //so, we have to remove this comma (s).
558
        int i;
559
        while ( ( i = importMsg.indexOf(',')) != -1){
560
            if(importMsg.charAt(i-1) == '.'){
561
                int j = importMsg.lastIndexOf('.');
562
                importMsg = importMsg.substring(0, j);
563
            }
564
            
565
            int j = importMsg.lastIndexOf('.');
566
            importMsg = importMsg.substring(0, j);
567
        }
568
569
        //if it is something like aaa.sss.bb : removes the bb because it is the qualifier
570
        //if it is something like aaa.sss.   : removes only the last point
571
        if (importMsg.length() > 0 && importMsg.indexOf('.') != -1){
572
            importMsg = importMsg.substring(0, importMsg.lastIndexOf('.'));
573
        }
574
        
575
        
576
        return importMsg;
577
    }
578
579
    /**
580
     * Return a document to parse, using some heuristics to make it parseable.
581
     * 
582
     * @param doc
583
     * @param documentOffset
584
     * @return
585
     */
586
    public static String getDocToParse(IDocument doc, int documentOffset) {
587
        int lineOfOffset = -1;
588
        try {
589
            lineOfOffset = doc.getLineOfOffset(documentOffset);
590
        } catch (BadLocationException e) {
591
            e.printStackTrace();
592
        }
593
        
594
        if(lineOfOffset!=-1){
595
            String docToParseFromLine = getDocToParseFromLine(doc, lineOfOffset);
596
            if(docToParseFromLine != null)
597
                return docToParseFromLine;
598
//                return "\n"+docToParseFromLine;
599
            else
600
                return "";
601
        }else{
602
            return "";
603
        }
604
    }
605
606
    /**
607
     * Return a document to parse, using some heuristics to make it parseable.
608
     * (Changes the line specified by a pass)
609
     * 
610
     * @param doc
611
     * @param documentOffset
612
     * @param lineOfOffset
613
     * @return
614
     */
615
    public static String getDocToParseFromLine(IDocument doc, int lineOfOffset) {
616
        return DocUtils.getDocToParseFromLine(doc, lineOfOffset);
617
    }
618
619
    /**
620
     * 
621
     * @param useSimpleTipper
622
     * @return the script to get the variables.
623
     * 
624
     * @throws CoreException
625
     */
626
    public static File getAutoCompleteScript() throws CoreException {
627
        return PydevPlugin.getScriptWithinPySrc("simpleTipper.py");
628
    }
629
630
    
631
    /**
632
     * @param pythonAndTemplateProposals
633
     * @param qualifier
634
     * @return
635
     */
636
    public ICompletionProposal[] onlyValidSorted(List pythonAndTemplateProposals, String qualifier) {
637
        //FOURTH: Now, we have all the proposals, only thing is deciding wich ones are valid (depending on
638
        //qualifier) and sorting them correctly.
639
        Collection returnProposals = new HashSet();
640
        String lowerCaseQualifier = qualifier.toLowerCase();
641
        
642
        for (Iterator iter = pythonAndTemplateProposals.iterator(); iter.hasNext();) {
643
            Object o = iter.next();
644
            if (o instanceof ICompletionProposal) {
645
                ICompletionProposal proposal = (ICompletionProposal) o;
646
            
647
	            if (proposal.getDisplayString().toLowerCase().startsWith(lowerCaseQualifier)) {
648
	                returnProposals.add(proposal);
649
	            }
650
            }else{
651
                throw new RuntimeException("Error: expected instanceof ICompletionProposal and received: "+o.getClass().getName());
652
            }
653
        }
654
655
        ICompletionProposal[] proposals = new ICompletionProposal[returnProposals.size()];
656
657
        // and fill with list elements
658
        returnProposals.toArray(proposals);
659
660
        Arrays.sort(proposals, PROPOSAL_COMPARATOR);
661
        return proposals;
662
    }
663
664
    /**
665
     * Compares proposals so that we can order them.
666
     */
667
    public static final ProposalsComparator PROPOSAL_COMPARATOR = new ProposalsComparator();
668
669
}