/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.debugger; import com.google.common.collect.ImmutableList; import com.intellij.debugger.NoDataException; import com.intellij.debugger.PositionManager; import com.intellij.debugger.SourcePosition; import com.intellij.debugger.engine.CompoundPositionManager; import com.intellij.debugger.engine.DebugProcess; import com.intellij.debugger.engine.DebugProcessImpl; import com.intellij.debugger.engine.JVMNameUtil; import com.intellij.debugger.engine.TopLevelParentClassProvider; import com.intellij.debugger.jdi.VirtualMachineProxyImpl; import com.intellij.debugger.requests.ClassPrepareRequestor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.impl.DirectoryIndex; import com.intellij.openapi.util.NullableComputable; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.Trinity; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiUtil; import com.intellij.util.Processor; import com.intellij.util.Query; import com.sun.jdi.AbsentInformationException; import com.sun.jdi.Location; import com.sun.jdi.ReferenceType; import com.sun.jdi.request.ClassPrepareRequest; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.module.IExecutionEnvironment; import gw.plugin.ij.core.PluginLoaderUtil; import gw.plugin.ij.filetypes.GosuCodeFileType; import gw.plugin.ij.filetypes.GosuFileTypes; import gw.plugin.ij.lang.psi.IGosuFile; import gw.plugin.ij.lang.psi.IGosuFileBase; import gw.plugin.ij.lang.psi.impl.GosuScratchpadFileImpl; import gw.plugin.ij.lang.psi.impl.expressions.GosuBlockExpressionImpl; import gw.plugin.ij.lang.psi.impl.resolvers.PsiTypeResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class GosuPositionManager implements PositionManager { private static final Logger LOG = Logger.getInstance( "#gw.plugin.ij.debugger.GosuPositionManager" ); private final DebugProcess _debugProcess; public GosuPositionManager( DebugProcess debugProcess ) { _debugProcess = debugProcess; } public DebugProcess getDebugProcess() { return _debugProcess; } /** * Returns the list of bytecode locations in a specific class corresponding to the specified position in the source code. * * @param type a Java class (one of the list returned by {@link #getAllClasses}). * @param position the position in the source code. * * @return the list of corresponding bytecode locations. * * @throws NoDataException if the location is not in the code managed by this {@code PositionManager} * @see ReferenceType#locationsOfLine(int) */ @NotNull public List<Location> locationsOfLine( @NotNull ReferenceType type, @NotNull SourcePosition position ) throws NoDataException { if( !GosuFileTypes.isTopLevelGosuFile( position.getFile() ) ) { throw new NoDataException(); } try { int line = position.getLine() + 1; List<Location> locations = getDebugProcess().getVirtualMachineProxy().versionHigher( "1.4" ) ? type.locationsOfLine( DebugProcessImpl.JAVA_STRATUM, null, line ) : type.locationsOfLine( line ); if( locations == null || locations.isEmpty() ) { throw new NoDataException(); } return locations; } catch( AbsentInformationException e ) { throw new NoDataException(); } } public ClassPrepareRequest createPrepareRequest( final ClassPrepareRequestor requestor, final SourcePosition position ) throws NoDataException { final Ref<String> waitPrepareFor = new Ref<>( null ); final Ref<ClassPrepareRequestor> waitRequestor = new Ref<>( null ); ApplicationManager.getApplication().runReadAction( new Runnable() { public void run() { PsiClass psiClass = JVMNameUtil.getClassAt( position ); if( psiClass == null ) { return; } if( PsiUtil.isLocalOrAnonymousClass( psiClass ) || getBlockAt( position.getElementAt() ) != null ) { PsiClass parent = TopLevelParentClassProvider.getTopLevelParentClass( psiClass ); if( parent == null ) { return; } final String parentQName = getClassName( parent ); if( parentQName == null ) { return; } waitPrepareFor.set( parentQName + "$*" ); waitRequestor.set( new ClassPrepareRequestor() { public void processClassPrepare( DebugProcess debuggerProcess, ReferenceType referenceType ) { final CompoundPositionManager positionManager = ((DebugProcessImpl)debuggerProcess).getPositionManager(); final List<ReferenceType> positionClasses = positionManager.getAllClasses( position ); if( positionClasses.isEmpty() ) { // fallback if( positionManager.locationsOfLine( referenceType, position ).size() > 0 ) { requestor.processClassPrepare( debuggerProcess, referenceType ); } } else { if( positionClasses.contains( referenceType ) ) { requestor.processClassPrepare( debuggerProcess, referenceType ); } } } } ); } else { waitPrepareFor.set( getClassName( psiClass ) ); waitRequestor.set( requestor ); } } } ); if( waitPrepareFor.get() == null ) { return null; // no suitable class found for this name } return _debugProcess.getRequestsManager().createClassPrepareRequest( waitRequestor.get(), waitPrepareFor.get() ); } private static String getClassName( PsiClass psiClass ) { if( psiClass.getContainingFile() instanceof GosuScratchpadFileImpl ) { return getScratchPadName( psiClass ); } return JVMNameUtil.getNonAnonymousClassName( psiClass ); } private static GosuBlockExpressionImpl getBlockAt( PsiElement element ) { if( element == null ) { return null; } if( element instanceof GosuBlockExpressionImpl ) { return (GosuBlockExpressionImpl)element; } return getBlockAt( element.getParent() ); } /** * Transform the fake name "scratchpad.Gosu ScratchPad" to the class name on disk, * this is also the actual bytecode class name dynamically created in the debug process's Gosu runtime. */ private static String getScratchPadName( PsiClass psiClass ) { String name = psiClass.getQualifiedName(); if( name.startsWith( GosuScratchpadFileImpl.FQN ) ) { VirtualFile file = GosuScratchpadFileImpl.getScratchpadFile( psiClass.getProject() ); String scratchPadFile = file.getCanonicalPath(); int iIndex = scratchPadFile.indexOf( '/' ); if( iIndex >= 0 ) { // remove root or drive scratchPadFile = scratchPadFile.substring( iIndex+1 ); } iIndex = scratchPadFile.lastIndexOf( '.' ); scratchPadFile = scratchPadFile.substring( 0, iIndex ); name = name.replace( GosuScratchpadFileImpl.FQN, scratchPadFile ); name = name.replace( '.', '$' ); name = name.replace( '/', '.' ); } return name; } /** * Returns the source position corresponding to the specified bytecode location. * * @param location the bytecode location. * * @return the corresponding source position. * * @throws NoDataException if the location is not in the code managed by this {@code PositionManager} */ public SourcePosition getSourcePosition( @Nullable final Location location ) throws NoDataException { if( location == null ) { throw new NoDataException(); } PsiFile psiFile = getPsiFileByLocation( getDebugProcess().getProject(), location ); if( psiFile == null || !GosuFileTypes.isTopLevelGosuFile( psiFile ) ) { throw new NoDataException(); } int lineNumber = calcLineIndex( location ); if( lineNumber < 0 ) { throw new NoDataException(); } return SourcePosition.createFromLine( psiFile, lineNumber ); } private int calcLineIndex( @Nullable Location location ) { LOG.assertTrue( _debugProcess != null ); if( location == null ) { return -1; } try { return location.lineNumber() - 1; } catch( InternalError e ) { return -1; } } public static List<String> getGosuFileExtensions() { return ImmutableList.copyOf( GosuCodeFileType.INSTANCE.getExtensions() ); } @Nullable private PsiFile getPsiFileByLocation( @NotNull final Project project, @Nullable final Location location ) { if( location == null ) { return null; } final ReferenceType refType = location.declaringType(); if( refType == null ) { return null; } final String originalQName = refType.name().replace( '/', '.' ); int dollar = originalQName.indexOf( '$' ); String qName = dollar >= 0 ? originalQName.substring( 0, dollar ) : originalQName; if( qName.endsWith( GosuScratchpadFileImpl.GOSU_SCRATCHPAD_NAME ) ) { return GosuScratchpadFileImpl.instance( project ); } IExecutionEnvironment execEnv = TypeSystem.getExecutionEnvironment( PluginLoaderUtil.getFrom( project ) ); TypeSystem.pushModule( execEnv.getGlobalModule() ); try { PsiClass cls = PsiTypeResolver.resolveType( qName, new ProjectPsiElement( project ) ); if( cls != null ) { return cls.getContainingFile(); } } catch( ProcessCanceledException e ) { return null; } catch( IndexNotReadyException e ) { return null; } finally { TypeSystem.popModule( execEnv.getGlobalModule() ); } DirectoryIndex directoryIndex = DirectoryIndex.getInstance( project ); int dotIndex = qName.lastIndexOf( "." ); String packageName = dotIndex > 0 ? qName.substring( 0, dotIndex ) : ""; Query<VirtualFile> query = directoryIndex.getDirectoriesByPackageName( packageName, true ); final String fileNameWithoutExtension = dotIndex > 0 ? qName.substring( dotIndex + 1 ) : qName; final List<String> extensions = getGosuFileExtensions(); final Ref<PsiFile> result = new Ref<>(); query.forEach( new Processor<VirtualFile>() { public boolean process( @NotNull VirtualFile vDir ) { for( final String extension : extensions ) { VirtualFile vFile = vDir.findChild( fileNameWithoutExtension + "." + extension ); if( vFile != null ) { PsiFile psiFile = PsiManager.getInstance( project ).findFile( vFile ); if( psiFile instanceof IGosuFileBase ) { result.set( psiFile ); return false; } } } return true; } } ); PsiFile res = result.get(); if( res != null ) { return res; } if( StringUtil.isEmpty( packageName ) ) { final ProjectFileIndex fileIndex = ProjectRootManager.getInstance( project ).getFileIndex(); for( final String extension : extensions ) { for( final PsiFile file : FilenameIndex.getFilesByName( project, qName + "." + extension, GlobalSearchScope.projectScope( project ) ) ) { final VirtualFile vFile = file.getVirtualFile(); if( file instanceof IGosuFile && vFile != null && !fileIndex.isInSource( vFile ) ) { for( PsiClass aClass : ((IGosuFile)file).getClasses() ) { if( qName.equals( aClass.getQualifiedName() ) ) { return file; } } } } } } //## todo: something like this for embedded scripts // for( ScriptPositionManagerHelper helper : ScriptPositionManagerHelper.EP_NAME.getExtensions() ) // { // if( helper.isAppropriateRuntimeName( qName ) ) // { // PsiFile file = helper.getExtraScriptIfNotFound( refType, qName, project ); // if( file != null ) // { // return file; // } // } // } return null; } @NotNull public List<ReferenceType> getAllClasses( final SourcePosition classPosition ) throws NoDataException { final Trinity<String, Boolean, PsiClass> trinity = calcClassName( classPosition ); if( trinity == null ) { return Collections.emptyList(); } final String className = trinity.getFirst(); final boolean isNonAnonymousClass = trinity.getSecond(); final PsiClass classAtPosition = trinity.getThird(); if( isNonAnonymousClass ) { return _debugProcess.getVirtualMachineProxy().classesByName( className ); } // the name is a parent class for a local or anonymous class final List<ReferenceType> outers = _debugProcess.getVirtualMachineProxy().classesByName( className ); final List<ReferenceType> result = new ArrayList<>( outers.size() ); for( ReferenceType outer : outers ) { final ReferenceType nested = findNested( outer, classAtPosition, classPosition ); if( nested != null ) { result.add( nested ); } } return result; } @Nullable private static Trinity<String, Boolean, PsiClass> calcClassName( final SourcePosition classPosition ) { return ApplicationManager.getApplication().runReadAction( new NullableComputable<Trinity<String, Boolean, PsiClass>>() { public Trinity<String, Boolean, PsiClass> compute() { final PsiClass psiClass = JVMNameUtil.getClassAt( classPosition ); if( psiClass == null ) { return null; } if( PsiUtil.isLocalOrAnonymousClass( psiClass ) || getBlockAt( classPosition.getElementAt() ) != null) { final PsiClass parentNonLocal = TopLevelParentClassProvider.getTopLevelParentClass( psiClass ); if( parentNonLocal == null ) { LOG.error( "Local or anonymous class has no non-local parent" ); return null; } final String parentClassName = getClassName( parentNonLocal ); if( parentClassName == null ) { LOG.error( "The name of a parent of a local (anonymous) class is null" ); return null; } return new Trinity<>( parentClassName, Boolean.FALSE, psiClass ); } final String className = getClassName( psiClass ); return className != null ? new Trinity<>( className, Boolean.TRUE, psiClass ) : null; } } ); } @Nullable private ReferenceType findNested( final ReferenceType fromClass, final PsiClass classToFind, SourcePosition classPosition ) { final VirtualMachineProxyImpl vmProxy = (VirtualMachineProxyImpl)_debugProcess.getVirtualMachineProxy(); if( fromClass.isPrepared() ) { final List<ReferenceType> nestedTypes = vmProxy.nestedTypes( fromClass ); try { final int lineNumber = classPosition.getLine() + 1; for( ReferenceType nested : nestedTypes ) { final ReferenceType found = findNested( nested, classToFind, classPosition ); if( found != null ) { // check if enclosing class also has executable code at the same line, and if yes, prefer enclosing class return fromClass.locationsOfLine( lineNumber ).isEmpty() ? found : fromClass; } } if( fromClass.locationsOfLine( lineNumber ).size() > 0 ) { return fromClass; } int rangeBegin = Integer.MAX_VALUE; int rangeEnd = Integer.MIN_VALUE; for( Location location : fromClass.allLineLocations() ) { final int locationLine = location.lineNumber() - 1; rangeBegin = Math.min( rangeBegin, locationLine ); rangeEnd = Math.max( rangeEnd, locationLine ); } if( classPosition.getLine() >= rangeBegin && classPosition.getLine() <= rangeEnd ) { // choose the second line to make sure that only this class' code exists on the line chosen // Otherwise the line (depending on the offset in it) can contain code that belongs to different classes // and JVMNameUtil.getClassAt(candidatePosition) will return the wrong class. // Example of such line: // list.add(new Runnable(){...... // First offsets belong to parent class, and offsets inside te substring "new Runnable(){" belong to anonymous runnable. final int finalRangeBegin = rangeBegin; final int finalRangeEnd = rangeEnd; return ApplicationManager.getApplication().runReadAction( new NullableComputable<ReferenceType>() { public ReferenceType compute() { final int line = Math.min( finalRangeBegin + 1, finalRangeEnd ); final SourcePosition candidatePosition = SourcePosition.createFromLine( classToFind.getContainingFile(), line ); return classToFind.equals( JVMNameUtil.getClassAt( candidatePosition ) ) ? fromClass : null; } } ); } } catch( AbsentInformationException ignored ) { } } return null; } }