You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@netbeans.apache.org by "mbien (via GitHub)" <gi...@apache.org> on 2026/10/21 02:52:07 UTC

[I] Directory groups → Generic Project ("dirproject") [netbeans]

mbien opened a new issue, #9738:
URL: https://github.com/apache/netbeans/issues/9738

   this was generated during implementation of https://github.com/apache/netbeans/pull/9737. Removed about 30% of the text. Might be still a little too verbose / redundant but the gist is in it.
   
   # Directory groups → Generic Project ("dirproject")
   
   Status: **implemented** (build + unit tests green); the document
   describes the initial draft implementation.
   
   Idea: when a directory becomes a group in the Projects view, promote
   the directory itself into a `GenericProject` — so it appears as a real
   project node (logical view = all files, plus the full stock project menu).
   The group becomes the stock `SubprojectsGroup` for the promoted project:
   nested projects are still listed as project nodes, but discovery is
   delegated to a `ProjectContainerProvider` in the project's lookup (see
   below) instead of a hard-coded scan in `projectui`.
   
   ## What `DirectoryGroup` is today
   
   `ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java`:
   
   - Created by the "New Group" dialog: `NewGroupPanel.java:221`
     (`DirectoryGroup.create(name, f)`).
   - Persisted in NbPreferences as `name` / `kind` (`KIND = "directory"`) /
     `path` (directory URL) under the group node; re-instantiated by the
     `KIND` switch in `Group.load`.
   - `findProjects` scans the directory for *projects* (with a
     `SharabilityQuery` guard) — the group node lists nested project groups.
     **Files are never shown.**
   - A group is per-user view state: everything lives in preferences, the
     file tree is never touched.
   
   ## The relationship reversal (at a glance)
   
   Today the **view knows the discovery policy** — `DirectoryGroup` walks
   the directory itself and hard-codes *how* projects are found. After the
   change the **project knows** — the stock group simply *asks the
   project's lookup*, and the policy lives in the project type. Push
   becomes pull:
   
   ```
   BEFORE — the view reaches into the directory (push)
   
     projectui (view)                        filesystem
     ┌──────────────────────────────────┐
     │ DirectoryGroup                   │
     │   findProjects()                 │── directory scan ──▶  X/
     │     processFolderChildren()      │                      ├── a/          (project)
     │       walk folders,              │                      ├── b/
     │       findProject(),             │                      │    └── c/      (project)
     │       sharability guard,         │                      └── ...
     │       convertor filter           │
     └──────────────────────────────────┘
         ↑ discovery policy hard-coded here
           (view module knows how to find
            projects in a directory)
           projects inside are passive —
           they know nothing about being listed
   
   
   AFTER — the view asks the project (pull)
   
     projectui (view)                project type (generic.project)
     ┌────────────────────────────┐
     │ SubprojectsGroup (stock,   │
     │ unchanged)                 │
     │   findProjects()           │
     │     visitSubprojects(p)    │
     │       p.getLookup()        │◄─────────────────────────┐
     │        .lookup(            │  "which projects do       │
     │  ProjectContainerProvider) │   you contain?"           │
     │                            │                           │
     └────────────────────────────┘                           │
                                                              │
                                          ┌───────────────────┴───────────────┐
                                          │ GpProjectContainerProvider        │
                                          │ (in p's lookup,                  │
                                          │  base-lookup inner class         │
                                          │   walks X/ + sharability +       │
                                          │   convertor filters              │
                                          └──────────────────────────────────┘
                                                ↑ discovery policy lives here
                                                  (in the project type)
   ```
   
   The "New Group" flow through the promotion seam:
   
   ```
     "New Group" → directory X
           │
           ▼
     NewGroupPanel (projectui)
           │  Lookup.lookup(DirectoryProjectProvider.class)
           ▼
     GpDirectoryProjectProvider (generic.project)
           │
           ├── findProject(X) != null? ─── yes ──▶ return the existing project
           ├── X NOT_SHARABLE? ─────────── yes ──▶ null (declined)
           └── set MARKER_ATTRIBUTE, clearNonProjectCache(), findProject(X)
           │
           ▼
     OpenProjectList.open({p}, …, asynchronously=true)
     +  SubprojectsGroup.create(name, p)
   ```
   
   The open uses the stock new-project-wizard pattern
   `OpenProjectList.open(projects, /*prime=*/false,
   /*openSubprojects=*/false, /*asynchronously=*/true,
   /*mainProject=*/null)` — `OpenProjects.requestOpen` does not exist in
   this tree — and runs off the EDT: `GroupsMenu` posts `create()` to a
   request processor. A `null` result (decline), an absent provider, or an
   `IOException` is a hard error at the seam (`IllegalStateException` /
   `AssertionError`) — fail loud rather than fall back to a passive
   directory group.
   
   |                    | before                       | after                            |
   |--------------------|------------------------------|----------------------------------|
   | group class        | `DirectoryGroup`             | `SubprojectsGroup` (stock)       |
   | discovery policy   | hard-coded in `projectui`    | in the project's lookup          |
   | view → projects    | push: view walks the dir     | pull: view asks the project      |
   | nested projects    | listed by blind scan         | listed by the project itself     |
   | new project types  | invisible to the mechanism   | plug in their own provider       |
   
   ## Design
   
   ### Semantics of the promotion
   
   - **Private (attribute-based) promotion, always for the group flow:**
     set `GenericProject.MARKER_ATTRIBUTE` on the directory's `FileObject`
     (stored in userdir's `attributes.xml`: per-user, file tree untouched,
     reversible). Rationale: a group *is* per-user state — a per-user UI
     action must not mutate the file tree or make the folder a project for
     every user on the machine.
   - **Canonical instance:** set attribute → `ProjectManager.findProject(dir)`.
     Never `new GenericProject` directly (two identities for one directory
     would break `FileOwnerQuery`, open lists, ...).
     **Gotcha (found by the tests):** `NbProjectManager` *negatively
     caches* directories that were not projects — the idempotency
     `findProject(dir)` call at the top of the promotion stores a
     `NO_SUCH_PROJECT` entry that the second `findProject(dir)` would
     honor. The stock wizard flow hits the same wall and solves it with
     `ProjectManager.clearNonProjectCache()` ("useful after creating
     project metadata in a folder") — the promotion does the same between
     setting the attribute and the final lookup.
   - **Sharability guard:** do not promote a `NOT_SHARABLE` directory —
     `createProject` declines with `null` and the seam reports the failure
     (there is no passive fallback group anymore). Note this is the
     *flip side* of the `SharabilityQuery` check in `findProjects` (which is
     about *reading* projects out of non-sharable dirs) — keep the two
     decisions distinct in comments.
   - **Idempotent:** if the directory already is a project (of *any* type),
     return/open it instead of promoting.
   - **Group removal does not clear the attribute** — the folder remains
     *your* project; it stays re-openable and is still deletable via the
     stock "Delete" menu entry.
   
   ### The seam: a lookup service, not a hard dependency
   
   `projectui` must not depend on `generic.project` (a common project-UI
   module depending on one specific project type is the wrong direction).
   With zero new module dependencies (both sides already depend on
   `projectuiapi`):
   
   1. **New interface in `projectuiapi`** (package e.g.
      `org.netbeans.spi.project.ui.support`):
      ```java
      public interface DirectoryProjectProvider {
          /**
           * @return a project for the directory — the existing project if
           *         the directory already is one, a newly promoted one if
           *         this provider can make it so — or null if it cannot
           *         handle it.
           */
          Project createProject(FileObject dir) throws IOException;
      }
      ```
   2. **`generic.project`** registers the implementation via
      `@ServiceProvider` (no new dependency):
      `findProject(dir) != null` → return it; else sharability check, set
      `MARKER_ATTRIBUTE`, return `findProject(dir)`. This is the same
      primitive as `GenericProjectAPI.markAsLocalProject(dir)` from the
      API-module design — the two conversations unify on it.
   3. **`projectui`** changes the call site (`NewGroupPanel:221`): ask the
      `Lookup` for `DirectoryProjectProvider`; non-null result → open the
      project and create a stock **`SubprojectsGroup.create(name, p)`**
      (KIND = "subprojects" — already in `Group.load`'s switch); `null`
      (decline), absent provider, or `IOException` → hard error
      (`IllegalStateException` / `AssertionError`) — fail loud rather than
      fall back to a passive directory group.
   
   No dependency cycle: `generic.project → projectuiapi` and
   `projectui → projectuiapi` both already exist; `projectui` never imports
   our classes.
   
   ### Why there is no `shared` boolean anywhere near the seam
   
   - The group flow is essentially per-user, so it only ever wants the
     private variant; a `shared` flag on `createProject` would be dead
     weight in the SPI until some hypothetical "New Group" checkbox asks
     for it.
   - The shared/private axis is expressed **at the API level as two named
     methods**, not a boolean:
     ```java
     GenericProjectAPI.markAsLocalProject(dir);    // attribute, per-user
     GenericProjectAPI.markAsGenericProject(dir);  // marker file, shared
     ```
   - The one place a creation-time *choice* makes sense is the explicit
     "Convert to Generic Project" command on a never-project folder — the
     wizard's job, which already offers it. That command calls the API
     directly and never goes through the SPI.
   
   ### Private by default, share on demand
   
   The upgrade path is already implemented and cheap ("two clicks"):
   `GenericProject.setMarkerState(dir, true)` is a symmetric toggle that
   creates the marker *before* clearing the redundant attribute (and sets
   the attribute before deleting the marker on the way down), so the
   directory is never in an unrecognized state. Recognition is the OR of
   both mechanisms, so the project's identity never changes — same
   `GenericProject` instance, no reload, no IDE restart.
   
   - Default to the *reversible* side: private→shared is lossless (nothing
     in the tree changed).
   - Follow-up (separate, trivial): a "Make Shared / Make Private" menu
     entry — one line over `setMarkerState`.
   
   ### Nested projects: the stock `ProjectContainerProvider`
   
   `ProjectContainerProvider` (projectapi SPI, since 1.56 — "a less vague
   variant of `SubprojectProvider` for code that wants to access projects
   that the current project serves as container for") is exactly the seam
   to delegate discovery to the project type. And the stock discovery
   built on it **already exists** — no rewrite needed:
   `SubprojectsGroup.visitSubprojects` (the group's `findProjects`) does,
   per project:
   
   1. look up `ProjectContainerProvider` in the project's lookup;
   2. if present: add `getContainedProjects().getProjects()`; if
      `!res.isRecursive()`, re-visit the same project (see caveat below);
   3. else fall back to the semi-deprecated `SubprojectProvider` and
      visit each returned project the same way.
   
   So the promotion flow just creates a stock `SubprojectsGroup` for the
   promoted project, and all discovery runs through *our* provider.
   `SubprojectsGroup` persists the project directory URL (KIND
   "subprojects", already handled by `Group.load`), falls back to the
   project's display name (so our editable name shows), and resolves to an
   empty group if the directory ever stops being a project.
   
   **Our provider** — new `generic.project` class
   `GpProjectContainerProvider implements ProjectContainerProvider,
   SubprojectProvider`:
   
   - Registered as a **base-lookup inner class** of `GenericProject`
     (added to the project's base `Lookup` in the constructor) — *not* the
     maven `@ProjectServiceProvider` layer pattern: every other service in
     this module is a base-lookup inner class, it replaces the previous
     direct-children-only `GpSubprojectProvider` in place, and no layer
     entry is needed for a class that exists for the life of the project
     instance.
   - Implements **both** interfaces over the same set: the stock
     `OpenSubprojects` ("Open Required Projects" menu item) enables itself
     on the presence of the *old* `SubprojectProvider` only.
   - `getContainedProjects()` = a single pass over the project
     directory's folders: `getFolders(false)`, `findProject` per folder,
     skip `ProjectConvertors.isConvertorProject`, skip the children of a
     `NOT_SHARABLE` folder (stop re-checking once the folder is proven
     `SHARABLE`), exclude the project itself, `Thread.interrupted()`
     checks (callers include the open-project dialog — maven does the
     same).
   - **Returns the projects directly contained in the directory,
     reported as complete (`recursive=false`).** A single pass over the
     directory's folders: the deeper containment scan proved too slow
     for large trees, so the exposed set is what the old
     `GpSubprojectProvider` reported — projects nested deeper are not
     exposed through this provider.
     Maven already trips over this (returns `recursive=false`): a 3-level
     maven tree shows only the first module level in a stock subprojects
     group. (`OpenProjectList.open` handles `recursive=false` correctly
     via its `toHandle` queue — the no-op is specific to
     `SubprojectsGroup`.)
   - `ChangeSupport` is present but **never fires in v1** — no
     `FileChangeListener` is registered on the project directory. Rationale
     (revisited at implementation time): no stock consumer registers a
     listener today (maven's own TODO: "is someone actually currently
     listening on SubprojectProvider changes?"), and a listener on the
     project directory would pin the project instance as long as the
     listener lives. The group re-queries whenever projects are opened or
     closed; a silently added nested project may not show until the next
     refresh — acceptable for v1.
   
   **Blast radius** — every in-tree consumer of
   `ProjectContainerProvider`, checked:
   
   | consumer | effect for generic projects |
   |---|---|
   | `SubprojectsGroup` (projectui) | the intended use: the group lists nested projects |
   | `OpenProjectList.open(…, openSubprojects=true, …)` (projectui) | "Open Required Projects" / open-dialog flows with the flag auto-open the project's contained projects. Plain `open(p)` (flag false) does **not** cascade |
   | `ProjectChooserAccessory` (open dialog, projectui) | same pattern as above |
   | maven debug/execute utils | type-gated to maven, no effect |
   
   ## Resulting user experience
   
   - "New Group → directory group" for folder X → X is promoted and
     **opened as a project** (logical view = all files, full stock project
     menu: New File, Find, Rename/Move/Copy/Delete, versioning, Customize).
     The group is a stock `SubprojectsGroup` for X, so the projects
     contained in X appear as project nodes under it — the old
     `DirectoryGroup` UX, minus the hard-coded scan.
   - Contained projects are visible in two places, as in any container setup:
     as project nodes in the group, and as folders in X's file logical
     view. Optional polish: a `ProjectNodeFactory` in the generic logical
     view to render them as project nodes there too.
   
   ## Files touched
   
   | module        | change |
   |---|---|
   | `projectuiapi` | +1 interface (`DirectoryProjectProvider`, promotion only); spec version 1.124 → 1.125; `apichanges.xml` entry (and migration of its DOCTYPE from DTD 1.0 to 1.1 — the 1.0 DTD's year list stops at 2025) |
   | `projectui`    | `NewGroupPanel` call site: ask the `Lookup` for `DirectoryProjectProvider`; on promotion, open the project (asynchronously, stock wizard pattern) and create `SubprojectsGroup.create(name, p)` instead of `DirectoryGroup.create`; `DirectoryGroup` + `DirectoryGroupEditPanel` (and their `Bundle.properties` keys) **removed**; legacy `kind="directory"` nodes migrated to subprojects in `Group.load` (see *Pre-existing groups*) |
   | `generic.project` | +`promoteToProject` static, +`GpDirectoryProjectProvider` (`@ServiceProvider`), `GpSubprojectProvider` **replaced** by `GpProjectContainerProvider` (base lookup, single pass over the directory's folders), +`org.netbeans.modules.queries` and `org.netbeans.modules.projectuiapi.base` (for `ProjectConvertors` — the class lives in the `base` module, not in `projectuiapi` itself; spec 1.120, matching the built artifact) deps, 7 new unit tests (+1 rewritten) |
   
   **Note:** this deliberately breaks the "projectui / projectuiapi are
   read-only reference" rule from the generic.project work — by a small,
   well-justified amount (one interface, one call-site rework).
   
   ## Pre-existing groups
   
   `DirectoryGroup` (class, edit panel, form, bundle keys) is **removed**;
   legacy nodes are migrated on load. `Group.load`'s `KIND` switch accepts
   the legacy `kind="directory"` and runs a one-shot migration before
   returning a stock `SubprojectsGroup`:
   
   - the stored directory is resolved and passed to
     `ProjectManager.findProject`; if that finds no project, a
     `DirectoryProjectProvider` is looked up and `createProject`
     (promotion) is attempted — so a legacy group on an *already-project*
     directory is salvaged even in a distribution without
     `generic.project`;
   - the node's `kind` is rewritten to `"subprojects"` **in all cases** —
     success and failure alike — so the migration runs once; a transient
     failure (e.g. a one-time IO error) is *not* retried, the group stays
     empty until the user removes it or creates a new one on the directory;
   - any failure (unresolvable path, no provider registered, provider
     declined, IO error) degrades to an *empty* `SubprojectsGroup` —
     visible in Manage Groups (re-pointable to any project, or deletable),
     with one warning logged per group.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists


Re: [I] Directory groups → Generic Project ("dirproject") [netbeans]

Posted by "mbien (via GitHub)" <gi...@apache.org>.
mbien commented on issue #9738:
URL: https://github.com/apache/netbeans/issues/9738#issuecomment-6310871032

   @pressing-buttons well, it wouldn't solve it directly but it would be a workaround at least ;)
   
   you can test it btw. the PRs workflow runs add dev-build zips on the bottom of the summary page. In essence: you would be able to open any folder as project.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists


Re: [I] Directory groups → Generic Project ("dirproject") [netbeans]

Posted by "pressing-buttons (via GitHub)" <gi...@apache.org>.
pressing-buttons commented on issue #9738:
URL: https://github.com/apache/netbeans/issues/9738#issuecomment-6309717598

   Would this solve the issue with "resource only" sub projects in Gradle, or be usable as a workaround? 
   
   https://github.com/apache/netbeans/issues/8180


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists