Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is loosely inspired by Keep a Changelog.

### Added

- **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).
- **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).
- **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).
- **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).
Expand Down
4 changes: 2 additions & 2 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "visustruct-web",
"private": true,
"version": "0.14.13",
"version": "0.14.14",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
172 changes: 172 additions & 0 deletions web/src/core/rename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type { VSDocument, VSElement } from './model'

/**
* Umbenennen (Refactoring): eine Variable an allen Stellen umbenennen, im Java-Code und im Struktogramm.
* Genau dieselbe Schreibweise (`zahl` ≠ `Zahl`), nur ganze Wörter; Text in "…"/'…' und Kommentare bleiben.
*/

export const javaKeywords = new Set(
(
'abstract assert boolean break byte case catch char class const continue default do double else enum extends final ' +
'finally float for goto if implements import instanceof int interface long native new package private protected public ' +
'return short static strictfp super switch synchronized this throw throws transient try void volatile while ' +
'true false null var record yield'
).split(' '),
)

const wordChar = /[\p{L}\p{N}_$]/u
const wordStart = /[\p{L}_$]/u

export interface Word {
from: number
to: number
name: string
}

/** Alle Wörter im Code außerhalb von Zeichenketten, Zeichen und Kommentaren. */
export function codeWords(text: string): Word[] {
const words: Word[] = []
let i = 0
while (i < text.length) {
const c = text[i]
if (c === '/' && text[i + 1] === '/') {
while (i < text.length && text[i] !== '\n') i++
} else if (c === '/' && text[i + 1] === '*') {
const end = text.indexOf('*/', i + 2)
i = end < 0 ? text.length : end + 2
} else if (c === '"' || c === "'") {
i++
while (i < text.length && text[i] !== c && text[i] !== '\n') i += text[i] === '\\' ? 2 : 1
i++
} else if (wordStart.test(c)) {
const from = i
while (i < text.length && wordChar.test(text[i])) i++
words.push({ from, to: i, name: text.slice(from, i) })
} else if (/\d/.test(c)) {
// Zahlen wie 1e5 oder 10L gehören nicht zu einem Namen.
while (i < text.length && wordChar.test(text[i])) i++
} else i++
}
return words
}

/** Stellen, an denen die Variable `name` vorkommt (nicht hinter einem Punkt: `feld.length` ist keine Variable `length`). */
export function nameRanges(text: string, name: string): Word[] {
return codeWords(text).filter((w) => w.name === name && !afterDot(text, w.from))
}

function afterDot(text: string, from: number): boolean {
let i = from - 1
while (i >= 0 && (text[i] === ' ' || text[i] === '\t')) i--
return text[i] === '.'
}

/** Wort unter dem Cursor (auch direkt dahinter), wenn es ein umbenennbarer Name ist. */
export function nameAt(text: string, pos: number): Word | undefined {
const w = codeWords(text).find((w) => pos >= w.from && pos <= w.to)
if (!w || javaKeywords.has(w.name) || afterDot(text, w.from)) return undefined
return w
}

/** Text mit umbenannter Variable und Anzahl der geänderten Stellen. */
export function renameInText(text: string, from: string, to: string): { text: string; count: number } {
const ranges = nameRanges(text, from)
let out = ''
let last = 0
for (const r of ranges) {
out += text.slice(last, r.from) + to
last = r.to
}
return { text: out + text.slice(last), count: ranges.length }
}

/** Alle Texte des Struktogramms (Kopfzeile, Elemente, Fallnamen). */
function documentTexts(doc: VSDocument): string[] {
const texts = [doc.caption]
const walk = (elements: VSElement[]) => {
for (const el of elements) {
texts.push(el.textLines.join('\n'))
if (el.loopBody) walk(el.loopBody)
for (const c of el.cases ?? []) {
texts.push(c.name)
walk(c.elements)
}
}
}
walk(doc.elements)
return texts
}

export function renameInDocument(doc: VSDocument, from: string, to: string): { doc: VSDocument; count: number } {
let count = 0
const rename = (s: string) => {
const r = renameInText(s, from, to)
count += r.count
return r.text
}
const walk = (elements: VSElement[]): VSElement[] =>
elements.map((el) => ({
...el,
textLines: rename(el.textLines.join('\n')).split('\n'),
...(el.loopBody ? { loopBody: walk(el.loopBody) } : {}),
...(el.cases ? { cases: el.cases.map((c) => ({ name: rename(c.name), elements: walk(c.elements) })) } : {}),
}))
const next = { ...doc, caption: rename(doc.caption), elements: walk(doc.elements) }
return { doc: next, count }
}

/**
* Namen, die wie Variablen aussehen: keine Schlüsselwörter, keine Methoden `name(`, keine Klassen
* (`Scanner sc`, `Math.max`) und nichts hinter einem Punkt. Reihenfolge wie im Text.
*/
export function variableNames(texts: string[]): string[] {
const names = new Set<string>()
const skip = new Set<string>()
const declared = new Set<string>()
for (const text of texts) {
const words = codeWords(text).filter((w) => !afterDot(text, w.from))
words.forEach((w, i) => {
// „ø“ = leere Anweisung im Struktogramm, kein Name.
if (javaKeywords.has(w.name) || w.name === 'ø') return
const rest = text.slice(w.to)
const next = words[i + 1]
const between = next ? text.slice(w.to, next.from) : ''
// `Scanner sc`, `String[] namen`: das erste Wort ist ein Typ, das zweite die Variable.
const isType = next !== undefined && !javaKeywords.has(next.name) && /^(?:\s*\[\s*\])*[ \t]+$/.test(between)
if (isType && /^\s*(?:[=;,):]|$)/.test(text.slice(next.to))) declared.add(next.name)
const isClass = /^\p{Lu}/u.test(w.name) && /^\s*\./.test(rest)
if (/^\s*\(/.test(rest) || isClass || isType) skip.add(w.name)
else names.add(w.name)
})
}
return [...names].filter((n) => !skip.has(n) || declared.has(n))
}

/** Alle Wörter im Struktogramm, auch Methoden und Klassen: Der neue Name soll keinen davon treffen. */
export function documentWords(doc: VSDocument): string[] {
return documentTexts(doc).flatMap((t) => codeWords(t).map((w) => w.name))
}

export function countInDocument(doc: VSDocument, name: string): number {
return documentTexts(doc).reduce((n, t) => n + nameRanges(t, name).length, 0)
}

export function documentVariableNames(doc: VSDocument): string[] {
return variableNames(documentTexts(doc))
}

export function elementVariableNames(el: VSElement): string[] {
return variableNames([el.textLines.join('\n')])
}

export type RenameCheck = { ok: true; warning?: 'exists' } | { ok: false; reason: 'empty' | 'same' | 'invalid' | 'keyword' }

/** Neuen Namen prüfen; `existing` = Namen, die schon vorkommen. */
export function checkNewName(from: string, to: string, existing: Iterable<string>): RenameCheck {
if (to === '') return { ok: false, reason: 'empty' }
if (to === from) return { ok: false, reason: 'same' }
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(to)) return { ok: false, reason: 'invalid' }
if (javaKeywords.has(to)) return { ok: false, reason: 'keyword' }
for (const n of existing) if (n === to) return { ok: true, warning: 'exists' }
return { ok: true }
}
13 changes: 13 additions & 0 deletions web/src/i18n/ar.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "كتل الشيفرة ملوّنة";
"web_code_scopes_hint" = "الصنف والدالة والحلقة والتفرّع في مربعات خاصة";
"web_pseudocode" = "شبه كود";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "إعادة تسمية المتغير …";
"web_rename_title" = "إعادة تسمية المتغير";
"web_rename_old" = "الاسم";
"web_rename_new" = "الاسم الجديد";
"web_rename_hint" = "يغيّر كل موضع بهذه الكتابة تمامًا (number ليس Number)، في الشيفرة وفي المخطط الهيكلي. يبقى النص بين \"…\" والتعليقات كما هي.";
"web_rename_count_fmt" = "يظهر %d مرة.";
"web_rename_exists_fmt" = "«%@» موجود بالفعل: بعدها يصبح المتغيران متغيرًا واحدًا.";
"web_rename_invalid" = "يبدأ الاسم بحرف ويحتوي على حروف وأرقام و _ فقط.";
"web_rename_keyword_fmt" = "«%@» كلمة مفتاحية في Java.";
"web_rename_apply" = "إعادة تسمية";
"web_rename_cancel" = "إلغاء";
13 changes: 13 additions & 0 deletions web/src/i18n/da.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "Farvede kodeblokke";
"web_code_scopes_hint" = "Klasse, metode, løkke, forgrening i egne felter";
"web_pseudocode" = "Pseudokode";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Omdøb variabel …";
"web_rename_title" = "Omdøb variabel";
"web_rename_old" = "Navn";
"web_rename_new" = "Nyt navn";
"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.";
"web_rename_count_fmt" = "Forekommer %d gange.";
"web_rename_exists_fmt" = "“%@” findes allerede: bagefter ville begge være én variabel.";
"web_rename_invalid" = "Et navn begynder med et bogstav og indeholder kun bogstaver, cifre og _.";
"web_rename_keyword_fmt" = "“%@” er et Java-nøgleord.";
"web_rename_apply" = "Omdøb";
"web_rename_cancel" = "Annuller";
12 changes: 12 additions & 0 deletions web/src/i18n/de.strings
Original file line number Diff line number Diff line change
Expand Up @@ -741,3 +741,15 @@
"web_visuflow_status_edge" = "Pfeil gewählt. Löschen entfernt ihn.";
"web_visuflow_status_connect" = "Loslassen über dem Symbol, zu dem der Pfeil führt.";
"web_visuflow_edit_label" = "Text im Symbol";
/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Variable umbenennen …";
"web_rename_title" = "Variable umbenennen";
"web_rename_old" = "Name";
"web_rename_new" = "Neuer Name";
"web_rename_hint" = "Ändert jede Stelle mit genau dieser Schreibweise (zahl ist nicht Zahl), im Code und im Struktogramm. Text in \"…\" und Kommentare bleiben.";
"web_rename_count_fmt" = "Kommt %d-mal vor.";
"web_rename_exists_fmt" = "„%@“ gibt es schon: Danach wären beide eine Variable.";
"web_rename_invalid" = "Ein Name beginnt mit einem Buchstaben und enthält nur Buchstaben, Ziffern und _.";
"web_rename_keyword_fmt" = "„%@“ ist ein Java-Schlüsselwort.";
"web_rename_apply" = "Umbenennen";
"web_rename_cancel" = "Abbrechen";
13 changes: 13 additions & 0 deletions web/src/i18n/el.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "Χρωματιστά μπλοκ κώδικα";
"web_code_scopes_hint" = "Κλάση, μέθοδος, βρόχος, διακλάδωση σε δικά τους πλαίσια";
"web_pseudocode" = "Ψευδοκώδικας";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Μετονομασία μεταβλητής …";
"web_rename_title" = "Μετονομασία μεταβλητής";
"web_rename_old" = "Όνομα";
"web_rename_new" = "Νέο όνομα";
"web_rename_hint" = "Αλλάζει κάθε σημείο με ακριβώς αυτή τη γραφή (number δεν είναι Number), στον κώδικα και στο δομόγραμμα. Το κείμενο σε \"…\" και τα σχόλια μένουν.";
"web_rename_count_fmt" = "Εμφανίζεται %d φορές.";
"web_rename_exists_fmt" = "Το «%@» υπάρχει ήδη: μετά θα ήταν και τα δύο μία μεταβλητή.";
"web_rename_invalid" = "Ένα όνομα αρχίζει με γράμμα και περιέχει μόνο γράμματα, ψηφία και _.";
"web_rename_keyword_fmt" = "Το «%@» είναι λέξη-κλειδί της Java.";
"web_rename_apply" = "Μετονομασία";
"web_rename_cancel" = "Άκυρο";
12 changes: 12 additions & 0 deletions web/src/i18n/en.strings
Original file line number Diff line number Diff line change
Expand Up @@ -741,3 +741,15 @@
"web_visuflow_status_edge" = "Arrow selected. Delete removes it.";
"web_visuflow_status_connect" = "Let go over the symbol the arrow leads to.";
"web_visuflow_edit_label" = "Text in the symbol";
/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Rename variable …";
"web_rename_title" = "Rename variable";
"web_rename_old" = "Name";
"web_rename_new" = "New name";
"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.";
"web_rename_count_fmt" = "Used %d times.";
"web_rename_exists_fmt" = "“%@” already exists: afterwards both would be one variable.";
"web_rename_invalid" = "A name starts with a letter and contains only letters, digits and _.";
"web_rename_keyword_fmt" = "“%@” is a Java keyword.";
"web_rename_apply" = "Rename";
"web_rename_cancel" = "Cancel";
13 changes: 13 additions & 0 deletions web/src/i18n/es.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "Bloques de código en color";
"web_code_scopes_hint" = "Clase, método, bucle, condición en sus propias cajas";
"web_pseudocode" = "Pseudocódigo";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Renombrar variable …";
"web_rename_title" = "Renombrar variable";
"web_rename_old" = "Nombre";
"web_rename_new" = "Nombre nuevo";
"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.";
"web_rename_count_fmt" = "Aparece %d veces.";
"web_rename_exists_fmt" = "«%@» ya existe: después ambas serían una sola variable.";
"web_rename_invalid" = "Un nombre empieza con una letra y solo contiene letras, cifras y _.";
"web_rename_keyword_fmt" = "«%@» es una palabra clave de Java.";
"web_rename_apply" = "Renombrar";
"web_rename_cancel" = "Cancelar";
13 changes: 13 additions & 0 deletions web/src/i18n/fr.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "Blocs de code en couleur";
"web_code_scopes_hint" = "Classe, méthode, boucle, condition dans leurs propres cadres";
"web_pseudocode" = "Pseudo-code";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Renommer la variable …";
"web_rename_title" = "Renommer la variable";
"web_rename_old" = "Nom";
"web_rename_new" = "Nouveau nom";
"web_rename_hint" = "Modifie chaque endroit avec exactement cette écriture (nombre n’est pas Nombre), dans le code et dans le structogramme. Le texte entre \"…\" et les commentaires restent.";
"web_rename_count_fmt" = "Apparaît %d fois.";
"web_rename_exists_fmt" = "« %@ » existe déjà : ensuite les deux seraient une seule variable.";
"web_rename_invalid" = "Un nom commence par une lettre et ne contient que des lettres, des chiffres et _.";
"web_rename_keyword_fmt" = "« %@ » est un mot-clé Java.";
"web_rename_apply" = "Renommer";
"web_rename_cancel" = "Annuler";
13 changes: 13 additions & 0 deletions web/src/i18n/he.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "בלוקי קוד צבעוניים";
"web_code_scopes_hint" = "מחלקה, מתודה, לולאה, הסתעפות בתיבות משלהן";
"web_pseudocode" = "פסאודו-קוד";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "שינוי שם המשתנה …";
"web_rename_title" = "שינוי שם המשתנה";
"web_rename_old" = "שם";
"web_rename_new" = "שם חדש";
"web_rename_hint" = "משנה כל מקום עם בדיוק הכתיב הזה (number אינו Number), בקוד ובתרשים המבנה. טקסט בתוך \"…\" והערות נשארים.";
"web_rename_count_fmt" = "מופיע %d פעמים.";
"web_rename_exists_fmt" = "„%@” כבר קיים: אחר כך שניהם יהיו משתנה אחד.";
"web_rename_invalid" = "שם מתחיל באות ומכיל רק אותיות, ספרות ו־_.";
"web_rename_keyword_fmt" = "„%@” היא מילת מפתח של Java.";
"web_rename_apply" = "שינוי שם";
"web_rename_cancel" = "ביטול";
13 changes: 13 additions & 0 deletions web/src/i18n/it.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "Blocchi di codice colorati";
"web_code_scopes_hint" = "Classe, metodo, ciclo, selezione in riquadri propri";
"web_pseudocode" = "Pseudocodice";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "Rinomina variabile …";
"web_rename_title" = "Rinomina variabile";
"web_rename_old" = "Nome";
"web_rename_new" = "Nuovo nome";
"web_rename_hint" = "Cambia ogni punto con esattamente questa scrittura (numero non è Numero), nel codice e nello strutturogramma. Il testo tra \"…\" e i commenti restano.";
"web_rename_count_fmt" = "Compare %d volte.";
"web_rename_exists_fmt" = "«%@» esiste già: dopo le due sarebbero una sola variabile.";
"web_rename_invalid" = "Un nome inizia con una lettera e contiene solo lettere, cifre e _.";
"web_rename_keyword_fmt" = "«%@» è una parola chiave di Java.";
"web_rename_apply" = "Rinomina";
"web_rename_cancel" = "Annulla";
13 changes: 13 additions & 0 deletions web/src/i18n/ja.strings
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
"web_code_scopes" = "コードブロックを色分け";
"web_code_scopes_hint" = "クラス、メソッド、ループ、分岐をそれぞれの枠で表示";
"web_pseudocode" = "疑似コード";

/* Variable umbenennen (Refactoring) */
"web_rename_menu" = "変数の名前を変更 …";
"web_rename_title" = "変数の名前を変更";
"web_rename_old" = "名前";
"web_rename_new" = "新しい名前";
"web_rename_hint" = "つづりが完全に同じ箇所をすべて、コードと構造図で変更します(number と Number は別です)。\"…\" 内の文字列とコメントは変わりません。";
"web_rename_count_fmt" = "%d 回使われています。";
"web_rename_exists_fmt" = "「%@」はすでにあります:変更すると 2 つの変数が 1 つになります。";
"web_rename_invalid" = "名前は文字で始まり、文字・数字・_ だけを含みます。";
"web_rename_keyword_fmt" = "「%@」は Java のキーワードです。";
"web_rename_apply" = "名前を変更";
"web_rename_cancel" = "キャンセル";
Loading
Loading