Skip to content

File Index API v1002+

The File Index API is the preferred way to list and search files in large local workspaces. It moves indexing for SAF (content:) and file:// roots into a native Android SQLite index so the WebView no longer builds a full in-memory file tree.

INFO

Available from versionCode 1002. Set "minVersionCode": 1002 in plugin.json when your plugin depends on it.

Why use fileIndex?

fileList (legacy)fileIndex (new)
SAF / file://No longer fully listedNative SQLite index
FTP / SFTP / customStill worksNot supported — use fileList
API styleSync tree objectsAsync flat records
Large workspacesHeavy WebView treePaginated native queries
SearchApp-side workersOptional native streaming search

acode.require("fileList") is deprecated. It now contains files from non-native providers only. Plugins that need SAF or file:// files must migrate to fileIndex.

Import

js
const fileIndex = acode.require("fileIndex");

Feature detection:

js
const fileIndex = acode.require("fileIndex");
if (!fileIndex?.query) {
  // Running on an older Acode build — use fileList fallback
}

Compatibility

ProviderIndex / queryContent search
SAF (content:)NativeNative
file://NativeNative
FTP / SFTPNot supportedJavaScript fallback elsewhere
Custom storage pluginsNot supportedJavaScript fallback elsewhere

Check support before scanning:

js
if (fileIndex.supports(workspaceUrl)) {
  await fileIndex.scan(workspaceUrl);
}

Quick start

js
const fileIndex = acode.require("fileIndex");
const addedFolder = acode.require("addedfolder");

const roots = addedFolder
  .filter((folder) => folder.listFiles && fileIndex.supports(folder.url))
  .map((folder) => folder.url);

const { entries, hasMore, cursor } = await fileIndex.query({
  roots,
  text: "filename",
  limit: 200,
});

for (const file of entries) {
  console.log(file.name, file.path, file.url);
}

Methods

supports(url?: string): boolean

Returns true when the URL can be indexed natively (file: or content: and the native plugin is available).

js
fileIndex.supports("file:///sdcard/MyProject"); // true on supported builds
fileIndex.supports("ftp://example.com/www"); // false

query(options?): Promise<FileIndexQueryResult>

Query indexed entries. Results are flat metadata records, not Tree objects, and support cursor pagination.

OptionTypeDefaultDescription
rootsstring[][]Limit to these workspace roots. Empty = all indexed roots
textstring""Case-insensitive match on name and path
urlstring""Exact URL lookup
includeDirectoriesbooleanfalseInclude folders
limitnumber200Page size (capped at 1000)
cursornumber0Offset from a previous page
js
let cursor = 0;
const all = [];

while (true) {
  const page = await fileIndex.query({
    roots: [workspaceUrl],
    text: "util",
    limit: 100,
    cursor,
  });
  all.push(...page.entries);
  if (!page.hasMore) break;
  cursor = page.cursor;
}

get(url: string): Promise<FileIndexEntry | null>

Fetch a single indexed entry by exact URL (includes directories).

js
const entry = await fileIndex.get(fileUrl);

scan(root, options?): Promise & { id, cancel }

Fully scan a SAF or file:// workspace into the native index. Acode already scans open folders; plugins usually only need this for custom roots.

js
const job = fileIndex.scan(
  { url: rootUrl, name: "My Project" },
  { indexContent: false },
);

// Optional: cancel an in-flight scan
// await job.cancel();

const done = await job;
console.log(done.files, done.dirs);
OptionTypeDescription
title / namestringDisplay title for the workspace
excludeFoldersstring[]Glob-like exclude patterns (defaults to app settings)
showHiddenFilesbooleanInclude hidden files
defaultEncodingstringEncoding for optional content indexing
indexContentbooleanAlso cache file text for faster search

update(root, changes?): Promise<{ added, removed }>

Incrementally add or remove paths without a full rescan.

js
await fileIndex.update(rootUrl, {
  added: [{ url: newFileUrl, parentUrl: parentDirUrl }],
  removed: [deletedFileUrl],
});

search(options, onEvent?): { id, result, cancel }

Start a native streaming search or replace.

OptionTypeDefaultDescription
rootsstring[][]Workspace roots to search from the index
filesobject[][]Explicit file list (merged with indexed root files)
searchstringPattern / text to find
replacestringReplacement text when mode is "replace"
mode"search" | "replace""search"Operation mode
options.regExpbooleanfalseTreat search as a regular expression
options.wholeWordbooleanfalseWhole-word matching
options.caseSensitivebooleanfalseCase-sensitive matching
options.includestringInclude globs
options.excludestringExclude globs
overlaysRecord<string, string>{}In-memory content (e.g. open dirty editors)
batchResultsbooleantrueEmit batched search-results events
useIndexbooleanfalsePrefer cached file contents when available
defaultEncodingstringapp settingEncoding for disk reads
js
const { id, result, cancel } = fileIndex.search(
  {
    roots: [workspaceUrl],
    search: "TODO",
    options: { caseSensitive: false },
    batchResults: true,
  },
  (event) => {
    switch (event.type) {
      case "search-results":
        for (const item of event.data) {
          console.log(item.file.url, item.matches.length);
        }
        break;
      case "progress":
        console.log(`${event.data}%`);
        break;
      case "error":
        console.error(event.error);
        break;
    }
  },
);

await result; // resolves on done-searching / done-replacing
// await cancel();

Batched vs single events

fileIndex.search defaults batchResults to true, so you usually handle search-results (array).

The low-level sdcard.workspaceSearch() keeps single search-result events unless you pass batchResults: true.

markDirty(urls: string[]): Promise

Invalidate cached contents after an editor save or external file change.

js
await fileIndex.markDirty([fileUrl]);

clear(roots?: string[]): Promise

Remove native indexes for the given roots (or clear as implemented by the native layer).

js
await fileIndex.clear([rootUrl]);

whenReady(roots?: string[]): Promise

Wait until in-flight scans finish. Pass roots to wait only for those workspaces.

js
await fileIndex.whenReady(roots);
const { entries } = await fileIndex.query({ roots, text: query });

subscribe(listener): () => void

Listen for scan / index events. Returns an unsubscribe function.

js
const stop = fileIndex.subscribe((event) => {
  if (event.type === "status") {
    console.log(event.message, event.progress);
  }
});

// later
stop();

cancel(id: string): Promise

Cancel a scan or search job by id.

Entry shape

Query and search results use flat records (not nested Tree objects):

FieldTypeDescription
rootUrlstringWorkspace root URL
parent / parentUrlstringParent directory URL
namestringFile or folder name
pathstringPath relative to the workspace title
url / uristringAbsolute URL
mime / typestringMIME type when known
isDirectorybooleanDirectory flag
isFilebooleanFile flag
sizenumberSize in bytes
modifiedDatenumberLast modified timestamp

Migrating from fileList

Before (deprecated)

js
const fileList = acode.require("fileList");
const files = fileList(); // all files as Tree objects

fileList.on("add-file", (file) => {
  console.log(file.path);
});

After

js
const fileIndex = acode.require("fileIndex");
const addedFolder = acode.require("addedfolder");

const roots = addedFolder
  .filter((f) => f.listFiles && fileIndex.supports(f.url))
  .map((f) => f.url);

await fileIndex.whenReady(roots);

const { entries } = await fileIndex.query({
  roots,
  text: "",
  limit: 200,
});

Key differences:

  1. fileIndex is asynchronous — always await queries and scans.
  2. Results are flat records — no children / parent tree navigation.
  3. Pagination — use cursor / hasMore for large result sets.
  4. SAF + file:// only — keep using fileList for FTP/SFTP if needed.
  5. Search events may be batched — handle search-results as well as search-result.

Hybrid pattern (native roots + remote fallback):

js
const fileIndex = acode.require("fileIndex");
const fileList = acode.require("fileList");
const addedFolder = acode.require("addedfolder");

const nativeRoots = [];
const remoteFiles = [];

for (const folder of addedFolder) {
  if (!folder.listFiles) continue;
  if (fileIndex.supports(folder.url)) {
    nativeRoots.push(folder.url);
  }
}

const { entries } = nativeRoots.length
  ? await fileIndex.query({ roots: nativeRoots, text: query, limit: 300 })
  : { entries: [] };

// Non-native providers still appear in the legacy list
for (const file of fileList()) {
  remoteFiles.push(file);
}

Events

Scan and search jobs emit events with a shared shape. Common type values:

TypeWhen
statusProgress message during scan/search
progressNumeric progress (data is 0–100)
batchOptional entry batches during scan
search-resultOne file's matches (batchResults: false)
search-resultsArray of file match payloads (batchResults: true)
replace-resultFile content after replace
doneScan finished
cancelledScan cancelled
done-searching / done-replacingSearch/replace finished
errorFailure (error message string)

Released under the MIT License.