Skip to content

Commit 0742f8a

Browse files
Web 0.14.14: Variable umbenennen (Refactoring) in Code und Struktogramm (#230)
Rechtsklick auf einen Variablennamen im Code oder auf ein Element im Struktogramm, F2 oder Knopf im Objektinspektor: Alle Stellen mit genau derselben Schreibweise ändern sich in einem Rückgängig-Schritt. Zeichenketten, Kommentare und Felder hinter dem Punkt bleiben. Claude-Session: https://claude.ai/code/session_01Ugz28StNfBzy9w8dRvcuGj Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3231475 commit 0742f8a

60 files changed

Lines changed: 2405 additions & 1352 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

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

1616
### Added
1717

18+
- **Web 0.14.14, rename a variable (refactoring):** right-click a variable name in the Code area (or press F2) and choose „Variable umbenennen …“; in the structure chart right-click an element, press F2 or use the new button in the object inspector. A small window shows the name, how often it is used and the new name. Every place with exactly the same spelling changes in the code and in the structure chart in one undo step (`zahl` and `Zahl` stay separate, matching the case-sensitivity exercises); text in "…", characters, comments and fields after a dot (`feld.length`) stay. Invalid names and Java keywords are refused; if the new name already exists, a warning says both would become one variable (all 21 languages).
1819
- **Web 0.14.13, no robot emoji in the world chooser:** the button „🤖 VisuBot“ in the gear menu and the start dialog is now just „VisuBot“, like the other worlds (Holger's wish).
1920
- **Web 0.14.12, the „Einrücken“ button also formats:** in the Code area the button no longer only fixes the spaces at the start of a line. It now puts every block brace `}` on its own line (`} else {`, `} catch`, `} while` of do-while stay together), one statement per line, and spaces around operators like `>=`, `=`, `&&`, `? :`, after commas and in `if (`/`) {`. It changes only whitespace between tokens, never joins lines, and is careful with array initialisers, generics, signs like `-1` and `(int) -x`, `import java.util.*`, pseudocode like `if (Alter über 18?)`, strings and comments. One undo step; breakpoints move with their line; with a selection only those lines. Nothing is reformatted while typing. New `web/src/core/javaFormat.ts` with tests (#244).
2021
- **Web 0.14.11, one Angeberwissen per step in lesson 8:** the „goto fail“ box moves from step „Jetzt in Java“ (which keeps Duke) to step „Lückentext: Gerade oder ungerade“, right after its explanation about curly brackets (de/en/pt).

‎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.14.13",
4+
"version": "0.14.14",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎web/src/core/rename.ts‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { VSDocument, VSElement } from './model'
2+
3+
/**
4+
* Umbenennen (Refactoring): eine Variable an allen Stellen umbenennen, im Java-Code und im Struktogramm.
5+
* Genau dieselbe Schreibweise (`zahl` ≠ `Zahl`), nur ganze Wörter; Text in "…"/'…' und Kommentare bleiben.
6+
*/
7+
8+
export const javaKeywords = new Set(
9+
(
10+
'abstract assert boolean break byte case catch char class const continue default do double else enum extends final ' +
11+
'finally float for goto if implements import instanceof int interface long native new package private protected public ' +
12+
'return short static strictfp super switch synchronized this throw throws transient try void volatile while ' +
13+
'true false null var record yield'
14+
).split(' '),
15+
)
16+
17+
const wordChar = /[\p{L}\p{N}_$]/u
18+
const wordStart = /[\p{L}_$]/u
19+
20+
export interface Word {
21+
from: number
22+
to: number
23+
name: string
24+
}
25+
26+
/** Alle Wörter im Code außerhalb von Zeichenketten, Zeichen und Kommentaren. */
27+
export function codeWords(text: string): Word[] {
28+
const words: Word[] = []
29+
let i = 0
30+
while (i < text.length) {
31+
const c = text[i]
32+
if (c === '/' && text[i + 1] === '/') {
33+
while (i < text.length && text[i] !== '\n') i++
34+
} else if (c === '/' && text[i + 1] === '*') {
35+
const end = text.indexOf('*/', i + 2)
36+
i = end < 0 ? text.length : end + 2
37+
} else if (c === '"' || c === "'") {
38+
i++
39+
while (i < text.length && text[i] !== c && text[i] !== '\n') i += text[i] === '\\' ? 2 : 1
40+
i++
41+
} else if (wordStart.test(c)) {
42+
const from = i
43+
while (i < text.length && wordChar.test(text[i])) i++
44+
words.push({ from, to: i, name: text.slice(from, i) })
45+
} else if (/\d/.test(c)) {
46+
// Zahlen wie 1e5 oder 10L gehören nicht zu einem Namen.
47+
while (i < text.length && wordChar.test(text[i])) i++
48+
} else i++
49+
}
50+
return words
51+
}
52+
53+
/** Stellen, an denen die Variable `name` vorkommt (nicht hinter einem Punkt: `feld.length` ist keine Variable `length`). */
54+
export function nameRanges(text: string, name: string): Word[] {
55+
return codeWords(text).filter((w) => w.name === name && !afterDot(text, w.from))
56+
}
57+
58+
function afterDot(text: string, from: number): boolean {
59+
let i = from - 1
60+
while (i >= 0 && (text[i] === ' ' || text[i] === '\t')) i--
61+
return text[i] === '.'
62+
}
63+
64+
/** Wort unter dem Cursor (auch direkt dahinter), wenn es ein umbenennbarer Name ist. */
65+
export function nameAt(text: string, pos: number): Word | undefined {
66+
const w = codeWords(text).find((w) => pos >= w.from && pos <= w.to)
67+
if (!w || javaKeywords.has(w.name) || afterDot(text, w.from)) return undefined
68+
return w
69+
}
70+
71+
/** Text mit umbenannter Variable und Anzahl der geänderten Stellen. */
72+
export function renameInText(text: string, from: string, to: string): { text: string; count: number } {
73+
const ranges = nameRanges(text, from)
74+
let out = ''
75+
let last = 0
76+
for (const r of ranges) {
77+
out += text.slice(last, r.from) + to
78+
last = r.to
79+
}
80+
return { text: out + text.slice(last), count: ranges.length }
81+
}
82+
83+
/** Alle Texte des Struktogramms (Kopfzeile, Elemente, Fallnamen). */
84+
function documentTexts(doc: VSDocument): string[] {
85+
const texts = [doc.caption]
86+
const walk = (elements: VSElement[]) => {
87+
for (const el of elements) {
88+
texts.push(el.textLines.join('\n'))
89+
if (el.loopBody) walk(el.loopBody)
90+
for (const c of el.cases ?? []) {
91+
texts.push(c.name)
92+
walk(c.elements)
93+
}
94+
}
95+
}
96+
walk(doc.elements)
97+
return texts
98+
}
99+
100+
export function renameInDocument(doc: VSDocument, from: string, to: string): { doc: VSDocument; count: number } {
101+
let count = 0
102+
const rename = (s: string) => {
103+
const r = renameInText(s, from, to)
104+
count += r.count
105+
return r.text
106+
}
107+
const walk = (elements: VSElement[]): VSElement[] =>
108+
elements.map((el) => ({
109+
...el,
110+
textLines: rename(el.textLines.join('\n')).split('\n'),
111+
...(el.loopBody ? { loopBody: walk(el.loopBody) } : {}),
112+
...(el.cases ? { cases: el.cases.map((c) => ({ name: rename(c.name), elements: walk(c.elements) })) } : {}),
113+
}))
114+
const next = { ...doc, caption: rename(doc.caption), elements: walk(doc.elements) }
115+
return { doc: next, count }
116+
}
117+
118+
/**
119+
* Namen, die wie Variablen aussehen: keine Schlüsselwörter, keine Methoden `name(`, keine Klassen
120+
* (`Scanner sc`, `Math.max`) und nichts hinter einem Punkt. Reihenfolge wie im Text.
121+
*/
122+
export function variableNames(texts: string[]): string[] {
123+
const names = new Set<string>()
124+
const skip = new Set<string>()
125+
const declared = new Set<string>()
126+
for (const text of texts) {
127+
const words = codeWords(text).filter((w) => !afterDot(text, w.from))
128+
words.forEach((w, i) => {
129+
// „ø“ = leere Anweisung im Struktogramm, kein Name.
130+
if (javaKeywords.has(w.name) || w.name === 'ø') return
131+
const rest = text.slice(w.to)
132+
const next = words[i + 1]
133+
const between = next ? text.slice(w.to, next.from) : ''
134+
// `Scanner sc`, `String[] namen`: das erste Wort ist ein Typ, das zweite die Variable.
135+
const isType = next !== undefined && !javaKeywords.has(next.name) && /^(?:\s*\[\s*\])*[ \t]+$/.test(between)
136+
if (isType && /^\s*(?:[=;,):]|$)/.test(text.slice(next.to))) declared.add(next.name)
137+
const isClass = /^\p{Lu}/u.test(w.name) && /^\s*\./.test(rest)
138+
if (/^\s*\(/.test(rest) || isClass || isType) skip.add(w.name)
139+
else names.add(w.name)
140+
})
141+
}
142+
return [...names].filter((n) => !skip.has(n) || declared.has(n))
143+
}
144+
145+
/** Alle Wörter im Struktogramm, auch Methoden und Klassen: Der neue Name soll keinen davon treffen. */
146+
export function documentWords(doc: VSDocument): string[] {
147+
return documentTexts(doc).flatMap((t) => codeWords(t).map((w) => w.name))
148+
}
149+
150+
export function countInDocument(doc: VSDocument, name: string): number {
151+
return documentTexts(doc).reduce((n, t) => n + nameRanges(t, name).length, 0)
152+
}
153+
154+
export function documentVariableNames(doc: VSDocument): string[] {
155+
return variableNames(documentTexts(doc))
156+
}
157+
158+
export function elementVariableNames(el: VSElement): string[] {
159+
return variableNames([el.textLines.join('\n')])
160+
}
161+
162+
export type RenameCheck = { ok: true; warning?: 'exists' } | { ok: false; reason: 'empty' | 'same' | 'invalid' | 'keyword' }
163+
164+
/** Neuen Namen prüfen; `existing` = Namen, die schon vorkommen. */
165+
export function checkNewName(from: string, to: string, existing: Iterable<string>): RenameCheck {
166+
if (to === '') return { ok: false, reason: 'empty' }
167+
if (to === from) return { ok: false, reason: 'same' }
168+
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(to)) return { ok: false, reason: 'invalid' }
169+
if (javaKeywords.has(to)) return { ok: false, reason: 'keyword' }
170+
for (const n of existing) if (n === to) return { ok: true, warning: 'exists' }
171+
return { ok: true }
172+
}

‎web/src/i18n/ar.strings‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,16 @@
177177
"web_code_scopes" = "كتل الشيفرة ملوّنة";
178178
"web_code_scopes_hint" = "الصنف والدالة والحلقة والتفرّع في مربعات خاصة";
179179
"web_pseudocode" = "شبه كود";
180+
181+
/* Variable umbenennen (Refactoring) */
182+
"web_rename_menu" = "إعادة تسمية المتغير …";
183+
"web_rename_title" = "إعادة تسمية المتغير";
184+
"web_rename_old" = "الاسم";
185+
"web_rename_new" = "الاسم الجديد";
186+
"web_rename_hint" = "يغيّر كل موضع بهذه الكتابة تمامًا (number ليس Number)، في الشيفرة وفي المخطط الهيكلي. يبقى النص بين \"…\" والتعليقات كما هي.";
187+
"web_rename_count_fmt" = "يظهر %d مرة.";
188+
"web_rename_exists_fmt" = "«%@» موجود بالفعل: بعدها يصبح المتغيران متغيرًا واحدًا.";
189+
"web_rename_invalid" = "يبدأ الاسم بحرف ويحتوي على حروف وأرقام و _ فقط.";
190+
"web_rename_keyword_fmt" = "«%@» كلمة مفتاحية في Java.";
191+
"web_rename_apply" = "إعادة تسمية";
192+
"web_rename_cancel" = "إلغاء";

‎web/src/i18n/da.strings‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,16 @@
177177
"web_code_scopes" = "Farvede kodeblokke";
178178
"web_code_scopes_hint" = "Klasse, metode, løkke, forgrening i egne felter";
179179
"web_pseudocode" = "Pseudokode";
180+
181+
/* Variable umbenennen (Refactoring) */
182+
"web_rename_menu" = "Omdøb variabel …";
183+
"web_rename_title" = "Omdøb variabel";
184+
"web_rename_old" = "Navn";
185+
"web_rename_new" = "Nyt navn";
186+
"web_rename_hint" = "Ændrer hvert sted med præcis denne stavemåde (tal er ikke Tal), i koden og i strukturdiagrammet. Tekst i \"…\" og kommentarer bliver.";
187+
"web_rename_count_fmt" = "Forekommer %d gange.";
188+
"web_rename_exists_fmt" = "“%@” findes allerede: bagefter ville begge være én variabel.";
189+
"web_rename_invalid" = "Et navn begynder med et bogstav og indeholder kun bogstaver, cifre og _.";
190+
"web_rename_keyword_fmt" = "“%@” er et Java-nøgleord.";
191+
"web_rename_apply" = "Omdøb";
192+
"web_rename_cancel" = "Annuller";

‎web/src/i18n/de.strings‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,15 @@
741741
"web_visuflow_status_edge" = "Pfeil gewählt. Löschen entfernt ihn.";
742742
"web_visuflow_status_connect" = "Loslassen über dem Symbol, zu dem der Pfeil führt.";
743743
"web_visuflow_edit_label" = "Text im Symbol";
744+
/* Variable umbenennen (Refactoring) */
745+
"web_rename_menu" = "Variable umbenennen …";
746+
"web_rename_title" = "Variable umbenennen";
747+
"web_rename_old" = "Name";
748+
"web_rename_new" = "Neuer Name";
749+
"web_rename_hint" = "Ändert jede Stelle mit genau dieser Schreibweise (zahl ist nicht Zahl), im Code und im Struktogramm. Text in \"…\" und Kommentare bleiben.";
750+
"web_rename_count_fmt" = "Kommt %d-mal vor.";
751+
"web_rename_exists_fmt" = "„%@“ gibt es schon: Danach wären beide eine Variable.";
752+
"web_rename_invalid" = "Ein Name beginnt mit einem Buchstaben und enthält nur Buchstaben, Ziffern und _.";
753+
"web_rename_keyword_fmt" = "„%@“ ist ein Java-Schlüsselwort.";
754+
"web_rename_apply" = "Umbenennen";
755+
"web_rename_cancel" = "Abbrechen";

‎web/src/i18n/el.strings‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,16 @@
177177
"web_code_scopes" = "Χρωματιστά μπλοκ κώδικα";
178178
"web_code_scopes_hint" = "Κλάση, μέθοδος, βρόχος, διακλάδωση σε δικά τους πλαίσια";
179179
"web_pseudocode" = "Ψευδοκώδικας";
180+
181+
/* Variable umbenennen (Refactoring) */
182+
"web_rename_menu" = "Μετονομασία μεταβλητής …";
183+
"web_rename_title" = "Μετονομασία μεταβλητής";
184+
"web_rename_old" = "Όνομα";
185+
"web_rename_new" = "Νέο όνομα";
186+
"web_rename_hint" = "Αλλάζει κάθε σημείο με ακριβώς αυτή τη γραφή (number δεν είναι Number), στον κώδικα και στο δομόγραμμα. Το κείμενο σε \"…\" και τα σχόλια μένουν.";
187+
"web_rename_count_fmt" = "Εμφανίζεται %d φορές.";
188+
"web_rename_exists_fmt" = "Το «%@» υπάρχει ήδη: μετά θα ήταν και τα δύο μία μεταβλητή.";
189+
"web_rename_invalid" = "Ένα όνομα αρχίζει με γράμμα και περιέχει μόνο γράμματα, ψηφία και _.";
190+
"web_rename_keyword_fmt" = "Το «%@» είναι λέξη-κλειδί της Java.";
191+
"web_rename_apply" = "Μετονομασία";
192+
"web_rename_cancel" = "Άκυρο";

‎web/src/i18n/en.strings‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,15 @@
741741
"web_visuflow_status_edge" = "Arrow selected. Delete removes it.";
742742
"web_visuflow_status_connect" = "Let go over the symbol the arrow leads to.";
743743
"web_visuflow_edit_label" = "Text in the symbol";
744+
/* Variable umbenennen (Refactoring) */
745+
"web_rename_menu" = "Rename variable …";
746+
"web_rename_title" = "Rename variable";
747+
"web_rename_old" = "Name";
748+
"web_rename_new" = "New name";
749+
"web_rename_hint" = "Changes every place with exactly this spelling (number is not Number), in the code and in the structogram. Text in \"…\" and comments stay.";
750+
"web_rename_count_fmt" = "Used %d times.";
751+
"web_rename_exists_fmt" = "“%@” already exists: afterwards both would be one variable.";
752+
"web_rename_invalid" = "A name starts with a letter and contains only letters, digits and _.";
753+
"web_rename_keyword_fmt" = "“%@” is a Java keyword.";
754+
"web_rename_apply" = "Rename";
755+
"web_rename_cancel" = "Cancel";

‎web/src/i18n/es.strings‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,16 @@
177177
"web_code_scopes" = "Bloques de código en color";
178178
"web_code_scopes_hint" = "Clase, método, bucle, condición en sus propias cajas";
179179
"web_pseudocode" = "Pseudocódigo";
180+
181+
/* Variable umbenennen (Refactoring) */
182+
"web_rename_menu" = "Renombrar variable …";
183+
"web_rename_title" = "Renombrar variable";
184+
"web_rename_old" = "Nombre";
185+
"web_rename_new" = "Nombre nuevo";
186+
"web_rename_hint" = "Cambia cada lugar con exactamente esta escritura (numero no es Numero), en el código y en el estructograma. El texto entre \"…\" y los comentarios no cambian.";
187+
"web_rename_count_fmt" = "Aparece %d veces.";
188+
"web_rename_exists_fmt" = "«%@» ya existe: después ambas serían una sola variable.";
189+
"web_rename_invalid" = "Un nombre empieza con una letra y solo contiene letras, cifras y _.";
190+
"web_rename_keyword_fmt" = "«%@» es una palabra clave de Java.";
191+
"web_rename_apply" = "Renombrar";
192+
"web_rename_cancel" = "Cancelar";

0 commit comments

Comments
 (0)