From 38cf35ab871f09c8e2d16eae39be1b97d13d4ead Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 5 Sep 2024 09:23:18 +0200 Subject: [PATCH] Roots (and directories) containing multi-file launcher sources should respond to ClassPath queries. --- .../AttributeBasedSingleFileOptions.java | 20 ++++ .../java/file/launcher/SharedRootData.java | 59 +++++++--- .../file/launcher/SingleSourceFileUtil.java | 9 ++ .../queries/MultiSourceRootProvider.java | 101 ++++++++++++++---- .../SingleFileOptionsQueryImplementation.java | 5 +- .../queries/Bundle_registerroots.properties} | 1 - .../queries/MultiSourceRootProviderTest.java | 61 ++++++++++- .../SingleFileOptionsQueryImpl.java | 8 +- .../netbeans/modules/java/Bundle.properties | 2 + .../org/netbeans/modules/java/JavaNode.java | 29 +++++ 10 files changed, 257 insertions(+), 38 deletions(-) rename java/{java.lsp.server/nbcode/branding/modules/org-netbeans-modules-java-file-launcher.jar/org/netbeans/modules/java/file/launcher/queries/Bundle.properties => java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/Bundle_registerroots.properties} (99%) diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/AttributeBasedSingleFileOptions.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/AttributeBasedSingleFileOptions.java index 65e6cd433a03..3312081635cf 100644 --- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/AttributeBasedSingleFileOptions.java +++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/AttributeBasedSingleFileOptions.java @@ -29,12 +29,15 @@ import org.openide.filesystems.FileUtil; import org.openide.util.ChangeSupport; import org.openide.util.Lookup; +import org.openide.util.RequestProcessor; import org.openide.util.WeakListeners; import org.openide.util.lookup.ServiceProvider; @ServiceProvider(service=SingleFileOptionsQueryImplementation.class) public class AttributeBasedSingleFileOptions implements SingleFileOptionsQueryImplementation { + private static final RequestProcessor WORKER = new RequestProcessor(AttributeBasedSingleFileOptions.class.getName(), 1, false, false); + @Override public Result optionsFor(FileObject file) { if (!SingleSourceFileUtil.isSupportedFile(file)) { @@ -66,6 +69,15 @@ private static final class ResultImpl implements Result { private final FileChangeListener attributeChanges = new FileChangeAdapter() { @Override public void fileAttributeChanged(FileAttributeEvent fe) { + if (root != null && registerRoot()) { + //propagation of flags from files to the root is usually only + //started when the root is indexed. And when the registerRoot + //flag is flipped to true on a file in a non-indexed root, + //there's no other mechanism to propagate the flag to the root. + //So, when the flag is set to true on a file, force the propagation + //of the flags for the given root: + WORKER.post(() -> SharedRootData.ensureRootRegistered(root)); + } cs.fireChange(); } }; @@ -100,6 +112,14 @@ public URI getWorkDirectory() { return root != null ? root.toURI() : source.getParent().toURI(); } + @Override + public boolean registerRoot() { + Object value = source != null ? source.getAttribute(SingleSourceFileUtil.FILE_REGISTER_ROOT) + : root != null ? root.getAttribute(SingleSourceFileUtil.FILE_REGISTER_ROOT) + : null; + return SingleSourceFileUtil.isTrue(value); + } + @Override public void addChangeListener(ChangeListener listener) { cs.addChangeListener(listener); diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SharedRootData.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SharedRootData.java index 98530378dd60..7f965bee11e2 100644 --- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SharedRootData.java +++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SharedRootData.java @@ -21,10 +21,12 @@ import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.modules.java.file.launcher.api.SourceLauncher; import org.openide.filesystems.FileAttributeEvent; @@ -45,7 +47,13 @@ public class SharedRootData { private static final Map root2Data = new HashMap<>(); public static synchronized void ensureRootRegistered(FileObject root) { - root2Data.computeIfAbsent(root, r -> new SharedRootData(r)); + if (root2Data.get(root) != null) { + return ; + } + + SharedRootData data = root2Data.computeIfAbsent(root, r -> new SharedRootData(r)); + + data.init(); } public static synchronized @CheckForNull SharedRootData getDataForRoot(FileObject root) { @@ -53,18 +61,18 @@ public static synchronized void ensureRootRegistered(FileObject root) { } private final FileObject root; - private final Map options = new TreeMap<>(); + private final Map properties = new TreeMap<>(); private final FileChangeListener listener = new FileChangeAdapter() { @Override public void fileAttributeChanged(FileAttributeEvent fe) { - Map newProperties = new HashMap<>(); + Map newProperties = new HashMap<>(); addPropertiesFor(fe.getFile(), newProperties); setNewProperties(newProperties); } @Override public void fileDeleted(FileEvent fe) { - Map newProperties = new HashMap<>(); + Map newProperties = new HashMap<>(); newProperties.put(FileUtil.getRelativePath(root, fe.getFile()), null); setNewProperties(newProperties); @@ -73,9 +81,12 @@ public void fileDeleted(FileEvent fe) { private SharedRootData(FileObject root) { this.root = root; + } + + private void init() { root.addRecursiveListener(listener); Enumeration todo = root.getChildren(true); - Map newProperties = new HashMap<>(); + Map newProperties = new HashMap<>(); while (todo.hasMoreElements()) { FileObject current = todo.nextElement(); addPropertiesFor(current, newProperties); @@ -83,25 +94,32 @@ private SharedRootData(FileObject root) { setNewProperties(newProperties); } - private void addPropertiesFor(FileObject file, Map newProperties) { + private void addPropertiesFor(FileObject file, Map newProperties) { if (file.isData() && "text/x-java".equals(file.getMIMEType())) { - newProperties.put(FileUtil.getRelativePath(root, file), (String) file.getAttribute(SingleSourceFileUtil.FILE_VM_OPTIONS)); + newProperties.put(FileUtil.getRelativePath(root, file), new FileProperties((String) file.getAttribute(SingleSourceFileUtil.FILE_VM_OPTIONS), + SingleSourceFileUtil.isTrue(file.getAttribute(SingleSourceFileUtil.FILE_REGISTER_ROOT)))); } } - private synchronized void setNewProperties(Map newProperties) { + private synchronized void setNewProperties(Map newProperties) { if (newProperties.isEmpty()) { return ; } for (String key : newProperties.keySet()) { - String value = newProperties.get(key); - if (value == null) { - options.remove(key); + FileProperties fileProperties = newProperties.get(key); + if (fileProperties == null) { + properties.remove(key); } else { - options.put(key, value); + properties.put(key, fileProperties); } } - String joinedCommandLine = SourceLauncher.joinCommandLines(options.values()); + + List vmOptions = properties.values() + .stream() + .map(p -> p.vmOptions) + .filter(p -> p != null) + .collect(Collectors.toList()); + String joinedCommandLine = SourceLauncher.joinCommandLines(vmOptions); try { if (!joinedCommandLine.equals(root.getAttribute(SingleSourceFileUtil.FILE_VM_OPTIONS))) { root.setAttribute(SingleSourceFileUtil.FILE_VM_OPTIONS, joinedCommandLine); @@ -109,6 +127,21 @@ private synchronized void setNewProperties(Map newProperties) { } catch (IOException ex) { LOG.log(Level.INFO, "Failed to set " + SingleSourceFileUtil.FILE_VM_OPTIONS + " for " + root.getPath(), ex); } + Boolean registerRoot = properties.values() + .stream() + .map(p -> p.registerRoot) + .filter(r -> r) + .findAny() + .isPresent(); + try { + if (!registerRoot.equals(root.getAttribute(SingleSourceFileUtil.FILE_REGISTER_ROOT))) { + root.setAttribute(SingleSourceFileUtil.FILE_REGISTER_ROOT, registerRoot); + } + } catch (IOException ex) { + LOG.log(Level.INFO, "Failed to set " + SingleSourceFileUtil.FILE_REGISTER_ROOT + " for " + root.getPath(), ex); + } } + record FileProperties(String vmOptions, boolean registerRoot) {} + } diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SingleSourceFileUtil.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SingleSourceFileUtil.java index 0152f195164d..d8fb70bb990a 100644 --- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SingleSourceFileUtil.java +++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/SingleSourceFileUtil.java @@ -64,6 +64,7 @@ public static int findJavaVersion() throws NumberFormatException { public static final String FILE_ARGUMENTS = "single_file_run_arguments"; //NOI18N public static final String FILE_JDK = "single_file_run_jdk"; //NOI18N public static final String FILE_VM_OPTIONS = "single_file_vm_options"; //NOI18N + public static final String FILE_REGISTER_ROOT = "register_root"; //NOI18N public static FileObject getJavaFileWithoutProjectFromLookup(Lookup lookup) { for (DataObject dObj : lookup.lookupAll(DataObject.class)) { @@ -153,6 +154,10 @@ public static List parseLine(String line, URI workingDirectory) { return PARSER.doParse(line, workingDirectory); } + public static boolean isTrue(Object value) { + return value instanceof Boolean b && b; + } + private static final LineParser PARSER = new LineParser(); private static class LineParser extends CompilerOptionsQueryImplementation.Result { @@ -217,6 +222,10 @@ public URI getWorkDirectory() { return delegate.getWorkDirectory(); } + public boolean registerRoot() { + return delegate.registerRoot(); + } + @Override public void addChangeListener(ChangeListener listener) { cs.addChangeListener(listener); diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java index 0bfc6bfbc23d..ff0d0ea144b5 100644 --- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java +++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java @@ -22,6 +22,7 @@ import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; +import java.lang.ref.Reference; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -90,20 +91,41 @@ public class MultiSourceRootProvider implements ClassPathProvider { //TODO: the cache will probably be never cleared, as the ClassPath/value refers to the key(?) private Map file2SourceCP = new WeakHashMap<>(); private Map root2SourceCP = new WeakHashMap<>(); + private Map root2RegistrationRefresh = new WeakHashMap<>(); + private final Set registeredRoots = Collections.newSetFromMap(new WeakHashMap<>()); private Map file2AllPath = new WeakHashMap<>(); private Map file2ClassPath = new WeakHashMap<>(); private Map file2ModulePath = new WeakHashMap<>(); - static boolean isSupportedFile(FileObject file) { - return SingleSourceFileUtil.isSingleSourceFile(file) - // MultiSourceRootProvider assumes it can convert FileObject to - // java.io.File, so filter here - && Objects.equals("file", file.toURI().getScheme()); + boolean isSupportedFile(FileObject file) { + // MultiSourceRootProvider assumes it can convert FileObject to + // java.io.File, so filter here + if (!Objects.equals("file", file.toURI().getScheme())) { + return false; + } + + if (SingleSourceFileUtil.isSingleSourceFile(file)) { + return true; + } + + Set registeredRootsCopy; + + synchronized (registeredRoots) { + registeredRootsCopy = registeredRoots; + } + + for (FileObject existingRoot : registeredRootsCopy) { + if (file.equals(existingRoot) || FileUtil.isParentOf(existingRoot, file)) { + return true; + } + } + + return false; } @Override public ClassPath findClassPath(FileObject file, String type) { - if (! isSupportedFile(file)) { + if (!isSupportedFile(file)) { return null; } switch (type) { @@ -148,13 +170,17 @@ private ClassPath getSourcePath(FileObject file) { } } - return root2SourceCP.computeIfAbsent(root, r -> { - ClassPath srcCP = ClassPathSupport.createClassPath(Arrays.asList(new RootPathResourceImplementation(r))); - if (registerRoot(r)) { - GlobalPathRegistry.getDefault().register(ClassPath.SOURCE, new ClassPath[] {srcCP}); - } - return srcCP; + ClassPath srcCP = root2SourceCP.computeIfAbsent(root, r -> { + return ClassPathSupport.createClassPath(Arrays.asList(new RootPathResourceImplementation(r))); }); + + ParsedFileOptions options = SingleSourceFileUtil.getOptionsFor(root); + + if (options != null) { + WORKER.post(root2RegistrationRefresh.computeIfAbsent(root, r -> new RegistrationRefresh(srcCP, options, r))); + } + + return srcCP; } catch (IOException ex) { LOG.log(Level.FINE, "Failed to read sourcefile " + file, ex); } @@ -269,13 +295,6 @@ private ClassPath attributeBasedPath(FileObject file, Map } } - @Messages({ - "SETTING_AutoRegisterAsRoot=false" - }) - private static boolean registerRoot(FileObject root) { - return "true".equals(Bundle.SETTING_AutoRegisterAsRoot()); - } - private static final class AttributeBasedClassPathImplementation extends FileChangeAdapter implements ChangeListener, ClassPathImplementation { private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private final Task updateDelegatesTask = WORKER.create(this::doUpdateDelegates); @@ -355,7 +374,10 @@ private void doUpdateDelegates() { for (File expanded : expandedPaths) { URL u = FileUtil.urlForArchiveOrDir(expanded); if (u == null) { - throw new IllegalArgumentException("Path entry looks to be invalid: " + piece); // NOI18N + LOG.log(Level.INFO, + "While parsing command line option '{0}' with parameter '{1}', path entry looks to be invalid: '{2}'", + new Object[] {currentOption, parsed.get(i + 1), piece}); + continue; } newURLs.add(u); newDelegates.add(ClassPathSupport.createResource(u)); @@ -468,4 +490,43 @@ public void removePropertyChangeListener(PropertyChangeListener listener) { } } + + private class RegistrationRefresh implements ChangeListener, Runnable { + private final ClassPath srcCP; + private final ParsedFileOptions options; + private final FileObject root; + + public RegistrationRefresh(ClassPath srcCP, + ParsedFileOptions options, + FileObject root) { + this.srcCP = srcCP; + this.options = options; + this.root = root; + options.addChangeListener(this); + } + + @Override + public void run() { + GlobalPathRegistry registry = GlobalPathRegistry.getDefault(); + if (options.registerRoot()) { + synchronized (registeredRoots) { + registeredRoots.add(root); + } + registry.register(ClassPath.SOURCE, new ClassPath[] {srcCP}); + } else { + synchronized (registeredRoots) { + registeredRoots.remove(root); + } + if (registry.getPaths(ClassPath.SOURCE).contains(srcCP)) { + registry.unregister(ClassPath.SOURCE, new ClassPath[] {srcCP}); + } + } + } + + @Override + public void stateChanged(ChangeEvent e) { + WORKER.post(this); + } + } + } diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/spi/SingleFileOptionsQueryImplementation.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/spi/SingleFileOptionsQueryImplementation.java index 216706463c3b..cd73cd5e028b 100644 --- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/spi/SingleFileOptionsQueryImplementation.java +++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/spi/SingleFileOptionsQueryImplementation.java @@ -28,10 +28,13 @@ public interface SingleFileOptionsQueryImplementation { public Result optionsFor(FileObject file); public interface Result { - public String getOptions(); + public @NonNull String getOptions(); public default @NonNull URI getWorkDirectory() { throw new UnsupportedOperationException(); } + public default boolean registerRoot() { + return false; + } public void addChangeListener(ChangeListener l); public void removeChangeListener(ChangeListener l); } diff --git a/java/java.lsp.server/nbcode/branding/modules/org-netbeans-modules-java-file-launcher.jar/org/netbeans/modules/java/file/launcher/queries/Bundle.properties b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/Bundle_registerroots.properties similarity index 99% rename from java/java.lsp.server/nbcode/branding/modules/org-netbeans-modules-java-file-launcher.jar/org/netbeans/modules/java/file/launcher/queries/Bundle.properties rename to java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/Bundle_registerroots.properties index 684c2d3a5fff..d13c21373f31 100644 --- a/java/java.lsp.server/nbcode/branding/modules/org-netbeans-modules-java-file-launcher.jar/org/netbeans/modules/java/file/launcher/queries/Bundle.properties +++ b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/Bundle_registerroots.properties @@ -14,5 +14,4 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - SETTING_AutoRegisterAsRoot=true diff --git a/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java index ff7245d3af48..c89798daad2c 100644 --- a/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java +++ b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java @@ -19,11 +19,14 @@ package org.netbeans.modules.java.file.launcher.queries; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.Writer; import java.net.URI; import java.nio.file.Files; import java.util.Arrays; import java.util.HashSet; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.swing.event.ChangeListener; @@ -31,12 +34,15 @@ import org.netbeans.api.java.classpath.JavaClassPathConstants; import org.netbeans.api.java.source.TestUtilities; import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.file.launcher.SingleSourceFileUtil; import org.netbeans.modules.java.file.launcher.spi.SingleFileOptionsQueryImplementation; import org.netbeans.modules.java.file.launcher.spi.SingleFileOptionsQueryImplementation.Result; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.ChangeSupport; +import org.openide.util.Exceptions; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; @@ -254,9 +260,10 @@ public void testMultiSourceRootProviderOnlySupportedForLocalFiles() throws IOExc supportedFile = Files.createTempFile("dummy", ".java").toFile(); FileObject realFileSource = FileUtil.createData(supportedFile); FileObject inMemorySource = FileUtil.createMemoryFileSystem().getRoot().createData("Ahoj.java"); + MultiSourceRootProvider provider = new MultiSourceRootProvider(); - assertFalse(MultiSourceRootProvider.isSupportedFile(inMemorySource)); - assertTrue(MultiSourceRootProvider.isSupportedFile(realFileSource)); + assertFalse(provider.isSupportedFile(inMemorySource)); + assertTrue(provider.isSupportedFile(realFileSource)); } finally { if(supportedFile != null && supportedFile.exists()) { supportedFile.delete(); @@ -264,6 +271,45 @@ public void testMultiSourceRootProviderOnlySupportedForLocalFiles() throws IOExc } } + public void testMultiSourceRootProviderRespondsForKnownFolders() throws IOException { + File wd = getWorkDir(); + File testDir = new File(wd, "test"); + File packDir = new File(testDir, "pack"); + File testFile = new File(packDir, "Test.java"); + + packDir.mkdirs(); + + try (Writer w = Files.newBufferedWriter(testFile.toPath())) { + w.write("package pack;"); + } + + testResult.setOptions(""); + testResult.setWorkDirectory(testDir.toURI()); + + MultiSourceRootProvider provider = new MultiSourceRootProvider(); + + //before recongizing testDir is a multi-source file root: + assertNull(provider.findClassPath(FileUtil.toFileObject(wd), ClassPath.SOURCE)); + assertNull(provider.findClassPath(FileUtil.toFileObject(testDir), ClassPath.SOURCE)); + assertNull(provider.findClassPath(FileUtil.toFileObject(packDir), ClassPath.SOURCE)); + + //recognize the source file as a multi-source file: + ClassPath cp = provider.findClassPath(FileUtil.toFileObject(testFile), ClassPath.SOURCE); + + assertNotNull(cp); + + //check properties: + assertNull(provider.findClassPath(FileUtil.toFileObject(wd), ClassPath.SOURCE)); + assertNull(provider.findClassPath(FileUtil.toFileObject(testDir), ClassPath.SOURCE)); + assertNull(provider.findClassPath(FileUtil.toFileObject(packDir), ClassPath.SOURCE)); + + testResult.setRegisterRoot(true); + + assertNull(provider.findClassPath(FileUtil.toFileObject(wd), ClassPath.SOURCE)); + assertSame(cp, provider.findClassPath(FileUtil.toFileObject(testDir), ClassPath.SOURCE)); + assertSame(cp, provider.findClassPath(FileUtil.toFileObject(packDir), ClassPath.SOURCE)); + } + @Override protected void setUp() throws Exception { super.setUp(); @@ -294,6 +340,7 @@ private static class TestResultImpl implements Result { private final ChangeSupport cs = new ChangeSupport(this); private final AtomicReference options = new AtomicReference<>(); private final AtomicReference workdir = new AtomicReference<>(); + private final AtomicBoolean registerRoot = new AtomicBoolean(); public TestResultImpl() { } @@ -318,6 +365,16 @@ public void setWorkDirectory(URI workdir) { cs.fireChange(); } + @Override + public boolean registerRoot() { + return registerRoot.get(); + } + + public void setRegisterRoot(boolean registerRoot) { + this.registerRoot.set(registerRoot); + cs.fireChange(); + } + @Override public void addChangeListener(ChangeListener l) { cs.addChangeListener(l); diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/singlesourcefile/SingleFileOptionsQueryImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/singlesourcefile/SingleFileOptionsQueryImpl.java index da4898786f11..912078b072d6 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/singlesourcefile/SingleFileOptionsQueryImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/singlesourcefile/SingleFileOptionsQueryImpl.java @@ -118,7 +118,8 @@ public ResultImpl(Map workspaceFolders2Results, @Override public String getOptions() { - return workspaceSettings.getOptions(); + String options = workspaceSettings.getOptions(); + return options != null ? options : ""; } @Override @@ -129,6 +130,11 @@ public URI getWorkDirectory() { return workDir.toURI(); } + @Override + public boolean registerRoot() { + return true; + } + @Override public void addChangeListener(ChangeListener l) { cs.addChangeListener(l); diff --git a/java/java.source/src/org/netbeans/modules/java/Bundle.properties b/java/java.source/src/org/netbeans/modules/java/Bundle.properties index 74949b6a2eb2..1d3268d9067e 100644 --- a/java/java.source/src/org/netbeans/modules/java/Bundle.properties +++ b/java/java.source/src/org/netbeans/modules/java/Bundle.properties @@ -37,6 +37,8 @@ PROP_JavaNode_singlefile_arguments=Program Arguments HINT_JavaNode_singlefile_arguments=Arguments passed to the main method while running the file. PROP_JavaNode_singlefile_options=VM Options HINT_JavaNode_singlefile_options=VM Options to be considered while running the file. +PROP_JavaNode_singlefile_registerRoot=Enable Source File Launcher Indexing +HINT_JavaNode_singlefile_registerRoot=The root corresponding to this file should have source file launcher indexing enabled PROP_JavaNode_classfile_version=Classfile Version HINT_JavaNode_classfile_version=The Java API and Language Level of this class file PROP_JavaNode_compile_classpath=Compile Classpath diff --git a/java/java.source/src/org/netbeans/modules/java/JavaNode.java b/java/java.source/src/org/netbeans/modules/java/JavaNode.java index 6dec93234e29..60bfeced4598 100644 --- a/java/java.source/src/org/netbeans/modules/java/JavaNode.java +++ b/java/java.source/src/org/netbeans/modules/java/JavaNode.java @@ -111,6 +111,7 @@ public final class JavaNode extends DataNode implements ChangeListener { private static final String FILE_ARGUMENTS = "single_file_run_arguments"; //NOI18N private static final String FILE_JDK = "single_file_run_jdk"; //NOI18N private static final String FILE_VM_OPTIONS = "single_file_vm_options"; //NOI18N + private static final String FILE_REGISTER_ROOT = "register_root"; //NOI18N private static final Map IMAGE_CACHE = new ConcurrentHashMap<>(); private static final boolean ALWAYS_PREFFER_COMPUTED_ICON = Boolean.getBoolean("JavaNode.prefferComputedIcon"); //NOI18N @@ -242,6 +243,7 @@ protected final Sheet createSheet () { ss.setName("runFileArguments"); // NOI18N ss.setDisplayName(getMessage(JavaNode.class, "LBL_JavaNode_without_project_run")); // NOI18N ss.setShortDescription("Run the file's source code."); + ss.put(new JavaFileBooleanAttributeProperty(dObj, FILE_REGISTER_ROOT, "registerRoot", "singlefile_registerRoot")); // NOI18N ss.put(new RunFileJDKProperty(dObj)); ss.put(new JavaFileAttributeProperty(dObj, FILE_ARGUMENTS, "runFileArguments", "singlefile_arguments")); // NOI18N ss.put(new JavaFileAttributeProperty(dObj, FILE_VM_OPTIONS, "runFileVMOptions", "singlefile_options")); // NOI18N @@ -466,6 +468,33 @@ public Component getCustomEditor() { } + // editable file attribute + private static final class JavaFileBooleanAttributeProperty extends PropertySupport.ReadWrite { + + private final String attribute; + private final DataObject dObj; + + public JavaFileBooleanAttributeProperty(DataObject dObj, String attribute, String name, String msgKeyPart) { + super(name, Boolean.class, getMessage(JavaNode.class, "PROP_JavaNode_" + msgKeyPart), getMessage(JavaNode.class, "HINT_JavaNode_" + msgKeyPart)); // NOI18N + this.dObj = dObj; + this.attribute = attribute; + } + + @Override + public Boolean getValue() { + return dObj.getPrimaryFile().getAttribute(attribute) instanceof Boolean val ? val : false; + } + + @Override + public void setValue(Boolean o) { + try { + dObj.getPrimaryFile().setAttribute(attribute, o); + } catch (IOException ex) { + LOG.log(Level.WARNING, "Java File does not exist : {0}", dObj.getPrimaryFile().getName()); //NOI18N + } + } + } + // editable file attribute private static final class JavaFileAttributeProperty extends PropertySupport.ReadWrite {