// Copyright (c) 2003-2008 by Leif Frenzel - see http://leiffrenzel.de
// Copyright (c) 2011 by Alejandro Serrano
// This code is made available under the terms of the Eclipse Public License,
// version 1.0 (EPL). See http://www.eclipse.org/legal/epl-v10.html
package net.sf.eclipsefp.haskell.ui.internal.editors.haskell.codeassist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import net.sf.eclipsefp.haskell.browser.BrowserPlugin;
import net.sf.eclipsefp.haskell.browser.Database;
import net.sf.eclipsefp.haskell.browser.items.Constructor;
import net.sf.eclipsefp.haskell.browser.items.Declaration;
import net.sf.eclipsefp.haskell.browser.items.Documented;
import net.sf.eclipsefp.haskell.browser.items.Gadt;
import net.sf.eclipsefp.haskell.browser.items.Instance;
import net.sf.eclipsefp.haskell.browser.items.Local;
import net.sf.eclipsefp.haskell.browser.items.Module;
import net.sf.eclipsefp.haskell.browser.items.PackageIdentifier;
import net.sf.eclipsefp.haskell.browser.items.Packaged;
import net.sf.eclipsefp.haskell.browser.items.TypeClass;
import net.sf.eclipsefp.haskell.browser.util.HtmlUtil;
import net.sf.eclipsefp.haskell.browser.util.ImageCache;
import net.sf.eclipsefp.haskell.buildwrapper.BWFacade;
import net.sf.eclipsefp.haskell.buildwrapper.BuildWrapperPlugin;
import net.sf.eclipsefp.haskell.buildwrapper.types.CabalPackage;
import net.sf.eclipsefp.haskell.buildwrapper.types.ImportDef;
import net.sf.eclipsefp.haskell.buildwrapper.types.Location;
import net.sf.eclipsefp.haskell.buildwrapper.types.ThingAtPoint;
import net.sf.eclipsefp.haskell.core.cabalmodel.CabalSyntax;
import net.sf.eclipsefp.haskell.core.cabalmodel.PackageDescription;
import net.sf.eclipsefp.haskell.core.cabalmodel.PackageDescriptionLoader;
import net.sf.eclipsefp.haskell.core.cabalmodel.PackageDescriptionStanza;
import net.sf.eclipsefp.haskell.core.compiler.CompilerManager;
import net.sf.eclipsefp.haskell.core.util.ResourceUtil;
import net.sf.eclipsefp.haskell.ui.HaskellUIPlugin;
import net.sf.eclipsefp.haskell.ui.internal.backend.BackendManager;
import net.sf.eclipsefp.haskell.ui.internal.editors.haskell.HaskellEditor;
import net.sf.eclipsefp.haskell.ui.internal.editors.haskell.imports.AnImport;
import net.sf.eclipsefp.haskell.ui.internal.editors.haskell.imports.AnImport.FileDocumented;
import net.sf.eclipsefp.haskell.ui.internal.editors.haskell.imports.ImportsManager;
import net.sf.eclipsefp.haskell.ui.internal.preferences.editor.IEditorPreferenceNames;
import net.sf.eclipsefp.haskell.ui.internal.preferences.editor.ProposalScope;
import net.sf.eclipsefp.haskell.ui.internal.util.UITexts;
import net.sf.eclipsefp.haskell.util.FileUtil;
import net.sf.eclipsefp.haskell.util.HaskellText;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
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.CompletionProposal;
import org.eclipse.jface.text.contentassist.ContentAssistEvent;
import org.eclipse.jface.text.contentassist.ContentAssistant;
import org.eclipse.jface.text.contentassist.ICompletionListener;
import org.eclipse.jface.text.contentassist.ICompletionListenerExtension;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.keys.IBindingService;
/**
* Computes the content assist completion proposals and context information. This class is fairly stateful, since
* what are presented as completion proposals depends on where the editor's point is located and the preceding token's
* lexical type.
*
* @author Leif Frenzel (original author)
* @author B. Scott Michel (bscottm@ieee.org)
* @author Alejandro Serrano
* @author JP Moresmau
*/
public class HaskellContentAssistProcessor implements IContentAssistProcessor {
/** The associated content assistant, used to add/remove listeners */
private final ContentAssistant assistant;
/** Current completion prefix */
private String prefix;
/** The different context states that the completion processor needs to track */
enum CompletionContext {
NO_CONTEXT
, DEFAULT_CONTEXT
, IMPORT_STMT
, IMPORT_LIST
, TYCON_CONTEXT
, CONID_CONTEXT
, LANGUAGE_EXTENSIONS_CONTEXT
}
/** Default number of tokens to grab before point when determining completion context */
// private final static int NUM_PRECEDING_TOKENS = 10;
/** The current completion context state */
private CompletionContext context;
/** The original prefix offset */
private int prefixOffsetAnchor;
// Module context variables:
/** Module names in the modules graph */
private ArrayList<String> moduleGraphNames;
/** Module names exposed by the cabal project file */
private ArrayList<String> exposedModules;
/** The module name for import list completion */
private String moduleName;
private char[] autoCompletionCharacters=null;
private Boolean searchAll;
/**
* the currently showing scope of proposals
*/
private ProposalScope scope=ProposalScope.IMPORTED;
/**
* the key binding for content assist
*/
private String contentAssistBinding;
/**
* The constructor.
*
* @param assistant The associated content assistant
*/
public HaskellContentAssistProcessor(final ContentAssistant assistant) {
super();
this.assistant = assistant;
internalReset();
String s= HaskellUIPlugin.getDefault().getPreferenceStore().getString( IEditorPreferenceNames.CA_AUTOACTIVATION_TRIGGERS ); //new char[] { '.' };
if (s!=null){
autoCompletionCharacters=s.toCharArray();
}
// Add the listener, who modulates the completion context
this.assistant.addCompletionListener( new CAListener() );
}
// =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=
// interface methods of IContentAssistProcessor
// =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=
/**
* {@inheritDoc}
*/
@Override
public ICompletionProposal[] computeCompletionProposals(final ITextViewer viewer, final int offset) {
// reset to default
assistant.setStatusLineVisible( false );
assistant.setShowEmptyList( false );
//ScionTokenScanner sts=((HaskellEditor)HaskellUIPlugin.getTextEditor( viewer )).getScanner();
IFile theFile = HaskellUIPlugin.getFile( viewer );
IDocument doc = viewer.getDocument();
// Figure out what we're doing...
//ScionInstance scion = HaskellUIPlugin.getScionInstance( viewer );
String prf = getCompletionPrefix( doc, offset );
if (!isHaskell( theFile )){
scope=ProposalScope.ALL;
} else {
if (prf!=null){ // && (!prf.startsWith( prefix ) || prf.length()==0)){
if (prf.length()>0){
if (prf.equals( prefix )){
scope=scope.next();
}
}
// || !prf.startsWith( prefix )
if (prf.length()==0){
scope=ProposalScope.IMPORTED;
}
}
}
// HaskellUIPlugin.log( "prefix:"+prefix+", prf:"+prf+",scope:"+scope,IStatus.INFO );
prefix=prf;
switch (context) {
case NO_CONTEXT: {
//if ( scion != null ) {
int offsetPrefix = offset - prefix.length();
searchAll=prefix.length()>0;
try {
Location lineBegin = new Location(theFile.toString(), doc, doc.getLineInformationOfOffset( offset ));
if (offsetPrefix < 0) {
offsetPrefix = 0;
}
//Region point = new Region( offsetPrefix, 0 );
//HaskellLexerToken[] tokens = scion.tokensPrecedingPoint( NUM_PRECEDING_TOKENS, theFile, doc, point );
IRegion lineR=doc.getLineInformationOfOffset( offsetPrefix );
String line=doc.get(lineR.getOffset(),lineR.getLength());
String lineContents = doc.get( lineBegin.getStartOffset( doc ), offset - lineBegin.getStartOffset( doc ) );
if (lineContents.trim().startsWith( "import" ) && lineContents.contains( "(" )) {
context = CompletionContext.IMPORT_LIST;
// Get the module name
String[] words = lineContents.trim().split( "[ ]+" );
if (words.length > 1) {
moduleName = words[1];
if (moduleName.equals( "qualified" )) {
if (words.length > 2) {
moduleName = words[2];
} else {
moduleName = null;
}
}
} else {
moduleName = null;
}
// Return imports list
return importsList( viewer, theFile, doc, offset );
}
if (line.startsWith( "import" )) {
return moduleNamesContext(theFile,offset);
} else if (line.startsWith("{-# LANGUAGE")){
return getLanguageExtensions( offset );
} else if (line.contains("::") || line.contains("->")) {
return defaultCompletionContext( viewer, theFile, doc, offset, true );
}
//if (tokens != null) {
// if (LexerTokenCategories.hasImportContext( tokens, lineBegin )) {
// return moduleNamesContext(theFile,offset);
// } else if (LexerTokenCategories.hasTyConContext( tokens, lineBegin )) {
// return defaultCompletionContext( viewer, theFile, doc, offset, true );
// }
// }
} catch (BadLocationException ble) {
// Ignore, pass through to default completion context.
}
return defaultCompletionContext( viewer, theFile, doc, offset, false );
// }
// break;
}
case DEFAULT_CONTEXT: {
return defaultCompletionContext( viewer, theFile, doc, offset, false );
}
case IMPORT_STMT: {
return filterModuleNames( offset );
}
case TYCON_CONTEXT: {
return defaultCompletionContext( viewer, theFile, doc, offset, true );
}
case IMPORT_LIST: {
return importsList( viewer, theFile, doc, offset );
}
case CONID_CONTEXT: {
return null;
}
case LANGUAGE_EXTENSIONS_CONTEXT: {
return getLanguageExtensions( offset );
}
}
return null;
}
@Override
public IContextInformation[] computeContextInformation(final ITextViewer viewer, final int documentOffset) {
// unused
return null;
}
@Override
public char[] getCompletionProposalAutoActivationCharacters() {
return autoCompletionCharacters;
}
@Override
public char[] getContextInformationAutoActivationCharacters() {
// unused
return null;
}
@Override
public String getErrorMessage() {
// return null to indicate we had no problems
return null;
}
@Override
public IContextInformationValidator getContextInformationValidator() {
// unused
return null;
}
/** Hard reset internal state */
private void internalReset() {
context = CompletionContext.NO_CONTEXT;
prefixOffsetAnchor = -1;
prefix = new String();
moduleGraphNames = null;
exposedModules = null;
moduleName = null;
scope=ProposalScope.IMPORTED;
}
/** Get the completion prefix from the prefix offset, if non-zero, or reverse lex */
private String getCompletionPrefix( final IDocument doc, final int offset ) {
if (prefixOffsetAnchor > 0 && offset >= prefixOffsetAnchor ) {
try {
return doc.get( prefixOffsetAnchor, offset - prefixOffsetAnchor );
} catch( BadLocationException ex ) {
// Should not happen, but fall through to lexCompletionPrefix() call
}
}
// Prefix offset anchor isn't set, or fell through as the result of the exception
String retval = lexCompletionPrefix( doc, offset );
prefixOffsetAnchor = offset - retval.length();
return retval;
}
private boolean isHaskell(final IFile theFile){
return theFile!=null && BuildWrapperPlugin.getFacade( theFile.getProject() )!=null;
}
/**
* Default completion context, if no other context can be determined.
*/
private ICompletionProposal[] defaultCompletionContext( final ITextViewer viewer, final IFile theFile, final IDocument doc,
final int offset, final boolean typesHavePriority) {
boolean needSearch=prefix.length()>0 && Boolean.TRUE.equals(searchAll) ;
// we display the scope status bar if we can cycle
if (needSearch){
if (isHaskell( theFile )){
assistant.setStatusLineVisible( true );
// this doesn't work in the constructor, for some reason, so use lazy init
if (contentAssistBinding==null){
IBindingService bsvc=(IBindingService)PlatformUI.getWorkbench().getService( IBindingService.class );
contentAssistBinding=bsvc.getBestActiveBindingFormattedFor( "org.eclipse.ui.edit.text.contentAssist.proposals" );
}
String msg=NLS.bind( UITexts.proposal_category_mode, contentAssistBinding, scope.next().getDescription());
assistant.setStatusMessage( msg);
} else {
assistant.setStatusLineVisible( false );
}
// we need to show the list so the user sees the message to cycle through scopes
assistant.setShowEmptyList( true );
}
context = typesHavePriority ? CompletionContext.TYCON_CONTEXT : CompletionContext.DEFAULT_CONTEXT;
HaskellCompletionContext haskellCompletions = new HaskellCompletionContext( doc.get(), offset );
// ICompletionProposal[] haskellProposals = haskellCompletions.computeProposals();
HSCodeTemplateAssistProcessor templates = new HSCodeTemplateAssistProcessor();
ICompletionProposal[] templateProposals = templates.computeCompletionProposals( viewer, offset );
// Get rest of proposals
String prefix = haskellCompletions.getPointedQualifier();
HaskellEditor editor=(HaskellEditor)HaskellUIPlugin.getTextEditor( viewer );
ImportsManager mgr =editor!=null?editor.getImportsManager():null;
//new ImportsManager( theFile, doc );
//long t0=System.currentTimeMillis();
Map<String, Documented> decls = new HashMap<>();
//long t1=System.currentTimeMillis();
//HaskellUIPlugin.log( "getDeclarations:"+(t1-t0), IStatus.INFO );
ArrayList<String> elts = new ArrayList<>();
ArrayList<String> typeElts = new ArrayList<>();
// declaration key to package
Map<String,String> packages=new HashMap<>();
// constructor key to declaration name (if we use a constructor from a new module, we need to import to data type, not the constructor)
Map<String,String> constructors=new HashMap<>();
//ProposalScope ps=ProposalScope.valueOf( HaskellUIPlugin.getDefault().getPreferenceStore().getString( IEditorPreferenceNames.CA_PROPOSALS_SCOPE ) );
Map<String,Documented> importeds=mgr!=null?mgr.getDeclarations():new HashMap<String, Documented>();
IProject project=theFile.getProject();
if (needSearch && !scope.equals( ProposalScope.IMPORTED )){
try {
Set<String> pkgs=ResourceUtil.getImportPackages( new IFile[]{theFile});
// List<SymbolDef> sds=BuildWrapperPlugin.getDefault().getUsageAPI().listDefinedSymbols( theFile.getProject() );
// for (SymbolDef sd:sds){
// String name=sd.getName();
// if (name.startsWith( prefix ) && !importeds.containsKey( name )){
// name=name+" ("+sd.getModule()+")";
// Documented d=null;
// String comm=sd.getComment();
// if (comm==null){
// comm="";
// }
// switch (sd.getType()){
// case UsageQueryFlags.TYPE_CONSTRUCTOR:
// d=new Constructor( comm, sd.getName(), "?",null );
// break;
// case UsageQueryFlags.TYPE_TYPE:
// d=new DataType( comm, new String[0], sd.getName(), new String[0], "", new Constructor[0] );
// break;
// case UsageQueryFlags.TYPE_VAR:
// d=new Function( comm, sd.getName(), "" );
// }
// decls.put(name, d );
// }
// }
if (scope.equals( ProposalScope.ALL )){
// search on everything
if (BrowserPlugin.getSharedInstance().isAnyDatabaseLoaded() && !BrowserPlugin.getSharedInstance().isRunning()){
Packaged<Declaration>[] browserDecls=BrowserPlugin.getSharedInstance().getDeclarationsFromPrefix(Database.LOCAL, prefix);
if (browserDecls.length > 0) {
// If the browser found the module
for (Packaged<Declaration> browserDecl : browserDecls) {
boolean newPackage=!pkgs.contains( browserDecl.getPackage().getName() );
addBrowserDecl( browserDecl, decls, packages, constructors, newPackage );
}
}
}
} else if (scope.equals( ProposalScope.PROJECT )){
// things in project
for (IContainer src:ResourceUtil.getAllSourceContainers( theFile )){
Collection<IFile> fs=ResourceUtil.getSourceFiles(src);
addFileSymbols(prefix,fs,importeds,decls,constructors);
}
// we reference ourselves, more exactly the library stanza
// so we need to retrieve exposed modules from the library
if (pkgs.contains( project.getName() )){
IFile cf=BuildWrapperPlugin.getCabalFile( project );
PackageDescription pd=PackageDescriptionLoader.load(cf);
Map<String,List<PackageDescriptionStanza>> pds=pd.getStanzasBySourceDir();
// retrieve all possible source containers for the library
Set<IContainer> srcs=new HashSet<>();
PackageDescriptionStanza pdLibrary=null;
for (String src:pds.keySet()){
for (PackageDescriptionStanza pd1:pds.get( src )){
if (CabalSyntax.SECTION_LIBRARY.equals(pd1.getType())){
srcs.add( ResourceUtil.getContainer(project,src) );
pdLibrary=pd1;
break;
}
}
}
if (pdLibrary!=null){
Collection<IFile> fs=new ArrayList<>();
// find the file for all exposed modules
String ps=pdLibrary.getProperties().get( CabalSyntax.FIELD_EXPOSED_MODULES );
List<String> ls=PackageDescriptionLoader.parseList( ps );
for (String m:ls){
String path=m.replace( '.', '/' );
outer:for (String ext:FileUtil.haskellExtensions){
for (IContainer fldr:srcs){
IFile file=fldr.getFile( new Path( path+"."+ext ) ); //$NON-NLS-1$
if (file.exists()){
fs.add(file);
break outer;
}
}
}
}
addFileSymbols(prefix,fs,importeds,decls,constructors);
}
}
// search on dependent packages only
BWFacade f=BuildWrapperPlugin.getFacade( project );
if (f!=null && BrowserPlugin.getSharedInstance().isAnyDatabaseLoaded() && !BrowserPlugin.getSharedInstance().isRunning()){
for (CabalPackage[] cps:f.getPackagesByDB().values()){
for (CabalPackage cp:cps){
if (pkgs.contains( cp.getName() )){
if (!f.getProject().getName().equals( cp.getName() )){
Database pkg=Database.Package( new PackageIdentifier( cp.getName(), cp.getVersion() ) );
Packaged<Declaration>[] browserDecls=BrowserPlugin.getSharedInstance().getDeclarationsFromPrefix(pkg, prefix);
if (browserDecls.length > 0) {
// If the browser found the module
for (Packaged<Declaration> browserDecl : browserDecls) {
addBrowserDecl( browserDecl, decls, packages, constructors, false );
}
}
}
}
}
}
}
}
} catch (Exception e){
HaskellUIPlugin.log( e );
}
}
if (scope.equals( ProposalScope.IMPORTED )){
addLocals( editor, project, theFile, doc, offset, importeds );
decls.putAll(importeds);
}
for ( Map.Entry<String, Documented> s : decls.entrySet() ) {
if ( s.getKey().startsWith( prefix ) && s.getValue()!=null) {
if (s.getValue().isType()) {
typeElts.add( s.getKey() );
} else {
elts.add( s.getKey() );
}
}
}
Comparator<String> pointedComparator = new Comparator<String>() {
@Override
public int compare( final String a, final String b ) {
boolean aPointed = isPointed(a);
boolean bPointed = isPointed(b);
if (aPointed && !bPointed) {
return 1;
} else if (!aPointed && bPointed) {
return -1;
} else {
return a.compareToIgnoreCase( b );
}
}
};
Collections.sort( elts, pointedComparator );
Collections.sort( typeElts, pointedComparator );
// Merge the results together (templates precede generated proposals):
int totalSize = templateProposals.length + elts.size() + typeElts.size();
int endIndex = 0;
ICompletionProposal[] result = new ICompletionProposal[ totalSize ];
if ( templateProposals.length > 0 ) {
System.arraycopy( templateProposals, 0, result, endIndex, templateProposals.length );
endIndex += templateProposals.length;
}
final int plength = prefix.length();
int i = 0;
if (typesHavePriority) {
i = 0;
for ( String s : typeElts ) {
Documented d = decls.get( s );
result[endIndex + i] = getCompletionProposal( s, d, offset, plength, true,packages.get( s ) ,null);
i++;
}
endIndex += typeElts.size();
}
i = 0;
for ( String s : elts ) {
Documented d = decls.get( s );
String realImport=null;
if (d instanceof Constructor){
realImport=constructors.get( s )+"(..)";
}
result[endIndex + i] =getCompletionProposal( s, d, offset, plength, false,packages.get( s ) ,realImport);
i++;
}
endIndex += elts.size();
if (!typesHavePriority) {
i = 0;
for ( String s : typeElts ) {
Documented d = decls.get( s );
result[endIndex + i] = getCompletionProposal( s, d, offset, plength, true,packages.get( s ) ,null);
i++;
}
endIndex += typeElts.size();
}
//scope=scope.next();
//HaskellUIPlugin.log( "scope:"+scope,IStatus.INFO);
return result;
// return (totalSize > 0 ? result : null);
}
private static void addFileSymbols(final String prefix, final Collection<IFile> fs,final Map<String,Documented> importeds,final Map<String, Documented> decls,final Map<String,String> constructors){
for (IFile f:fs){
String module=ResourceUtil.getModuleName( f );
for (FileDocumented fd:AnImport.getDeclarationsFromFile( f )){
String name=fd.getDocumented().getName();
if (name.startsWith( prefix ) && !importeds.containsKey( name )){
name=name+" ("+module+")";
decls.put(name, fd.getDocumented() );
if (fd.getDocumented() instanceof Constructor){
Constructor c=(Constructor)fd.getDocumented();
if (c.getTypeName()!=null){
constructors.put(name, c.getTypeName() );
}
}
}
}
}
}
/**
* add local definition from the method
* @param editor
* @param project
* @param theFile
* @param doc
* @param offset
* @param importeds
*/
private static void addLocals(final HaskellEditor editor,final IProject project,final IFile theFile,final IDocument doc,final int offset,final Map<String,Documented> importeds){
if (editor!=null){
BWFacade f=BuildWrapperPlugin.getFacade(project) ;
if (f!=null){
Location odL=editor.getOutlineSpan( offset );
if (odL!=null){
List<ThingAtPoint> taps=f.getLocals( theFile,odL);
for (ThingAtPoint t:taps){
Local func=new Local("",t.getName(),t.getType());
if (!importeds.containsKey( t.getName() )){
importeds.put(t.getName(),func);
}
}
}
}
}
}
private static boolean isPointed(final String name){
int ix=name.indexOf( " (" );
if (ix>-1){
return name.substring(0,ix).indexOf( '.' ) != -1;
}
return name.indexOf( '.' ) != -1;
}
private void addBrowserDecl(final Packaged<Declaration> browserDecl,final Map<String, Documented> decls,final Map<String,String> packages,final Map<String,String> constructors, final boolean newPackage){
String key= browserDecl.getElement().getName()+" (";
// prepend package name before module to indicate this will create a new reference
if (newPackage){
key+=browserDecl.getPackage().getName()+":";
}
key+=browserDecl.getElement().getModule().getName()+")";
if (!decls.containsKey( key ) && !decls.containsKey( browserDecl.getElement().getModule().getName()+"."+key )){
decls.put(key,browserDecl.getElement() );
if (newPackage){
packages.put( key, browserDecl.getPackage().getName() );
}
if (browserDecl.getElement() instanceof Gadt) {
Gadt g = (Gadt)browserDecl.getElement();
for (Constructor c : g.getConstructors()) {
key= c.getName()+" ("+browserDecl.getElement().getModule().getName()+")";
decls.put(key,c );
constructors.put( key, browserDecl.getElement().getName() );
if (newPackage){
packages.put( key, browserDecl.getPackage().getName() );
}
}
}
}
}
private ICompletionProposal getCompletionProposal(final String s,final Documented d,final int offset,final int length,final boolean isType,final String pkg,final String realImport){
Image i=isType?
ImageCache.getImageForDeclaration( ((Declaration)d).getType() ):
ImageCache.getImageForBinding( d ) ;
// new modules are shown after the declaration name
String repl=s;
int ix=s.indexOf( " (" );
String module=null;
if (ix>-1){
repl=s.substring( 0,ix );
module=s.substring( ix+2,s.length()-1 );
ix=module.indexOf( ":" );
if (ix>-1){
module=module.substring( ix+1 );
}
}
return new FullHaskellCompletionProposal( repl, offset - length, length, s.length(),
i, s,
HtmlUtil.generateDocument( d.getCompleteDefinition(), d.getDoc() ),pkg,module,realImport);
}
private ICompletionProposal[] importsList( final ITextViewer viewer, final IFile theFile,
final IDocument doc, final int offset ) {
// Case when we don't know the module name yet
if (moduleName == null) {
return new ICompletionProposal[0];
}
// Get prefix
HaskellCompletionContext haskellCompletions = new HaskellCompletionContext( doc.get(), offset );
String prefix = haskellCompletions.getPointedQualifier();
int plength = prefix.length();
// Reuse the general "imports" code, getting out the qualified names
AnImport imp = new AnImport( new ImportDef(moduleName, null, false, false, null ),false);
Map<String, FileDocumented> decls = imp.getDeclarations( theFile.getProject(), theFile, doc );
ArrayList<String> names = new ArrayList<>();
for (Map.Entry<String, FileDocumented> decl : decls.entrySet()) {
String s = decl.getKey();
if (s.indexOf( '.' ) == -1 && s.startsWith( prefix )) {
// Don't add qualified imports
Documented d = decl.getValue().getDocumented();
if (!(d instanceof Instance) && !(d instanceof TypeClass)) {
names.add( s );
}
}
}
Collections.sort( names );
ICompletionProposal[] r = new ICompletionProposal[names.size()];
for (int i = 0; i < names.size(); i++) {
String s = names.get( i );
Documented d = decls.get( s ).getDocumented();
String ddoc=BrowserPlugin.getDoc( d );
r[i] = new CompletionProposal( s, offset - plength, plength, s.length(),
d instanceof Constructor ? ImageCache.CONSTRUCTOR : ImageCache.getImageForDeclaration( ((Declaration)d).getType() ),
s, null, HtmlUtil.generateDocument( d.getCompleteDefinition(), ddoc ) );
}
return r;
}
// private ICompletionProposal[] moduleNamesContext(final ScionInstance scion, final int offset) {
// // Grab all of the module names, keep them cached for the duration of the completion session
//
//
// moduleGraphNames = new ArrayList<String>();
// moduleGraphNames.addAll( scion.moduleGraph() );
//
// exposedModules = new ArrayList<String>();
// exposedModules.addAll( scion.listExposedModules() );
// context = CompletionContext.IMPORT_STMT;
//
// return filterModuleNames( offset );
// }
private ICompletionProposal[] moduleNamesContext(final IFile file, final int offset) {
moduleGraphNames = new ArrayList<>();
for (PackageDescriptionStanza pds: ResourceUtil.getApplicableStanzas( new IFile[]{file} )){
moduleGraphNames.addAll(pds.listAllModules());
}
exposedModules = new ArrayList<>();
if (BackendManager.getCabalImplDetails().isSandboxed()){
try {
for (IProject p:file.getProject().getReferencedProjects()){
if (ResourceUtil.hasHaskellNature( p )){
IFile f=BuildWrapperPlugin.getCabalFile( p );
if (f!=null){
PackageDescription pd=PackageDescriptionLoader.load(f);
PackageDescriptionStanza pds=pd.getLibraryStanza();
if (pds!=null){
exposedModules.addAll(pds.listExposedModules());
}
}
}
}
} catch (CoreException ce){
HaskellUIPlugin.log( ce );
}
}
return filterModuleNames( offset );
}
/**
* Filter module names given a matching prefix.
*
* @param offset The offset into the document where the completions will be inserted.
* @return An ICompletionProposal array of matching completions, or null if none.
*/
private ICompletionProposal[] filterModuleNames( final int offset ) {
// List<String> modules = new ArrayList<String>();
final String normalizedPrefix = prefix.toLowerCase(Locale.ENGLISH);
Set<String> modules = new HashSet<>();
for (String m : BrowserPlugin.getSharedInstance().getCachedModuleNames()) {
if (prefix.length() == 0 || m.toLowerCase().startsWith( normalizedPrefix )) {
modules.add(m);
}
}
for (String m : moduleGraphNames ) {
if (prefix.length() == 0 || m.toLowerCase().startsWith( normalizedPrefix )) {
modules.add(m);
}
}
for (String m : exposedModules ) {
if (prefix.length() == 0 || m.toLowerCase().startsWith( normalizedPrefix )) {
modules.add(m);
}
}
if (modules.size() > 0) {
String[] modulesA = modules.toArray( new String[modules.size()] );
Arrays.sort( modulesA, String.CASE_INSENSITIVE_ORDER );
ICompletionProposal[] result = new ICompletionProposal[modulesA.length];
int i = 0;
final int prefixLength = prefix.length();
for (String m : modulesA) {
Module realM = BrowserPlugin.getSharedInstance().getCachedModule( m );
result[i] = new CompletionProposal( m, prefixOffsetAnchor, prefixLength, m.length(), ImageCache.MODULE, m, null,
realM == null ? "" : HtmlUtil.generateDocument( null, realM.getDoc() ));
++i;
}
return result;
}
return null;
}
private ICompletionProposal[] getLanguageExtensions(final int offset ) {
String normalizedPrefix = prefix.toUpperCase(Locale.ENGLISH);
int ix=normalizedPrefix.lastIndexOf( ',' );
if (ix==-1){
ix=normalizedPrefix.lastIndexOf( ' ' );
}
if (ix>-1){
prefixOffsetAnchor+=ix+1;
normalizedPrefix=normalizedPrefix.substring( ix+1 ).trim();
}
List<String> extensions=CompilerManager.getExtensions();
if (extensions!=null){
List<String> ext = new ArrayList<>();
for (String e:extensions){
if (normalizedPrefix.equals("") || e.toUpperCase( Locale.ENGLISH ).startsWith( normalizedPrefix )){
ext.add(e);
}
}
ICompletionProposal[] result = new ICompletionProposal[ext.size()];
int i=0;
final int prefixLength = normalizedPrefix.length();
for (String e:ext){
result[i]=new CompletionProposal( e, prefixOffsetAnchor, prefixLength, e.length());
i++;
}
return result;
}
return null;
}
/**
* Filter completion pairs
*/
/*private ICompletionProposal[] getTypeCompletions( final ScionInstance scion, final IFile file, final IDocument doc,
final int offset ) {
Map<String, String> completionPairs = scion.completionsForTypes( file, doc );
ArrayList<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
if( completionPairs != null ) {
final String normalizedPrefix = prefix.toLowerCase();
TreeSet<String> sortedKeys = new TreeSet<String>(
String.CASE_INSENSITIVE_ORDER );
final int prefixLength = prefix.length();
sortedKeys.addAll( completionPairs.keySet() );
for( String k: sortedKeys ) {
// Key may be a qualified name
String name = k;
int qualifier = k.lastIndexOf( "." );
if( qualifier > 0 ) {
name = k.substring( qualifier + 1 );
}
if( prefix.length() == 0
|| name.toLowerCase().startsWith( normalizedPrefix ) ) {
// String fullProposal = name + " -- " + completionPairs.get( k );
//
// if( !name.equals( k ) ) {
// fullProposal = fullProposal + " as " + k;
// }
CompletionProposal proposal = new CompletionProposal( k,
prefixOffsetAnchor, prefixLength, k.length(), ImageCache.TYPE, k,
null, "" );
proposals.add( proposal );
}
}
}
return proposals.toArray( new ICompletionProposal[ proposals.size() ] );
}*/
/**
* Get the completion prefix by reverse lexing from the offset. The reverse lexing process stops at the beginning of the
* line on which the editor point (offset) is located, unless a token has been otherwise collected.
*
* @param document The document from which to extract the completion prefix
* @param offset The current editor point in the document.
* @return The completion prefix string or an empty string if reverse lexing did not find anything useful.
*/
public static final String lexCompletionPrefix( final IDocument document, final int offset ) {
// If we're beyond the document limit (how?), return an empty string
if( offset > document.getLength() ) {
return new String();
}
try {
IRegion lineAt = document.getLineInformationOfOffset( offset );
final int lineBegin = lineAt.getOffset();
int i = offset - 1;
final char ch = document.getChar( i );
if (HaskellText.isHaskellIdentifierPart( ch ) || ch == '.') {
// Scan backward until non-identifier character
for (--i; i >= lineBegin; --i) {
char innerC = document.getChar( i );
if ( !HaskellText.isHaskellIdentifierPart(innerC) && innerC != '.' ) {
break;
}
}
++i;
String retval = document.get( i, offset - i );
if ( !retval.startsWith( "_" ) ) {
return retval;
}
} else if ( HaskellText.isCommentPart( ch ) ) {
// Scan backward until a non-comment character:
for (--i; i >= lineBegin && HaskellText.isCommentPart( document.getChar( i ) ); --i) {
// NOP
}
++i;
String retval = document.get( i, offset - i );
// Ensure that the prefix is really the start of a comment.
if (retval.startsWith( "{-" ) || retval.startsWith( "--" ) ) {
return retval;
}
} else if ( HaskellText.isSymbol( ch ) ) {
// Scan backward until a non-comment character:
for (--i; i >= lineBegin && HaskellText.isSymbol( document.getChar( i ) ); --i) {
// NOP
}
++i;
return document.get( i, offset - i );
} else if (ch == '(' || ch == ')') {
// Don't include parentheses in a prefix, e.g., ":: (<point>".
return new String();
} else if (!Character.isWhitespace( ch )) {
// Punt! Grab what we can until we hit whitespace
for (--i; i >= lineBegin && !Character.isWhitespace( document.getChar(i) ); --i) {
// NOP
}
if (++i < offset) {
return document.get( i, offset - i);
}
}
} catch( BadLocationException e ) {
// Dunno how we'd generate this exception, but catch it anyway and fall through
}
return new String();
}
/** Content assistant listener: This initializes and manages the transitions between completion context states. */
private class CAListener implements ICompletionListener, ICompletionListenerExtension {
public CAListener() {
// NOP
}
@Override
public void assistSessionStarted( final ContentAssistEvent event ) {
// HaskellUIPlugin.log( "CA session starts, prefix = '" + (prefix != null ? prefix : "<null>") + "', context = " + context, null );
// Reset the context to force computeCompletionProposals to figure out what the context,
// clean out existing state:
HaskellContentAssistProcessor.this.internalReset();
}
@Override
public void assistSessionEnded( final ContentAssistEvent event ) {
// HaskellUIPlugin.log( "CA session ends.", null);
// Reset internal state for completeness
HaskellContentAssistProcessor.this.internalReset();
}
@Override
public void assistSessionRestarted( final ContentAssistEvent event ) {
// HaskellUIPlugin.log( "CA session restarts, prefix = '" + (prefix != null ? prefix : "<null>") + "', context = " + context, null );
}
@Override
public void selectionChanged( final ICompletionProposal proposal, final boolean smartToggle ) {
// HaskellUIPlugin.log( "CA session selection changed, prefix = '" + (prefix != null ? prefix : "<null>") + "', context = " + context, null );
// NOP
}
}
}