refactor(fileBrowser): rewrite navigation history layer with event-driven NavStack and implement parent directory navigation#2500
Conversation
Greptile SummaryThis PR replaces the file browser's plain-array navigation history with an event-driven
Confidence Score: 4/5Safe to merge after verifying the two edge cases noted — no crashes or data-loss paths introduced. The refactor is well-contained: NavStack's event-driven design cleanly decouples state from DOM, popUntil is guarded by a has() pre-check before every call, contextMenuHandler already returns early for isOneDirUp entries, and createFileStructure argument semantics are preserved. The only concrete defects are a copy-paste error message in NavStack.get() and the .. row being silently non-functional when navStack contains only one entry. The .. insertion logic in getDir (fileBrowser.js ~line 1474) warrants a second look: the entry is unconditionally added whenever lsDir succeeds, but it only navigates correctly when the history stack has at least two entries. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User
participant FB as FileBrowserInclude
participant NS as NavStack
participant LS as localStorage
U->>FB: click folder
FB->>NS: navigate(url, name)
alt url already in stack
FB->>NS: popUntil(url)
NS-->>FB: emit pop x N
FB->>FB: remove navbar items and actionStack entries
else new url
NS-->>FB: not in stack
end
FB->>FB: await getDir(url, name)
alt not already in stack
FB->>NS: push url and name
NS-->>FB: emit push
FB->>FB: pushToNavbar
NS->>LS: saveFileBrowserState
end
FB->>FB: render dir
U->>FB: click ..
FB->>NS: get(-2)
alt prevDir exists
FB->>FB: navigate prevDir
else
FB->>FB: no-op silent
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User
participant FB as FileBrowserInclude
participant NS as NavStack
participant LS as localStorage
U->>FB: click folder
FB->>NS: navigate(url, name)
alt url already in stack
FB->>NS: popUntil(url)
NS-->>FB: emit pop x N
FB->>FB: remove navbar items and actionStack entries
else new url
NS-->>FB: not in stack
end
FB->>FB: await getDir(url, name)
alt not already in stack
FB->>NS: push url and name
NS-->>FB: emit push
FB->>FB: pushToNavbar
NS->>LS: saveFileBrowserState
end
FB->>FB: render dir
U->>FB: click ..
FB->>NS: get(-2)
alt prevDir exists
FB->>FB: navigate prevDir
else
FB->>FB: no-op silent
end
Reviews (3): Last reviewed commit: "refactor(fileBrowser): rewrite navigatio..." | Re-trigger Greptile |
| case "oneDirUp": { | ||
| const dir = navStack.get(-2); | ||
| if (!dir) break; | ||
| const { url, name } = dir; | ||
| navigate(url, name); | ||
| } |
There was a problem hiding this comment.
.. resolves to navigation-history parent, not the filesystem parent
navStack.get(-2) returns the previously-visited directory, not the actual URL-parent of the current directory. These are the same in linear navigation, but diverge in edge cases — e.g. if a future feature adds bookmarks or deep-links that push multiple levels to navStack at once (like loadStates already does). In that scenario pressing .. could land on a directory that is not an ancestor of the current one at all. The traditional expected behaviour of .. is Url.dirname(currentDir.url). Consider adding a clarifying comment or computing the real parent as a fallback.
| case "oneDirUp": { | ||
| const dir = navStack.get(-2); | ||
| if (!dir) break; | ||
| const { url, name } = dir; | ||
| navigate(url, name); | ||
| } |
There was a problem hiding this comment.
Missing
break at end of oneDirUp case
The oneDirUp block has no trailing break. While this is currently safe because it is the last case, future additions to the switch will silently fall through into the new case without any visible indication that the omission is intentional. Adding break makes the intent explicit and future-proof.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
de3a1ed to
6f52174
Compare
…iven `NavStack` and implement parent directory navigation
This commit completes an architectural refactor of Acode's file browser subsystem. It eliminates legacy technical debt centered around manual array manipulations, uncoupled state synchronization, and complex imperative DOM clearing loops. By replacing these workflows with a decoupled, object-oriented state engine, this change increases navigation performance, stabilizes platform state persistence, and improves UI safety across selection workflows.
Additionally, this change addresses long-standing user experience requests by introducing a classic parent-directory ascension element ("..") to folder listings, standardizing administrative file listings, and providing automated safety flags to prevent destructive modifications during multi-selection activities.
---
Historically, directory history within the file browser module was maintained via a plain JavaScript array (`state`). While functionally straightforward, this architecture coupled state manipulation directly to imperative DOM interactions. When users navigated deep into hierarchical folders or stepped backward using top breadcrumbs, the system relied on manual validation loops that concurrently modified the state index while explicitly destroying HTML fragments. This approach created code maintenance challenges and heightened the risk of race conditions, where mismatched state-to-DOM records could cause rendering errors during slow network or storage operations.
To resolve this coupling, this commit introduces the `NavStack` state controller class, which extends the native `EventTarget` framework. By inheriting from `EventTarget`, `NavStack` functions as an isolated, event-driven micro-service dedicated solely to routing history. It maintains its data structures through private properties, utilizing a private `Set` (`#urlSet`) for immediate $O(1)$ duplicate validation alongside a private sequential record array (`#arr`).
Instead of modifying external layout components directly, the state engine exposes high-level history actions (`push`, `pop`, `popUntil`) that dispatch explicit custom events containing state payloads. The core layout manager registers isolated event handlers reacting to these notifications. Consequently, state modifications automatically propagate to the browser interface and the storage synchronization hooks, standardizing data flows throughout the component's lifecycle.
---
The `NavStack` engine is designed with strong parameter defensive validation and precise lifecycle tracking interfaces. Below is an overview of its core API structure:
The `push({ url, name })` method serves as the entry gateway for navigation history tracker records.
* **Defensive Guard**: It strictly ensures that incoming `url` arguments are resolved into non-empty strings. If a null, undefined, or empty value is passed, it halts processing immediately by throwing a descriptive `TypeError`.
* **Idempotency Check**: It verifies the string against the internal `#urlSet`. If the URL has already been recorded in the active navigation history, the push operation returns early without altering the sequence, preventing duplicate navigation loops.
* **Adaptive Normalization**: If the descriptive folder `name` parameter is missing or evaluates to an empty string, it dynamically generates an fallback string by calling `Url.basename(url)`, falling back to the raw URL if necessary.
* **Reactive Dispatch**: Once pushed to the collection, it dispatches a new `CustomEvent("push")` carrying the immutable snapshot payload.
Stack truncation is executed through a centralized private worker method (`#popUntil(url)`) that handles single entries as well as multi-level structural descents.
* **Reverse Iteration Loop**: The routine scans backwards from the top index of the historical array. If an explicit target URL is provided, the loop terminates as soon as that folder record is reached, preserving the underlying path history.
* **Dynamic Splicing & Purging**: For every entry traversed during truncation, the engine removes the unique record identifier from the validation `#urlSet`, cuts down the structural length of the underlying history array, and dispatches a dedicated `CustomEvent("pop")` detailing the removed layer.
* **Public Aliasing**: The public interface splits this functionality into a single-step `pop()` operation and a validated `popUntil(url)` method. The latter includes runtime parameter guards that throw a `TypeError` if a blank query string is supplied.
* **Safe Indexing (`get`)**: The `get(i)` method incorporates relative negative tracking notation (e.g., passing `-1` reads the top active record, while `-2` retrieves the immediate parent folder). It features rigorous numeric validation via validation checks against `NaN` parameters and returns shallow object copies to prevent external code from mutating internal state arrays.
* **Persistence Serialization (`toJSON`)**: To allow clean integrations with local storage systems, the class exposes a native `toJSON()` interface. When passed into standard serialization mechanisms like `JSON.stringify()`, the instance automatically exports its private location list as a clean JSON structure, keeping its internal tracking mechanisms encapsulated.
---
Following the integration of the `NavStack` architecture, the layout controller within `FileBrowserInclude` has been converted from an imperative architecture into a collection of reactive event listeners.
The initialization loop registers a persistent serialization pipeline linked to the state engine's lifecycle hooks. When the application configuration allows state preservation (`doesOpenLast`), an automated serialization task attaches directly to the `"push"` and `"pop"` events. Any modification to the directory path instantly updates the `localStorage.fileBrowserState` value. This setup isolates data persistence tasks from folder navigation logic, ensuring the layout remains synchronized across application updates.
Manually managed UI cleaning procedures inside the directory navigation module have been replaced with a clean subscriber pattern:
* **The "pop" Interface Lifecycle**: When a location is evicted from history, the engine fires a pop handler. This event automatically searches for the exact DOM element via identifier queries (`tag.get('#' + getNavId(url))`) and removes it from the browser view. Concurrently, it automatically clears tracking indexes from the global `actionStack`.
* **The "push" Interface Lifecycle**: When entering a new directory, a push event listener calculates the relative location step by querying the previous index via `navStack.get(-2)`. If found, it establishes a back-navigation callback link and appends a clean navigation breadcrumb item to the upper navbar layout.
---
This commit introduces a native parent folder row ("..") to the internal directory file listings. This provides an alternative to the top breadcrumbs, offering a classic directory ascension model suitable for both touchscreen interactions and keyboard navigation.
Within the core file rendering query engine (`getDir`), when a storage location successfully performs an asynchronous listing check (`fs.lsDir()`), a flag (`oneDirUp = true`) is initialized. Upon verification, the loader intercepts the folder result array and calls an ingestion routine using `util.pushFolder`. This creates a pseudo-directory node mapped directly to a parent directory configuration:
```javascript
util.pushFolder(list, "..", null, {
oneDirUp: true,
notSelectable: true,
});
```
(AI generated commit message)
6f52174 to
682762f
Compare
This PR completes an architectural refactor of Acode's file browser subsystem. It eliminates legacy technical debt centered around manual array manipulations, uncoupled state synchronization, and complex imperative DOM clearing loops. By replacing these workflows with a decoupled, object-oriented state engine, this change increases navigation performance, stabilizes platform state persistence, and improves UI safety across selection workflows.
Additionally, this change addresses long-standing user experience requests by introducing a classic parent-directory ascension element ("..") to folder listings, standardizing administrative file listings, and providing automated safety flags to prevent destructive modifications during multi-selection activities.
1. Architectural Rationales & Structural Refactoring
Historically, directory history within the file browser module was maintained via a plain JavaScript array (
state). While functionally straightforward, this architecture coupled state manipulation directly to imperative DOM interactions. When users navigated deep into hierarchical folders or stepped backward using top breadcrumbs, the system relied on manual validation loops that concurrently modified the state index while explicitly destroying HTML fragments. This approach created code maintenance challenges and heightened the risk of race conditions, where mismatched state-to-DOM records could cause rendering errors during slow network or storage operations.To resolve this coupling, this PR introduces the$O(1)$ duplicate validation alongside a private sequential record array (
NavStackstate controller class, which extends the nativeEventTargetframework. By inheriting fromEventTarget,NavStackfunctions as an isolated, event-driven micro-service dedicated solely to routing history. It maintains its data structures through private properties, utilizing a privateSet(#urlSet) for immediate#arr).Instead of modifying external layout components directly, the state engine exposes high-level history actions (
push,pop,popUntil) that dispatch explicit custom events containing state payloads. The core layout manager registers isolated event handlers reacting to these notifications. Consequently, state modifications automatically propagate to the browser interface and the storage synchronization hooks, standardizing data flows throughout the component's lifecycle.2. Technical Specification of the NavStack Engine
The
NavStackengine is designed with strong parameter defensive validation and precise lifecycle tracking interfaces. Below is an overview of its core API structure:2.1 Stack Insertion Engine (
push)The
push({ url, name })method serves as the entry gateway for navigation history tracker records.urlarguments are resolved into non-empty strings. If a null, undefined, or empty value is passed, it halts processing immediately by throwing a descriptiveTypeError.#urlSet. If the URL has already been recorded in the active navigation history, the push operation returns early without altering the sequence, preventing duplicate navigation loops.nameparameter is missing or evaluates to an empty string, it dynamically generates an fallback string by callingUrl.basename(url), falling back to the raw URL if necessary.CustomEvent("push")carrying the immutable snapshot payload.2.2 Stack Evacuation Engine (
#popUntil/popUntil/pop)Stack truncation is executed through a centralized private worker method (
#popUntil(url)) that handles single entries as well as multi-level structural descents.#urlSet, cuts down the structural length of the underlying history array, and dispatches a dedicatedCustomEvent("pop")detailing the removed layer.pop()operation and a validatedpopUntil(url)method. The latter includes runtime parameter guards that throw aTypeErrorif a blank query string is supplied.2.3 Accessor and Serializer Utilities
get): Theget(i)method incorporates relative negative tracking notation (e.g., passing-1reads the top active record, while-2retrieves the immediate parent folder). It features rigorous numeric validation via validation checks againstNaNparameters and returns shallow object copies to prevent external code from mutating internal state arrays.toJSON): To allow clean integrations with local storage systems, the class exposes a nativetoJSON()interface. When passed into standard serialization mechanisms likeJSON.stringify(), the instance automatically exports its private location list as a clean JSON structure, keeping its internal tracking mechanisms encapsulated.3. Reactive UI Binding and State Synchronization Logic
Following the integration of the
NavStackarchitecture, the layout controller withinFileBrowserIncludehas been converted from an imperative architecture into a collection of reactive event listeners.3.1 Local Storage Synchronization
The initialization loop registers a persistent serialization pipeline linked to the state engine's lifecycle hooks. When the application configuration allows state preservation (
doesOpenLast), an automated serialization task attaches directly to the"push"and"pop"events. Any modification to the directory path instantly updates thelocalStorage.fileBrowserStatevalue. This setup isolates data persistence tasks from folder navigation logic, ensuring the layout remains synchronized across application updates.3.2 Automated Layout Purging
Manually managed UI cleaning procedures inside the directory navigation module have been replaced with a clean subscriber pattern:
tag.get('#' + getNavId(url))) and removes it from the browser view. Concurrently, it automatically clears tracking indexes from the globalactionStack.navStack.get(-2). If found, it establishes a back-navigation callback link and appends a clean navigation breadcrumb item to the upper navbar layout.4. Implementation of Native Parent Directory Traversal ("..")
This PR introduces a native parent folder row ("..") to the internal directory file listings. This provides an alternative to the top breadcrumbs, offering a classic directory ascension model suitable for both touchscreen interactions and keyboard navigation.
4.1 Dynamic Directory Ingestion
Within the core file rendering query engine (
getDir), when a storage location successfully performs an asynchronous listing check (fs.lsDir()), a flag (oneDirUp = true) is initialized. Upon verification, the loader intercepts the folder result array and calls an ingestion routine usingutil.pushFolder. This creates a pseudo-directory node mapped directly to a parent directory configuration:(PR name and description are AI generated)