From 329766f00b0b86172b88b04c5633b5db8f28b530 Mon Sep 17 00:00:00 2001 From: scott Date: Mon, 31 Jul 2023 21:26:27 -0400 Subject: [PATCH] https://github.com/manifold-systems/manifold/issues/467 - support error/warning highlights in fragments Other changes: - use smart pointers for psifile references in cache - make open files reparse when dbconfig file changes (see todo) - other minor fixes --- build.gradle | 6 +- gradle.properties | 4 +- src/main/java/manifold/ij/core/ManModule.java | 37 +++++- .../java/manifold/ij/core/ManProject.java | 10 +- ...eListener.java => ReparseFileTrigger.java} | 37 ++++-- .../ManPsiLiteralExpressionImpl.java | 10 +- .../extensions/ManStringFragmentInjector.java | 56 ++++++++- .../ij/extensions/ManifoldPsiClass.java | 53 +++++---- .../extensions/ManifoldPsiClassAnnotator.java | 110 ++++++++++++++---- .../ij/extensions/ManifoldPsiClassCache.java | 52 ++++++++- .../MaybeSmartPsiElementPointer.java | 54 +++++++++ .../ij/extensions/PsiFileFragment.java | 6 +- 12 files changed, 353 insertions(+), 82 deletions(-) rename src/main/java/manifold/ij/core/{BuildPropertiesFilePersistenceListener.java => ReparseFileTrigger.java} (66%) create mode 100644 src/main/java/manifold/ij/extensions/MaybeSmartPsiElementPointer.java diff --git a/build.gradle b/build.gradle index fe9ed5de..da9e6616 100644 --- a/build.gradle +++ b/build.gradle @@ -94,6 +94,10 @@ dependencies { manifoldAll group: 'systems.manifold', name: 'manifold-all', version: manifoldVersion manifoldEp group: 'systems.manifold', name: 'manifold-ext-producer-sample', version: manifoldVersion + // this is for manifold-sql where use of hikari brings in slf4j + // if we don't add the nop here, hikari's exception logging looks like unhandled exceptions that IJ reports as manifold errors + //implementation 'org.slf4j:slf4j-api:2.07' + implementation project('jps-plugin') } @@ -122,7 +126,7 @@ runIde { minHeapSize = '1g' maxHeapSize = '4g' // uncomment to override the ide that is run -// ideDir = new File("C:\\Program Files\\JetBrains\\IntelliJ IDEA Community Edition 2023.1.1") +// ideDir = new File("C:\\Program Files\\JetBrains\\IntelliJ IDEA 2023.1.2") } patchPluginXml { diff --git a/gradle.properties b/gradle.properties index de5dc857..716c1127 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ # # -version=2023.1.10 -manifoldVersion=2023.1.10-SNAPSHOT +version=2023.1.11 +manifoldVersion=2023.1.11-SNAPSHOT org.gradle.jvmargs=-Dfile.encoding=UTF-8 defaultIjVersion=LATEST-EAP-SNAPSHOT diff --git a/src/main/java/manifold/ij/core/ManModule.java b/src/main/java/manifold/ij/core/ManModule.java index ec995f53..6980a0d9 100644 --- a/src/main/java/manifold/ij/core/ManModule.java +++ b/src/main/java/manifold/ij/core/ManModule.java @@ -54,6 +54,7 @@ import manifold.internal.host.SimpleModule; import manifold.strings.StringLiteralTemplateProcessor; import manifold.util.NecessaryEvilUtil; +import manifold.util.ReflectUtil; import manifold.util.concurrent.LocklessLazyVar; import org.jetbrains.annotations.NotNull; import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions; @@ -325,8 +326,40 @@ private void initializeModuleClassLoader() URL[] urls = classpath.stream().map( dir -> dir.toURI().toURL() ).toArray( URL[]::new ); - // note this classloader is used exclusively for finding a loading type manifold services - _typeManifoldClassLoader = new URLClassLoader( urls, getClass().getClassLoader() ); + // note this classloader is used exclusively for finding and loading type manifold services + _typeManifoldClassLoader = + new URLClassLoader( urls, getClass().getClassLoader() ) + { + /** + * Total hack to avoid Jar-hell with IJ's PathClassLoader, a parent loader in the chain. For example, if a project + * uses manifold-sql with H2, those jars must load in this URLClassLoader so manifold-sql can do its thing. However, + * since IJ apparently uses part of H2 internally, its PathClassLoader, being a parent loader of this loader, will + * load H2 classes, which are probably not from the same version of H2, etc. Therefore, overloading loadClass() + * here for non-manifold classes ensures that ij's loader won't interfere with dependencies of manifold classes. + */ + @Override + protected Class loadClass( String name, boolean resolve ) throws ClassNotFoundException + { + ClassLoader parent = null; + try + { + if( !name.startsWith( "manifold." ) ) // if( name.startsWith( "org.h2." ) ) + { + // jump over PathClassLoader to the great-grandfather since none of these classes should have a dependency on IJ classes + parent = (ClassLoader)ReflectUtil.field( this, "parent" ).get(); + ReflectUtil.field( this, "parent" ).set( ClassLoader.getPlatformClassLoader() ); + } + return super.loadClass( name, resolve ); + } + finally + { + if( !name.startsWith( "manifold." ) ) + { + ReflectUtil.field( this, "parent" ).set( parent ); + } + } + } + }; } @Override diff --git a/src/main/java/manifold/ij/core/ManProject.java b/src/main/java/manifold/ij/core/ManProject.java index 9ed1b29e..261e7ebc 100644 --- a/src/main/java/manifold/ij/core/ManProject.java +++ b/src/main/java/manifold/ij/core/ManProject.java @@ -30,6 +30,7 @@ import com.intellij.openapi.compiler.CompilerPaths; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtil; @@ -44,7 +45,6 @@ import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.libraries.Library; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiElement; @@ -650,7 +650,7 @@ private int findJdkVersion() private void addBuildPropertiesFilePersistenceListener() { _permanentProjectConnection.subscribe( AppTopics.FILE_DOCUMENT_SYNC, - new BuildPropertiesFilePersistenceListener( _ijProject ) ); + new ReparseFileTrigger( _ijProject ) ); } private void addModuleClasspathListener() @@ -667,11 +667,11 @@ private void addModuleClasspathListener() */ private void addFileOpenedListener() { - _permanentProjectConnection.subscribe( FileEditorManagerListener.FILE_EDITOR_MANAGER, - new FileEditorManagerListener() + _permanentProjectConnection.subscribe( FileOpenedSyncListener.TOPIC, + new FileOpenedSyncListener() { @Override - public void fileOpenedSync( @NotNull FileEditorManager source, @NotNull VirtualFile file, @NotNull Pair editors ) + public void fileOpenedSync( @NotNull FileEditorManager source, @NotNull VirtualFile file, @NotNull List editors ) { if( !isManifoldInUse() ) { diff --git a/src/main/java/manifold/ij/core/BuildPropertiesFilePersistenceListener.java b/src/main/java/manifold/ij/core/ReparseFileTrigger.java similarity index 66% rename from src/main/java/manifold/ij/core/BuildPropertiesFilePersistenceListener.java rename to src/main/java/manifold/ij/core/ReparseFileTrigger.java index 6499dd99..2ca9a017 100644 --- a/src/main/java/manifold/ij/core/BuildPropertiesFilePersistenceListener.java +++ b/src/main/java/manifold/ij/core/ReparseFileTrigger.java @@ -32,13 +32,18 @@ import org.jetbrains.annotations.NotNull; /** - * For preprocessor. When a build.properties files is saved, open Java files reparse. + * For preprocessor and dbconfig. When a build.properties or *.dbconfig file is saved, open Java files reparse. + * + * todo: make the conditions for reparsing, currently dbconfig and build.properties, pluggable. + * todo: even better: if manifold IModel had concept of model dependencies, we could determine exactly the set of files + * that need to reparse instead of reparsing everything. We could also reparse non-open files, because we need to if the + * file was opened then closed because IJ does not retokenize when reopening a file. */ -class BuildPropertiesFilePersistenceListener implements FileDocumentManagerListener +class ReparseFileTrigger implements FileDocumentManagerListener { - private Project _ijProject; + private final Project _ijProject; - BuildPropertiesFilePersistenceListener( Project ijProject ) + ReparseFileTrigger( Project ijProject ) { _ijProject = ijProject; } @@ -46,30 +51,30 @@ class BuildPropertiesFilePersistenceListener implements FileDocumentManagerListe @Override public void beforeDocumentSaving( @NotNull Document document ) { - reparseFiles( document ); + maybeReparseOpenJavaFiles( document ); } @Override public void fileContentReloaded( @NotNull VirtualFile file, @NotNull Document document ) { - reparseFiles( document ); + maybeReparseOpenJavaFiles( document ); } @Override public void fileContentLoaded( @NotNull VirtualFile file, @NotNull Document document ) { - reparseFiles( document ); + maybeReparseOpenJavaFiles( document ); } - private void reparseFiles( @NotNull Document document ) + private void maybeReparseOpenJavaFiles( @NotNull Document document ) { - if( isBuildProperties( document ) ) + if( shouldReparse( document ) ) { ReparseUtil.reparseOpenJavaFiles( _ijProject ); } } - private boolean isBuildProperties( Document document ) + private boolean shouldReparse( Document document ) { VirtualFile vfile = FileDocumentManager.getInstance().getFile( document ); if( vfile == null || vfile instanceof LightVirtualFile ) @@ -85,7 +90,17 @@ private boolean isBuildProperties( Document document ) PsiFile psiFile = PsiDocumentManager.getInstance( _ijProject ).getPsiFile( document ); if( psiFile != null ) { - return Definitions.BUILD_PROPERTIES.equalsIgnoreCase( vfile.getName() ); + String fileExt = vfile.getExtension(); + if( fileExt != null && fileExt.equalsIgnoreCase( "dbconfig" ) ) + { + // DbConfig file changed + return true; + } + else if( Definitions.BUILD_PROPERTIES.equalsIgnoreCase( vfile.getName() ) ) + { + // Build.properties file changed + return true; + } } } catch( Throwable ignore ) diff --git a/src/main/java/manifold/ij/extensions/ManPsiLiteralExpressionImpl.java b/src/main/java/manifold/ij/extensions/ManPsiLiteralExpressionImpl.java index 33b7ad09..c5f1c2f3 100644 --- a/src/main/java/manifold/ij/extensions/ManPsiLiteralExpressionImpl.java +++ b/src/main/java/manifold/ij/extensions/ManPsiLiteralExpressionImpl.java @@ -97,6 +97,10 @@ public PsiType getType() if( value != null ) { String fqn = getFragmentValueFqn( value ); + if( fqn == null ) + { + return null; + } return JavaPsiFacade.getInstance( getProject() ).getParserFacade().createTypeFromText( fqn, this ); } } @@ -127,7 +131,7 @@ private String getFragmentClassName( IFileFragment fragment ) } else { - SmartPsiElementPointer container = (SmartPsiElementPointer)fragment.getContainer(); + MaybeSmartPsiElementPointer container = (MaybeSmartPsiElementPointer)fragment.getContainer(); if( container != null ) { PsiElement elem = container.getElement(); @@ -146,7 +150,9 @@ private String getFragmentValueFqn( PsiAnnotationMemberValue value ) String fqn = null; if( value instanceof PsiReferenceExpression ) { - fqn = ((PsiField)((PsiReferenceExpression)value).resolve()).computeConstantValue().toString(); + PsiField resolvedField = (PsiField)((PsiReferenceExpression)value).resolve(); + Object resolvedRef = resolvedField == null ? null : resolvedField.computeConstantValue(); + fqn = resolvedRef == null ? null : resolvedRef.toString(); } else if( value instanceof PsiLiteralExpression ) { diff --git a/src/main/java/manifold/ij/extensions/ManStringFragmentInjector.java b/src/main/java/manifold/ij/extensions/ManStringFragmentInjector.java index a5a99aa3..916defa1 100644 --- a/src/main/java/manifold/ij/extensions/ManStringFragmentInjector.java +++ b/src/main/java/manifold/ij/extensions/ManStringFragmentInjector.java @@ -22,14 +22,19 @@ import com.intellij.lang.Language; import com.intellij.lang.LanguageUtil; import com.intellij.lang.java.JavaLanguage; +import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile; import com.intellij.openapi.util.TextRange; import com.intellij.psi.InjectedLanguagePlaces; import com.intellij.psi.JavaTokenType; import com.intellij.psi.LanguageInjector; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl; +import com.intellij.psi.injection.ReferenceInjector; import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.LightVirtualFile; import manifold.ij.core.ManProject; import manifold.internal.javac.FragmentProcessor; import manifold.internal.javac.HostKind; @@ -68,7 +73,56 @@ public void getLanguagesToInject( @NotNull PsiLanguageInjectionHost host, @NotNu private Language getLanguageFromExt( String ext ) { - return LanguageUtil.getFileTypeLanguage( FileTypeManager.getInstance().getFileTypeByExtension( ext ) ); + ReferenceInjector injector = ReferenceInjector.findById( ext ); + if( injector != null ) + { + return injector.toLanguage(); + } + + Language fileTypeLanguage = LanguageUtil.getFileTypeLanguage( FileTypeManager.getInstance().getFileTypeByExtension( ext ) ); + if( fileTypeLanguage != null ) + { + return fileTypeLanguage; + } + + LightVirtualFile virtualFileNamedAsLanguageId = new LightVirtualFile( ext ); + LightVirtualFile virtualFileWithLanguageIdAsExtension = new LightVirtualFile( "textmate." + ext ); + for( FileType fileType : FileTypeManager.getInstance().getRegisteredFileTypes() ) + { + if( fileType instanceof LanguageFileType && + fileType instanceof FileTypeIdentifiableByVirtualFile fileTypeByVf ) + { + if( fileTypeByVf.isMyFileType( virtualFileNamedAsLanguageId ) || + fileTypeByVf.isMyFileType( virtualFileWithLanguageIdAsExtension ) ) + { + return ((LanguageFileType)fileType).getLanguage(); + } + } + } + + return null; + } + + public static Language getLanguageByString(@NotNull String languageId) { + ReferenceInjector injector = ReferenceInjector.findById(languageId); + if (injector != null) return injector.toLanguage(); + FileTypeManager fileTypeManager = FileTypeManager.getInstance(); + FileType fileType = fileTypeManager.getFileTypeByExtension(languageId); + if (fileType instanceof LanguageFileType) { + return ((LanguageFileType)fileType).getLanguage(); + } + + LightVirtualFile virtualFileNamedAsLanguageId = new LightVirtualFile(languageId); + LightVirtualFile virtualFileWithLanguageIdAsExtension = new LightVirtualFile("textmate." + languageId); + for (FileType registeredFileType : fileTypeManager.getRegisteredFileTypes()) { + if (registeredFileType instanceof FileTypeIdentifiableByVirtualFile && + registeredFileType instanceof LanguageFileType && + (((FileTypeIdentifiableByVirtualFile)registeredFileType).isMyFileType(virtualFileNamedAsLanguageId) || + ((FileTypeIdentifiableByVirtualFile)registeredFileType).isMyFileType(virtualFileWithLanguageIdAsExtension))) { + return ((LanguageFileType)registeredFileType).getLanguage(); + } + } + return null; } private HostKind getStringKind( @NotNull PsiLanguageInjectionHost host ) diff --git a/src/main/java/manifold/ij/extensions/ManifoldPsiClass.java b/src/main/java/manifold/ij/extensions/ManifoldPsiClass.java index 3ba54a6a..0ada1b25 100644 --- a/src/main/java/manifold/ij/extensions/ManifoldPsiClass.java +++ b/src/main/java/manifold/ij/extensions/ManifoldPsiClass.java @@ -31,18 +31,18 @@ import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; +import com.intellij.psi.*; import com.intellij.psi.impl.PsiManagerImpl; import com.intellij.psi.impl.light.LightClass; import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import com.intellij.psi.util.ClassUtil; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.swing.Icon; import javax.tools.DiagnosticCollector; +import javax.tools.JavaFileObject; + import manifold.api.fs.IFile; import manifold.api.fs.IFileFragment; import manifold.ij.core.ManModule; @@ -52,24 +52,19 @@ public class ManifoldPsiClass extends LightClass { public static final Key KEY_MANIFOLD_PSI_CLASS = new Key<>( "Facade" ); - private List _files; - private List _ifiles; - private String _fqn; - private ManModule _manModule; - private DiagnosticCollector _issues; + private final List> _files; + private final List _ifiles; + private final String _fqn; + private final ManModule _manModule; + private final DiagnosticCollector _issues; - public ManifoldPsiClass( PsiClass delegate, ManModule module, List files, String fqn, DiagnosticCollector issues ) + public ManifoldPsiClass( PsiClass delegate, ManModule module, List files, String fqn, DiagnosticCollector issues ) { super( delegate ); - initialize( delegate, module, files, fqn, issues ); - } - - public void initialize( PsiClass delegate, ManModule manModule, List files, String fqn, DiagnosticCollector issues ) - { _ifiles = files; _fqn = fqn; - _manModule = manModule; + _manModule = module; _issues = issues; PsiManager manager = PsiManagerImpl.getInstance( delegate.getProject() ); _files = new ArrayList<>( _ifiles.size() ); @@ -79,8 +74,11 @@ public void initialize( PsiClass delegate, ManModule manModule, List file if( vfile != null && vfile.isValid() ) { PsiFile file = manager.findFile( vfile ); - _files.add( file ); - file.putUserData( ModuleUtil.KEY_MODULE, manModule.getIjModule() ); + if( file != null ) + { + _files.add( SmartPointerManager.createPointer( file ) ); + file.putUserData( ModuleUtil.KEY_MODULE, module.getIjModule() ); + } } } if( _files.isEmpty ) @@ -88,7 +86,7 @@ public void initialize( PsiClass delegate, ManModule manModule, List file PsiFile containingFile = delegate.getContainingFile(); if( containingFile != null ) { - _files.add( containingFile ); + _files.add( SmartPointerManager.createPointer( containingFile ) ); } } if( getContainingClass() == null ) @@ -112,8 +110,8 @@ private void reassignFragmentContainer() { if( file instanceof IFileFragment ) { - Object container = ((IFileFragment)file).getContainer(); - if( !(container instanceof PsiFileFragment) ) + MaybeSmartPsiElementPointer container = (MaybeSmartPsiElementPointer)((IFileFragment)file).getContainer(); + if( container == null || !(container.getElement() instanceof PsiFileFragment) ) { continue; } @@ -129,7 +127,8 @@ private void reassignFragmentContainer() } if( elem != null ) { - ((IFileFragment)file).setContainer( SmartPointerManagerImpl.createPointer( elem ) ); + ((IFileFragment)file).setContainer( + new MaybeSmartPsiElementPointer( SmartPointerManagerImpl.createPointer( elem ) ) ); } } } @@ -186,7 +185,7 @@ public boolean isWritable() public List getRawFiles() { - return _files; + return _files.stream().map( f -> f.getElement() ).collect( Collectors.toList() ); } public List getFiles() @@ -220,13 +219,13 @@ public boolean canNavigateToSource() public String getText() { //todo: handle multiple files somehow - return _files.isEmpty() ? "" : _files.get( 0 ).getText(); + return _files.isEmpty() ? "" : getRawFiles().get( 0 ).getText(); } @Override public PsiElement getNavigationElement() { - return _files.isEmpty() ? null : _files.get( 0 ).getNavigationElement(); + return _files.isEmpty() ? null : getRawFiles().get( 0 ).getNavigationElement(); } // @Override @@ -238,7 +237,7 @@ public PsiElement getNavigationElement() @Override public Icon getIcon( int flags ) { - return _files.isEmpty() ? null : _files.get( 0 ).getIcon( flags ); + return _files.isEmpty() ? null : getRawFiles().get( 0 ).getIcon( flags ); } @Override @@ -252,7 +251,7 @@ public Module getModule() return _manModule.getIjModule(); } - public DiagnosticCollector getIssues() + public DiagnosticCollector getIssues() { return _issues; } diff --git a/src/main/java/manifold/ij/extensions/ManifoldPsiClassAnnotator.java b/src/main/java/manifold/ij/extensions/ManifoldPsiClassAnnotator.java index 3fba330b..7086e78a 100644 --- a/src/main/java/manifold/ij/extensions/ManifoldPsiClassAnnotator.java +++ b/src/main/java/manifold/ij/extensions/ManifoldPsiClassAnnotator.java @@ -19,10 +19,7 @@ package manifold.ij.extensions; -import com.intellij.lang.annotation.Annotation; -import com.intellij.lang.annotation.AnnotationHolder; -import com.intellij.lang.annotation.Annotator; -import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.lang.annotation.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.impl.DocumentMarkupModel; @@ -36,8 +33,11 @@ import java.util.Set; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; +import javax.tools.JavaFileObject; import com.intellij.util.SmartList; +import manifold.api.fs.IFileFragment; +import manifold.ij.core.ManModule; import manifold.ij.core.ManProject; import org.jetbrains.annotations.NotNull; @@ -61,12 +61,65 @@ public void annotate( @NotNull PsiElement element, @NotNull AnnotationHolder hol return; } - Set psiClasses = ResourceToManifoldUtil.findPsiClass( element.getContainingFile() ); - Set reported = new HashSet<>(); - psiClasses.forEach( psiClass -> annotate( psiClass, element, holder, reported ) ); + if( element instanceof PsiFileFragment ) + { + highlightFragment( (PsiFileFragment)element, holder ); + } + else + { + Set psiClasses = ResourceToManifoldUtil.findPsiClass( element.getContainingFile() ); + Set> reported = new HashSet<>(); + psiClasses.forEach( psiClass -> annotate( psiClass, element, holder, reported ) ); + } + } + + private void highlightFragment( @NotNull PsiFileFragment fragmentFile, @NotNull AnnotationHolder holder ) + { + ManModule manMod = ManProject.getModule( fragmentFile ); + IFileFragment fragment = fragmentFile.getFragment(); + if( manMod != null && fragment != null ) + { + String[] fqns = manMod.getTypesForFile( fragment ); + for( String fqn: fqns ) + { + PsiClass psiClass = ManifoldPsiClassCache.getPsiClass( manMod, fqn ); + if( psiClass instanceof ManifoldPsiClass && ((ManifoldPsiClass)psiClass).isFragment() ) + { + DiagnosticCollector issues = ((ManifoldPsiClass)psiClass).getIssues(); + if( issues == null ) + { + break; + } + +// PsiFile containingFile = fragmentFile.getContainingFile(); + Set> reported = new HashSet<>(); + for( Diagnostic issue : issues.getDiagnostics() ) + { + if( !reported.contains( issue ) ) + { + boolean created; +// if( containingFile instanceof PsiPlainTextFile ) +// { + created = createIssueOnTextRange( holder, issue ); +// } +// else +// { +// created = createIssueOnElement( holder, issue, element ); +// } + if( created ) + { + reported.add( (Diagnostic)issue ); + } + } + } + break; + } + } + } } - private void annotate( PsiClass psiClass, PsiElement element, AnnotationHolder holder, Set reported ) + + private void annotate( PsiClass psiClass, PsiElement element, AnnotationHolder holder, Set> reported ) { PsiFile containingFile = element.getContainingFile(); if( PsiErrorClassUtil.isErrorClass( psiClass ) && element instanceof PsiFileSystemItem ) @@ -89,26 +142,26 @@ private void annotate( PsiClass psiClass, PsiElement element, AnnotationHolder h { // IJ doesn't clear previously added annotations, so we do this bullshit - ApplicationManager.getApplication().invokeLater( () -> { - Project project = containingFile.getProject(); - Document document = PsiDocumentManager.getInstance( project ).getDocument( containingFile ); - if( document != null ) - { - MarkupModel markupModel = DocumentMarkupModel.forDocument( document, project, false ); - markupModel.removeAllHighlighters(); - } - } ); + if( ApplicationManager.getApplication().isDispatchThread() ) + { + removeAllHighlighters( containingFile ); + } + else + { + ApplicationManager.getApplication().invokeLater( () -> { + removeAllHighlighters( containingFile ); + } ); + } } - DiagnosticCollector issues = ((ManifoldPsiClass)psiClass).getIssues(); + DiagnosticCollector issues = ((ManifoldPsiClass)psiClass).getIssues(); if( issues == null ) { return; } - for( Object obj : issues.getDiagnostics() ) + for( Diagnostic issue : issues.getDiagnostics() ) { - Diagnostic issue = (Diagnostic)obj; if( !reported.contains( issue ) ) { boolean created; @@ -128,7 +181,18 @@ private void annotate( PsiClass psiClass, PsiElement element, AnnotationHolder h } } - private boolean createIssueOnTextRange( AnnotationHolder holder, Diagnostic issue ) + private static void removeAllHighlighters( PsiFile containingFile ) + { + Project project = containingFile.getProject(); + Document document = PsiDocumentManager.getInstance( project ).getDocument( containingFile ); + if( document != null ) + { + MarkupModel markupModel = DocumentMarkupModel.forDocument( document, project, false ); + markupModel.removeAllHighlighters(); + } + } + + private boolean createIssueOnTextRange( AnnotationHolder holder, Diagnostic issue ) { TextRange range = new TextRange( (int)issue.getStartPosition(), (int)issue.getEndPosition() ); String message = makeMessage( issue ); @@ -159,7 +223,7 @@ private boolean createIssueOnTextRange( AnnotationHolder holder, Diagnostic issu return true; } - private boolean createIssueOnElement( AnnotationHolder holder, Diagnostic issue, PsiElement element ) + private boolean createIssueOnElement( AnnotationHolder holder, Diagnostic issue, PsiElement element ) { if( element.getTextOffset() > issue.getStartPosition() || element.getTextOffset() + element.getTextLength() <= issue.getStartPosition() ) @@ -221,7 +285,7 @@ private boolean hasAnnotation( SmartList holder, PsiElement deepestE } @NotNull - private String makeMessage( Diagnostic issue ) + private String makeMessage( Diagnostic issue ) { return issue.getMessage( Locale.getDefault() ); } diff --git a/src/main/java/manifold/ij/extensions/ManifoldPsiClassCache.java b/src/main/java/manifold/ij/extensions/ManifoldPsiClassCache.java index d3fb9b1d..dfde9fd1 100644 --- a/src/main/java/manifold/ij/extensions/ManifoldPsiClassCache.java +++ b/src/main/java/manifold/ij/extensions/ManifoldPsiClassCache.java @@ -29,6 +29,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.PsiFileFactoryImpl; @@ -36,6 +37,7 @@ import com.intellij.psi.impl.PsiModificationTrackerImpl; import com.intellij.psi.search.GlobalSearchScope; +import java.io.File; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import javax.tools.DiagnosticCollector; @@ -47,19 +49,18 @@ import manifold.api.host.Dependency; import manifold.api.host.RefreshRequest; import manifold.api.type.ITypeManifold; -import manifold.ij.android.BuildVariantSymbols; import manifold.ij.core.ManModule; import manifold.ij.core.ManProject; -import manifold.ij.util.ReparseUtil; +import manifold.ij.fs.IjFile; import manifold.internal.javac.FragmentProcessor; import manifold.api.util.cache.FqnCache; import manifold.api.util.cache.FqnCacheNode; import manifold.api.util.cache.IllegalTypeNameException; -import manifold.preprocessor.definitions.ServiceDefinitions; import manifold.util.concurrent.ConcurrentHashSet; import manifold.util.concurrent.ConcurrentWeakHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.model.java.JavaResourceRootType; import static manifold.api.type.ContributorKind.*; @@ -129,7 +130,7 @@ private boolean isStaleFileFragment( IFile file ) { if( file instanceof IFileFragment ) { - SmartPsiElementPointer container = (SmartPsiElementPointer)((IFileFragment)file).getContainer(); + MaybeSmartPsiElementPointer container = (MaybeSmartPsiElementPointer)((IFileFragment)file).getContainer(); if( container == null ) { return true; @@ -292,6 +293,7 @@ private FqnCacheNode createPrimaryType( ManModule module, Stri String result = ""; DiagnosticCollector issues = new DiagnosticCollector<>(); String topLevelFqn = null; + boolean isTestContent = false; for( ITypeManifold tm : tms ) { // MUST start with top-level class, otherwise the classes enclosing an inner class will have null userData @@ -306,10 +308,12 @@ private FqnCacheNode createPrimaryType( ManModule module, Stri } found = tm; result = tm.contribute( null, topLevelFqn, false, result, issues ); + + isTestContent = isTestContent( module, topLevelFqn, isTestContent, tm ); } ManModule actualModule = (ManModule)found.getModule(); - PsiClass delegate = createPsiClass( actualModule, topLevelFqn, result ); + PsiClass delegate = createPsiClass( actualModule, topLevelFqn, isTestContent, result ); cacheAll( delegate, actualModule, found, issues ); } @@ -322,6 +326,16 @@ private FqnCacheNode createPrimaryType( ManModule module, Stri return fqnPsiCache.getNode( fqn ); } + private static boolean isTestContent( ManModule module, String topLevelFqn, boolean isTestContent, ITypeManifold tm ) + { + return isTestContent || tm.findFilesForType( topLevelFqn ).stream() + .map( file -> file instanceof IFileFragment ? file.getPhysicalFile() : file ) + .map( file -> ((IjFile)file).getVirtualFile() ) + .filter( vfile -> vfile != null ) + .anyMatch( virtualFile -> ModuleRootManager.getInstance( + module.getIjModule() ).getFileIndex().isInTestSourceContent( virtualFile ) ); + } + public static String findTopLevelFqn( ITypeManifold tm, String fqn ) { if( tm.isTopLevelType( fqn ) ) @@ -370,7 +384,7 @@ private void listenToChanges( ManProject project ) PsiManager.getInstance( ijProject ).addPsiTreeChangeListener( new PsiTreeChangeHandler(), ijProject ); } - private PsiClass createPsiClass( ManModule module, String fqn, String source ) + private PsiClass createPsiClass( ManModule module, String fqn, boolean isTestContent, String source ) { //System.out.println( "NEW: " + fqn + " MODULE: " + module.getName() ); ////new Exception().fillInStackTrace().printStackTrace(); @@ -382,9 +396,35 @@ private PsiClass createPsiClass( ManModule module, String fqn, String source ) { return PsiErrorClassUtil.create( module.getIjProject(), new RuntimeException( "Invalid class: " + fqn ) ); } + ensurePackageExistsOnDisk( module, classes, isTestContent ); return classes[0]; } + /** + * Ensure the directory structure mirroring the package exists on disk. Otherwise, code completion and such do not work. + * This is probably a bug in IJ, but I'm working around it. + */ + private static void ensurePackageExistsOnDisk( ManModule module, PsiClass[] classes, boolean isTestContent ) + { + if( classes[0] == null ) + { + return; + } + + JavaResourceRootType rootType = isTestContent ? JavaResourceRootType.TEST_RESOURCE : JavaResourceRootType.RESOURCE; + List sourceRoots = ModuleRootManager.getInstance( module.getIjModule() ).getSourceRoots( rootType ); + for( VirtualFile sourceRoot : sourceRoots ) + { + if( sourceRoot.isDirectory() ) + { + String rootPath = sourceRoot.getPath(); + String packageName = ((PsiClassOwner)classes[0].getContainingFile()).getPackageName(); + packageName = packageName.replace( '.', '/' ); + new File( rootPath, packageName ).mkdirs(); + } + } + } + private PsiJavaFile createDummyJavaFile( String type, ManModule module, PsiManager manager, final String text ) { // note, calling version of PsiFileFactory#createFileFromText that takes file content of any size, which is vital diff --git a/src/main/java/manifold/ij/extensions/MaybeSmartPsiElementPointer.java b/src/main/java/manifold/ij/extensions/MaybeSmartPsiElementPointer.java new file mode 100644 index 00000000..4cf89d2e --- /dev/null +++ b/src/main/java/manifold/ij/extensions/MaybeSmartPsiElementPointer.java @@ -0,0 +1,54 @@ +/* + * + * * Copyright (c) 2022 - Manifold Systems LLC + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + * + */ + +package manifold.ij.extensions; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.SmartPsiElementPointer; +import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; + +public class MaybeSmartPsiElementPointer +{ + private Object _element; + + public MaybeSmartPsiElementPointer( PsiElement element ) + { + _element = element; + } + public MaybeSmartPsiElementPointer( SmartPsiElementPointer element ) + { + _element = element; + } + + public PsiElement getElement() + { + if( _element instanceof SmartPsiElementPointer ) + { + return ((SmartPsiElementPointer)_element).getElement(); + } + + if( _element != null && ((PsiElement)_element).isValid() ) + { + _element = SmartPointerManagerImpl.createPointer( (PsiElement)_element ); + return getElement(); + } + + return (PsiElement)_element; + } +} diff --git a/src/main/java/manifold/ij/extensions/PsiFileFragment.java b/src/main/java/manifold/ij/extensions/PsiFileFragment.java index 93e3ae7b..b3e2a686 100644 --- a/src/main/java/manifold/ij/extensions/PsiFileFragment.java +++ b/src/main/java/manifold/ij/extensions/PsiFileFragment.java @@ -21,6 +21,7 @@ import com.intellij.lang.ASTNode; import com.intellij.lang.PsiBuilderFactory; +import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiJavaFile; import java.util.Set; @@ -78,8 +79,9 @@ default void handleFragments() FragmentProcessor.Fragment f = fragmentProcessor.parseFragment( 0, hostText, style ); if( f != null ) { + Project project = containingFile.getProject(); FileFragmentImpl fragment = new FileFragmentImpl( f.getScope(), f.getName(), f.getExt(), style, - FileUtil.toIFile( containingFile.getProject(), FileUtil.toVirtualFile( containingFile ) ), + FileUtil.toIFile( project, FileUtil.toVirtualFile( containingFile ) ), f.getOffset(), f.getContent().length(), f.getContent() ); // must add a callback for the offset because this element's parent chain is not connected yet @@ -99,7 +101,7 @@ default void handleFragments() } setFragment( fragment ); - fragment.setContainer( this ); + fragment.setContainer( new MaybeSmartPsiElementPointer( this ) ); deletedFragment( ManProject.manProjectFrom( containingFile.getProject() ), fragment ); createdFragment( ManProject.manProjectFrom( containingFile.getProject() ), fragment ); ReparseUtil.rerunAnnotators( containingFile );