diff --git a/frontend/src/reports/editor/EditorMenu.svelte b/frontend/src/reports/editor/EditorMenu.svelte
index e53099f48..0c9370af1 100644
--- a/frontend/src/reports/editor/EditorMenu.svelte
+++ b/frontend/src/reports/editor/EditorMenu.svelte
@@ -14,7 +14,10 @@
import AppMenu from "./AppMenu.svelte";
import AppMenuItem from "./AppMenuItem.svelte";
import AppMenuSubItem from "./AppMenuSubItem.svelte";
+ import File from "./File.svelte";
+ import Folder from "./Folder.svelte";
import Key from "./Key.svelte";
+ import { source_tree } from "./treeview";
export let file_path: string;
export let editor: EditorView;
@@ -33,18 +36,17 @@
editor.focus();
}
}
+ $: [filenodes, foldernodes] = source_tree(sources, goToFileAndLine);
- {#each sources as source}
- goToFileAndLine(source)}
- selected={source === file_path}
- >
- {source}
-
+ {#each filenodes as file}
+
+ {/each}
+ {#each foldernodes as folder}
+
{/each}
diff --git a/frontend/src/reports/editor/File.svelte b/frontend/src/reports/editor/File.svelte
new file mode 100644
index 000000000..6aeb499c1
--- /dev/null
+++ b/frontend/src/reports/editor/File.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/reports/editor/Folder.svelte b/frontend/src/reports/editor/Folder.svelte
new file mode 100644
index 000000000..735c00787
--- /dev/null
+++ b/frontend/src/reports/editor/Folder.svelte
@@ -0,0 +1,67 @@
+
+
+
+
{folder.name}
+
+
+ {#each folder.subfiles as file}
+ -
+
+
+ {/each}
+ {#each folder.subfolders as subfolder}
+ -
+
+
+ {/each}
+
+
+
+
diff --git a/frontend/src/reports/editor/treeview.ts b/frontend/src/reports/editor/treeview.ts
new file mode 100644
index 000000000..c222c054f
--- /dev/null
+++ b/frontend/src/reports/editor/treeview.ts
@@ -0,0 +1,90 @@
+export interface FileNode {
+ name: string;
+ path: string;
+ action: () => void;
+}
+export interface FolderNode {
+ name: string;
+ subfolders: FolderNode[];
+ subfiles: FileNode[];
+}
+
+const zip: (a: A[], b: B[]) => [A, B][] = (a: A[], b: B[]) =>
+ a.reduce((l: [A, B][], k: A, i) => {
+ const n = b[i];
+ if (n !== undefined) {
+ l.push([k, n]);
+ }
+ return l;
+ }, []);
+
+function shorten_folder(folder: FolderNode): FolderNode {
+ if (folder.subfiles.length === 0) {
+ const subfolder = folder.subfolders[0];
+ if (subfolder !== undefined) {
+ const new_name = `${folder.name}/${subfolder.name}`;
+ return shorten_folder({
+ name: new_name,
+ subfolders: subfolder.subfolders,
+ subfiles: subfolder.subfiles,
+ });
+ }
+ }
+ return {
+ name: folder.name,
+ subfolders: folder.subfolders.map(shorten_folder),
+ subfiles: folder.subfiles,
+ };
+}
+function _source_tree(
+ paths: string[][],
+ filenodes: FileNode[]
+): [FileNode[], FolderNode[]] {
+ const groupBy = (arr: T[], key: (i: T) => string) =>
+ arr.reduce>((groups, item) => {
+ (groups[key(item)] ||= []).push(item);
+ return groups;
+ }, {});
+
+ const r1 = zip(filenodes, paths)
+ .filter((a) => a[1].length === 0)
+ .map((a) => a[0]);
+ const r2 = Object.entries(
+ groupBy(
+ zip(filenodes, paths).reduce(
+ (acc: [FileNode, string[], string][], [file, path]) => {
+ const root = path[0];
+ if (root !== undefined) {
+ acc.push([file, path.slice(1), root]);
+ }
+ return acc;
+ },
+ []
+ ),
+ (a: [FileNode, string[], string]) => a[2]
+ )
+ ).map(([dir, group]: [string, [FileNode, string[], string][]]) => {
+ const [subfiles, subfolders] = _source_tree(
+ group.map((a: [FileNode, string[], string]) => a[1]),
+ group.map((a: [FileNode, string[], string]) => a[0])
+ );
+ return { name: dir, subfolders, subfiles };
+ });
+ return [r1, r2];
+}
+export function source_tree(
+ files: string[],
+ goToFileAndLine: (filename: string, line?: number) => void
+): [FileNode[], FolderNode[]] {
+ files.sort();
+ const paths = files.map((file) => file.split("/"));
+ const raw_filenodes = zip(files, paths).map(
+ ([file, path]: [string, string[]]) => ({
+ name: path.pop() ?? file,
+ path: file,
+ action: () => goToFileAndLine(file),
+ })
+ );
+ const [filenodes, foldernodes] = _source_tree(paths, raw_filenodes);
+ return [filenodes, foldernodes.map(shorten_folder)];
+}