Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.

Commit af3f383

Browse files
Web 0.10.1: Neu-Menü wie Eclipse, Source Folder, Inspektor unter dem Diagramm (#179)
Claude-Session: https://claude.ai/code/session_01Ugz28SuLhbcLydch8nbzdQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 78b7dd9 commit af3f383

33 files changed

Lines changed: 1688 additions & 472 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is loosely inspired by Keep a Changelog.
88

99
### Added
1010

11+
- **Web 0.10.1, VisuObject “New” like Eclipse:** a “+ New ▾” menu in the Package Explorer for Java project, source folder, package, class and interface, each with its own window; the class window works like Eclipse's New Java Class (source folder, package, name, modifier public/package, abstract/final, superclass, interfaces, stubs for `main`, superclass constructors and inherited abstract methods, Javadoc with @author). Several source folders per project (e.g. `test`), read from and written to `.classpath` in the workspace and kept in the ZIP. New toolbar tool “Association” as a plain line without navigation next to the directed association. The object inspector has a fixed place below the class diagram and collapses with “–”; it shows end A (where the line starts) and end B (where it ends) with role, multiplicity and navigability. Like in Astah, clicking a role or multiplicity at a selected line (or right-clicking near an end) opens a small editor right there. Renaming a class in the code renames its tab and file (#179).
1112
- **Web 0.10.0, VisuObject like Eclipse and Astah:** an Astah-style toolbar above the class diagram to drag classes and interfaces onto the canvas and draw association, aggregation, composition, inheritance and realization between two classes. Attributes whose type is another class are shown only as a role on the line (with visibility, e.g. `-meineKatzen`), not inside the class box. Object inspector for a relationship: name with reading direction taken from the drag direction (“reverse” button), navigability at each end (navigable = arrowhead, non-navigable = ×, unspecified = open), role and multiplicity at both ends; making the other end navigable adds an attribute named after the role. Inheritance only via the arrow (no “inherits from” choice in the class box). The left area is called “Package Explorer”. Workspace folder like Eclipse (Chrome/Edge, File System Access API): create “VisuObject-Workspace” or open an existing one, one folder per project with `src/package/Class.java`, `visuobject.json` (UML extras and box positions), `.project` and `.classpath` for importing into Eclipse; all projects listed in the Package Explorer, Ctrl+S saves, unsaved tabs show ● instead of × like VS Code, ZIP download stays (#175, #176, #177, #178).
1213
- **Web 0.9.0, VisuObject projects and relationships:** a project with packages like in Eclipse: a package explorer on the left (project › src › packages › files › classes › attributes/methods), every level collapsible, one file per class, `package` line follows the folder, UML package frames in the class diagram, open and save as ZIP (`Name/src/package/Class.java`, fflate, MIT). Layout like WebGoatCode: flat areas with a grey header bar, “–” collapses an area to a strip with its name, splitters between the areas, file tabs above the code. Relationships are drawn by dragging from a handle on the selected class to another class (association, aggregation, composition, inheritance, realization); an object inspector sets kind, name with reading direction, navigability (one way, both ways, no arrow), role and multiplicity at both ends. Java parts go into the code (attribute, list type, imports, `extends`/`implements`), UML-only details into `visuobject.json` in the ZIP. Hollow inheritance triangle; roles at the same distance as multiplicities. The website's VisuStruct links always open VisuStruct (#171, #172, #173).
1314
- **Web 0.8.0, VisuObject:** a third world next to VisuStruct and VisuBot, for object-oriented programming from year 12 (`?visuobject`, petrol colours). On the left the Java code, on the right the class diagram, both linked: typing classes builds the diagram (attributes, methods, constructors, `static`, `abstract`, inheritance, interfaces, associations with multiplicity from attributes like `ArrayList<Hund>`); adding classes, interfaces, attributes and methods in the diagram, double-clicking to change them in UML notation (`- name: String`, `+ bark(): void`), choosing the superclass or making a class abstract writes Java at exactly that spot, method bodies and comments stay. Plan for the next stages: `docs/visuobject/PLAN.md` (#167).

‎web/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎web/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "visustruct-web",
33
"private": true,
4-
"version": "0.10.0",
4+
"version": "0.10.1",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎web/src/core/javaProject.ts‎

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,107 @@ import { parseClasses, type UmlClass } from './classModel'
88
import type { UmlInfo } from './umlRelations'
99

1010
export interface ProjectFile {
11-
/** Pfad unter `src/`, z. B. `tiere/Hund.java`; ohne Ordner im Standard-Package. */
11+
/**
12+
* Pfad unter `src/`, z. B. `tiere/Hund.java`; ohne Ordner im Standard-Package.
13+
* In einem anderen Source Folder steht er davor mit Doppelpunkt: `test:tiere/HundTest.java`.
14+
*/
1215
path: string
1316
text: string
1417
}
1518

1619
export interface JavaProject {
1720
name: string
1821
files: ProjectFile[]
19-
/** Auch leere Packages, z. B. gleich nach „+ Package“. */
22+
/** Auch leere Packages, z. B. gleich nach „+ Package“ (in anderen Source Folders als `test:tiere`). */
2023
packages: string[]
24+
/** Source Folders außer `src`, z. B. `test`, wie in Eclipse; auch leere. */
25+
sourceFolders?: string[]
2126
/** Was nur UML kennt (Aggregation, Name der Beziehung …), siehe `umlRelations.ts`. */
2227
uml?: UmlInfo
2328
}
2429

2530
/** Klasse mit Datei und Package, für Diagramm und Explorer. */
2631
export interface ProjectClass extends UmlClass {
2732
path: string
33+
/** Java-Package ohne Source Folder (wie in `package …;`). */
2834
pkg: string
35+
/** Source Folder, meist `src`. */
36+
root: string
2937
}
3038

31-
export const packageOf = (path: string) => (path.includes('/') ? path.slice(0, path.lastIndexOf('/')).replace(/\//g, '.') : '')
32-
export const fileNameOf = (path: string) => path.slice(path.lastIndexOf('/') + 1)
33-
export const pathFor = (pkg: string, file: string) => (pkg ? `${pkg.replace(/\./g, '/')}/${file}` : file)
39+
export const defaultRoot = 'src'
40+
41+
/** `test:tiere/X.java` → `test` und `tiere/X.java`; ohne Doppelpunkt `src`. Genauso für Package-Schlüssel. */
42+
export function splitRoot(key: string): { root: string; rest: string } {
43+
const i = key.indexOf(':')
44+
return i < 0 ? { root: defaultRoot, rest: key } : { root: key.slice(0, i), rest: key.slice(i + 1) }
45+
}
46+
const withRoot = (root: string, rest: string) => (root === defaultRoot ? rest : `${root}:${rest}`)
47+
48+
/** Schlüssel des Packages im Explorer: `tiere`, in anderen Source Folders `test:tiere`. */
49+
export const packageKey = (root: string, pkg: string) => withRoot(root, pkg)
50+
/** Java-Package ohne Source Folder. */
51+
export const barePackage = (key: string) => splitRoot(key).rest
52+
export const rootOf = (key: string) => splitRoot(key).root
53+
54+
export const packageOf = (path: string) => {
55+
const { root, rest } = splitRoot(path)
56+
return withRoot(root, rest.includes('/') ? rest.slice(0, rest.lastIndexOf('/')).replace(/\//g, '.') : '')
57+
}
58+
export const fileNameOf = (path: string) => {
59+
const rest = splitRoot(path).rest
60+
return rest.slice(rest.lastIndexOf('/') + 1)
61+
}
62+
export const pathFor = (pkg: string, file: string) => {
63+
const { root, rest } = splitRoot(pkg)
64+
return withRoot(root, rest ? `${rest.replace(/\./g, '/')}/${file}` : file)
65+
}
66+
/** Pfad wie auf der Festplatte, z. B. `test/tiere/HundTest.java`. */
67+
export const diskPath = (path: string) => {
68+
const { root, rest } = splitRoot(path)
69+
return `${root}/${rest}`
70+
}
71+
72+
const folderName = /^[\p{L}\p{N}_$-][\p{L}\p{N}_$.-]*$/u
73+
export const isFolderName = (name: string) => folderName.test(name) && !name.includes('..')
74+
75+
/** Alle Source Folders: `src` zuerst, dann die übrigen (auch leere und solche, die nur aus Dateien bekannt sind). */
76+
export function sourceFoldersOf(project: JavaProject): string[] {
77+
const all = new Set([defaultRoot, ...(project.sourceFolders ?? [])])
78+
for (const f of project.files) all.add(rootOf(f.path))
79+
for (const p of project.packages) all.add(rootOf(p))
80+
return [...all].sort((a, b) => (a === defaultRoot ? -1 : b === defaultRoot ? 1 : a.localeCompare(b)))
81+
}
3482

3583
const packageName = /^[a-z_$][\p{L}\p{N}_$]*(\.[a-z_$][\p{L}\p{N}_$]*)*$/u
3684
export const isPackageName = (name: string) => packageName.test(name)
3785

38-
/** Alle Packages: aus den Dateien und die leeren, sortiert; das Standard-Package nur, wenn Dateien darin liegen. */
39-
export function packagesOf(project: JavaProject): string[] {
86+
/**
87+
* Alle Packages als Schlüssel: aus den Dateien und die leeren, sortiert nach Source Folder;
88+
* das Standard-Package nur, wenn Dateien darin liegen. Mit `root` nur die aus diesem Source Folder.
89+
*/
90+
export function packagesOf(project: JavaProject, root?: string): string[] {
4091
const all = new Set(project.packages)
4192
for (const f of project.files) all.add(packageOf(f.path))
42-
return [...all].filter((p) => p !== '' || project.files.some((f) => packageOf(f.path) === '')).sort((a, b) => (a === '' ? 1 : b === '' ? -1 : a.localeCompare(b)))
93+
const roots = sourceFoldersOf(project)
94+
return [...all]
95+
.filter((p) => barePackage(p) !== '' || project.files.some((f) => packageOf(f.path) === p))
96+
.filter((p) => root === undefined || rootOf(p) === root)
97+
.sort((a, b) => {
98+
const ra = roots.indexOf(rootOf(a)) - roots.indexOf(rootOf(b))
99+
if (ra) return ra
100+
const [x, y] = [barePackage(a), barePackage(b)]
101+
return x === '' ? 1 : y === '' ? -1 : x.localeCompare(y)
102+
})
43103
}
44104

45105
export function filesIn(project: JavaProject, pkg: string): ProjectFile[] {
46106
return project.files.filter((f) => packageOf(f.path) === pkg).sort((a, b) => a.path.localeCompare(b.path))
47107
}
48108

49109
/** `package …;` am Anfang setzen, ändern oder (Standard-Package) entfernen. */
50-
export function withPackageLine(text: string, pkg: string): string {
110+
export function withPackageLine(text: string, key: string): string {
111+
const pkg = barePackage(key)
51112
const line = /^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)package\s+[\w.]+\s*;[ \t]*\n?/
52113
const match = line.exec(text)
53114
if (match) {
@@ -64,7 +125,7 @@ export function declaredPackage(text: string): string {
64125
}
65126

66127
export function projectClasses(project: JavaProject): ProjectClass[] {
67-
return project.files.flatMap((f) => parseClasses(f.text).map((c) => ({ ...c, path: f.path, pkg: packageOf(f.path) })))
128+
return project.files.flatMap((f) => parseClasses(f.text).map((c) => ({ ...c, path: f.path, pkg: barePackage(packageOf(f.path)), root: rootOf(f.path) })))
68129
}
69130

70131
export function updateFile(project: JavaProject, path: string, text: string): JavaProject {
@@ -108,6 +169,33 @@ export function renamePackage(project: JavaProject, from: string, to: string): J
108169
}
109170
}
110171

172+
/** Neuer, leerer Source Folder wie `test`. */
173+
export function addSourceFolder(project: JavaProject, name: string): JavaProject {
174+
return sourceFoldersOf(project).includes(name) ? project : { ...project, sourceFolders: [...(project.sourceFolders ?? []), name] }
175+
}
176+
177+
/** Source Folder umbenennen; Dateien und Packages ziehen mit (`src` bleibt). */
178+
export function renameSourceFolder(project: JavaProject, from: string, to: string): JavaProject {
179+
const move = (key: string) => (rootOf(key) === from ? withRoot(to, barePackage(key)) : key)
180+
return {
181+
...project,
182+
sourceFolders: (project.sourceFolders ?? []).map((r) => (r === from ? to : r)).filter((r) => r !== defaultRoot),
183+
packages: project.packages.map(move),
184+
files: project.files.map((f) => (rootOf(f.path) === from ? { ...f, path: withRoot(to, splitRoot(f.path).rest) } : f)),
185+
}
186+
}
187+
188+
/** Source Folder samt Packages und Dateien löschen (`src` nicht). */
189+
export function removeSourceFolder(project: JavaProject, root: string): JavaProject {
190+
if (root === defaultRoot) return project
191+
return {
192+
...project,
193+
sourceFolders: (project.sourceFolders ?? []).filter((r) => r !== root),
194+
packages: project.packages.filter((p) => rootOf(p) !== root),
195+
files: project.files.filter((f) => rootOf(f.path) !== root),
196+
}
197+
}
198+
111199
/** Package samt Dateien löschen. */
112200
export function removePackage(project: JavaProject, pkg: string): JavaProject {
113201
return { ...project, packages: project.packages.filter((p) => p !== pkg), files: project.files.filter((f) => packageOf(f.path) !== pkg) }
@@ -142,6 +230,8 @@ export function projectFromJSON(json: string): JavaProject | undefined {
142230
if (!Array.isArray(data.files)) return undefined
143231
const files = data.files.filter((f): f is ProjectFile => typeof f?.path === 'string' && typeof f?.text === 'string')
144232
const project: JavaProject = { name: typeof data.name === 'string' && data.name ? data.name : 'Projekt', files, packages: Array.isArray(data.packages) ? data.packages.filter((p) => typeof p === 'string') : [] }
233+
const roots = Array.isArray(data.sourceFolders) ? data.sourceFolders.filter((r): r is string => typeof r === 'string' && isFolderName(r) && r !== defaultRoot) : []
234+
if (roots.length) project.sourceFolders = roots
145235
const uml = umlFromJSON(data.uml)
146236
if (uml) project.uml = uml
147237
return project

0 commit comments

Comments
 (0)