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 @@ -8,6 +8,7 @@ The format is loosely inspired by Keep a Changelog.

### Added

- **Web 0.12.3, `System.out.print` in VisuStruct:** `System.out.print(…)` (without a new line) works in the simulation, in Programmieren and in the code export (JavaScript `process.stdout.write`, Python `print(…, end="")`); an input right after an output no longer shows the extra question `name:`, after `System.out.print` it stays on the same line (#187).
- **Web 0.12.2, code colours like Eclipse:** string literals are blue instead of red (red only marks errors), keywords purple bold including `int`, `boolean`, `true`, `false`, `null`, comments green, numbers ochre, class names black. Applies to Programmieren and VisuObject.
- **Web 0.12.1, missing Scanner/import is an error:** in Programmieren, `eingabe.next()` without `Scanner eingabe = new Scanner(System.in);` is marked red and ▶ does not start (like javac); the VisuObject debugger now reports a missing `import java.util.…` for Scanner, ArrayList, HashMap, Arrays etc. (#188)
- **Web 0.12.0, VisuObject sequence diagram:** a third tab next to class and object diagram draws a sequence diagram live from the debugger run: a lifeline per object (`s1 : Spieler`) or class (static), call arrows with arguments, dashed return arrows with the value, «create» arrows to boxes that start where the object is created, activation bars, the latest arrow in yellow while paused; clicking an arrow shows the line in the code.
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.12.2",
"version": "0.12.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
10 changes: 10 additions & 0 deletions web/src/core/codeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,16 @@ function emitPrintLineIfNeeded(
intVariables: Set<string>,
stringVariables: Set<string>,
): string | undefined {
// `System.out.print(…)`: Ausgabe ohne neue Zeile.
const print = /^System\s*\.\s*out\s*\.\s*print\s*\(([\s\S]*)\)$/.exec(stripTrailingSemicolon(raw).trim())
if (print) {
const value = print[1].trim()
switch (target) {
case 'java': return sp(indent) + 'System.out.print(' + value + ');\n'
case 'javascript': return sp(indent) + 'process.stdout.write(String(' + value + '));\n'
case 'python': return sp(indent) + 'print(' + (value === '' ? '' : pythonConcatenation(value, intVariables, stringVariables) + ', ') + 'end="")\n'
}
}
const expr = expressionAfterOutputDiagramPrefix(raw)
if (expr === undefined) {
// Leere Ausgabe (`output:` ohne Text): eine Leerzeile.
Expand Down
38 changes: 32 additions & 6 deletions web/src/core/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface InputRequest {
variableName: string
valueType: SimValueType
prompt: string
/** Wie in Java hinter `System.out.print("Note: ")`: Die Eingabe steht in derselben Zeile wie diese Ausgabe. */
continuesLine?: boolean
}

/** Meldungen der Simulation, unabhängig von der Sprache. */
Expand Down Expand Up @@ -94,6 +96,8 @@ export interface SimState {
stepIndex: number
variables: Map<string, SimValue>
outputLines: string[]
/** Die letzte Ausgabe war `System.out.print(…)`: Die nächste Ausgabe schreibt in derselben Zeile weiter. */
outputLineOpen?: boolean
inputRequest?: InputRequest
/** Fehler zur letzten Eingabe (z. B. keine Zahl). */
inputErrorNotice?: SimNotice
Expand Down Expand Up @@ -977,6 +981,7 @@ export class SimulationEngine {
reset(document?: VSDocument): void {
if (document) this.steps = buildSteps(document.elements)
this.state = freshState()
this.afterOutput = false
this.nextFrame = 1
this.callDepth = 0
this.frames.clear()
Expand Down Expand Up @@ -1300,21 +1305,34 @@ export class SimulationEngine {

// MARK: Anweisungen

/** Die letzte Anweisung war eine Ausgabe (für die Frage einer folgenden Eingabe). */
private afterOutput = false

private executeStatement(rawText: string): void {
const state = this.state
const text = rawText.trim()
const lower = text.toLowerCase()
const afterOutput = this.afterOutput
this.afterOutput = false
if (lower.startsWith('input:')) {
state.inputRequest = this.parseInputRequest(text)
state.inputRequest = this.parseInputRequest(text, afterOutput)
// Mit Enter endet die Zeile.
state.outputLineOpen = false
state.inputErrorNotice = undefined
return
}

if (lower.startsWith('output:') || lower.startsWith('print:')) {
const expression = text.slice(lower.startsWith('output:') ? 'output:'.length : 'print:'.length)
// `System.out.print(…)` schreibt ohne neue Zeile, `System.out.println(…)` wie `output:`.
const javaOut = /^System\s*\.\s*out\s*\.\s*(println|print)\s*\(([\s\S]*)\)\s*;?$/u.exec(text)
if (lower.startsWith('output:') || lower.startsWith('print:') || javaOut) {
const expression = javaOut ? javaOut[2] : text.slice(lower.startsWith('output:') ? 'output:'.length : 'print:'.length)
const rendered = this.peek(() => this.renderOutputExpression(expression))
const output = this.evaluateOutputExpression(expression)
state.outputLines.push(output)
// `println()` ohne Text: nur eine neue Zeile.
const output = expression.trim() === '' ? '' : this.evaluateOutputExpression(expression)
if (state.outputLineOpen && state.outputLines.length) state.outputLines[state.outputLines.length - 1] += output
else state.outputLines.push(output)
state.outputLineOpen = javaOut?.[1] === 'print'
this.afterOutput = true
state.lastTrace = `${rendered} -> ${output}`
return
}
Expand Down Expand Up @@ -1554,7 +1572,12 @@ export class SimulationEngine {
return { variableName: name, valueType }
}

private parseInputRequest(text: string): InputRequest {
/**
* Ohne eigene Frage steht `name:` vor der Eingabe. Kommt direkt davor eine Ausgabe, fragt diese schon (wie in Java,
* z. B. `System.out.println("Note für Fach " + fach[i])`): Dann steht nichts davor, nach `System.out.print(…)` steht
* die Eingabe in derselben Zeile.
*/
private parseInputRequest(text: string, afterOutput = false): InputRequest {
const afterColon = text.slice('input:'.length).trim()
// Wie Swift `split(maxSplits: 2)`: zwei Wörter, dann der Rest.
const match = /^(\S+)(?:\s+(\S+))?(?:\s+([\s\S]*))?$/u.exec(afterColon)
Expand All @@ -1575,6 +1598,9 @@ export class SimulationEngine {
}
if (!isAssignableTarget(target)) throw unsupported(text)
const prompt = unquotedString(promptRaw) ?? promptRaw
if (prompt === '' && afterOutput) {
return this.state.outputLineOpen ? { variableName: target, valueType, prompt: '', continuesLine: true } : { variableName: target, valueType, prompt: '' }
}
return { variableName: target, valueType, prompt: prompt === '' ? `${target}:` : prompt }
}

Expand Down
19 changes: 16 additions & 3 deletions web/src/ui/codePanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,33 @@ export function javaScriptRunnerCode(js: string): string {
const workerSource = `
let answer
let lines = []
const flush = () => {
// Text von \`process.stdout.write\` ohne neue Zeile (wie \`System.out.print\`).
let partial = ''
const flush = (all) => {
if (all && partial) {
lines.push(partial)
partial = ''
}
if (lines.length) postMessage({ type: 'log', lines })
lines = []
}
const send = (type, text) => {
flush()
flush(true)
postMessage({ type, text })
}
const fmt = (a) => (typeof a === 'string' ? a : JSON.stringify(a))
// Ausgaben gesammelt senden, sonst bremsen viele kleine Nachrichten (z. B. in Endlosschleifen) die Seite aus.
console.log = (...args) => {
lines.push(args.map(fmt).join(' '))
lines.push(partial + args.map(fmt).join(' '))
partial = ''
if (lines.length >= 500) flush()
}
const process = { stdout: { write: (text) => {
const parts = (partial + String(text)).split('\\n')
partial = parts.pop()
lines.push(...parts)
if (lines.length >= 500) flush()
} } }
setInterval(flush, 50)
const __visuStructPrompt = (label) => new Promise((resolve, reject) => {
answer = (value) => (value === null ? reject(new Error(${JSON.stringify(cancelMessage())})) : resolve(value))
Expand Down
10 changes: 6 additions & 4 deletions web/src/ui/liveCodePanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,13 +625,15 @@ export class LiveCodePanel {
}

/** `typed`: Eingabezeile mit Frage und der getippten Antwort (farbig). */
private printNewLines(typed?: { prompt: string; value: string }) {
private printNewLines(typed?: { prompt: string; value: string; continuesLine?: boolean }) {
const run = this.run
if (!run) return
const lines = run.engine.state.outputLines
for (; run.printed < lines.length; run.printed++) this.consoleLine(lines[run.printed])
if (typed) {
const div = this.consoleLine(typed.prompt ? `${typed.prompt} ` : '')
// Nach `System.out.print("Note: ")` steht die Eingabe in derselben Zeile.
const last = this.consoleLines.lastElementChild
const div = typed.continuesLine && last instanceof HTMLElement ? last : this.consoleLine(typed.prompt ? `${typed.prompt} ` : '')
const value = document.createElement('span')
value.className = 'typed'
value.textContent = typed.value
Expand Down Expand Up @@ -671,10 +673,10 @@ export class LiveCodePanel {
const run = this.run
if (!run?.engine.state.inputRequest) return
const field = this.consoleForm.querySelector('input')!
const prompt = run.engine.state.inputRequest.prompt
const { prompt, continuesLine } = run.engine.state.inputRequest
run.engine.provideInput(field.value)
if (run.engine.state.inputRequest) return this.askInput()
this.printNewLines({ prompt, value: field.value })
this.printNewLines({ prompt, value: field.value, continuesLine })
field.value = ''
this.consoleForm.hidden = true
if (!run.stepping) return this.continueRun(false)
Expand Down
7 changes: 5 additions & 2 deletions web/src/ui/simulationPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,12 @@ export class SimulationPanel {
const shouldResume = this.isPlaying || this.wasPlayingBeforeInput
this.pause()
const prompt = this.engine.state.inputRequest?.prompt ?? ''
const continuesLine = this.engine.state.inputRequest?.continuesLine
this.engine.provideInput(field.value)
if (!this.engine.state.inputRequest) {
this.transcript.push({ text: prompt ? `${prompt} ` : '', typed: field.value })
const last = this.transcript[this.transcript.length - 1]
if (continuesLine && last && last.typed === undefined) last.typed = field.value
else this.transcript.push({ text: prompt ? `${prompt} ` : '', typed: field.value })
field.value = ''
}
this.onHighlight(this.engine.currentStepPath)
Expand Down Expand Up @@ -321,7 +324,7 @@ export class SimulationPanel {
const { state, canStep } = this.engine
this.onTranscript({
lines: this.transcript,
pending: state.inputRequest?.prompt,
pending: state.inputRequest?.continuesLine ? undefined : state.inputRequest?.prompt,
notice: state.notice ? noticeText(state.notice) : undefined,
error: state.isHalted,
})
Expand Down
43 changes: 43 additions & 0 deletions web/test/javaParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { parseLiveJava } from '../src/core/javaParser'
import { documentFromJava, mergeLiveJava, plainLiveJava, type JavaForm } from '../src/core/liveSource'
import { ElementType, makeElement, type VSDocument } from '../src/core/model'
import { decodeDocument } from '../src/core/xmlCodec'
import { SimulationEngine } from '../src/core/simulation'
import { generateCode } from '../src/core/codeGenerator'
import { indentJava } from '../src/core/javaIndent'

const labels: LiveLabels = { condition: 'Bedingung', selector: 'Wert', code: 'Dein Code', start: 'Startwert', step: 'Schritt', end: 'Ende' }
Expand Down Expand Up @@ -152,6 +154,47 @@ describe('Java tippen → Struktogramm', () => {
expect(keys('System.out.println("Scanner");')).toEqual([])
})

it('System.out.print schreibt ohne neue Zeile, auch mit Array-Fach', () => {
const d = doc(
'int[] intAllgemeinFach = {3, 5};\n' +
'for (int i = 0; i < intAllgemeinFach.length; i++) {\n' +
' System.out.print(intAllgemeinFach[i] + ": ");\n' +
' System.out.println("ok");\n' +
'}\n' +
'System.out.print("A");\n' +
'System.out.print("B");\n' +
'System.out.println();',
)
const engine = new SimulationEngine(d)
let steps = 0
while (engine.canStep && steps++ < 1000) engine.step()
expect(engine.message).not.toMatch(/Nicht unterstützte|Unsupported/u)
expect(engine.state.outputLines).toEqual(['3: ok', '5: ok', 'AB'])
const java = generateCode(d, 'java')
expect(java).toContain('System.out.print(intAllgemeinFach[i] + ": ");')
expect(generateCode(d, 'javascript')).toContain('process.stdout.write(String(intAllgemeinFach[i] + ": "));')
expect(generateCode(d, 'python')).toContain('print(str(intAllgemeinFach[i]) + ": ", end="")')
})

it('Eingabe nach einer Ausgabe fragt nicht noch einmal nach dem Variablennamen', () => {
const ask = (src: string) => {
const engine = new SimulationEngine(doc(src))
while (engine.canStep && !engine.state.inputRequest) engine.step()
return { request: engine.state.inputRequest, lines: engine.state.outputLines }
}
const fach = 'String[] fach = {"Mathe"};\nint[] note = new int[1];\nint i = 0;\n'
expect(ask(fach + 'System.out.println("Note für Fach " + fach[i]);\nnote[i] = scanner.nextInt();')).toEqual({
request: { variableName: 'note[i]', valueType: 'int', prompt: '' },
lines: ['Note für Fach Mathe'],
})
expect(ask(fach + 'System.out.print(fach[i] + ": ");\nnote[i] = scanner.nextInt();')).toEqual({
request: { variableName: 'note[i]', valueType: 'int', prompt: '', continuesLine: true },
lines: ['Mathe: '],
})
// Ohne Ausgabe davor bleibt die Frage `name:`.
expect(ask(fach + 'note[i] = scanner.nextInt();').request?.prompt).toBe('note[i]:')
})

it('Aufruf eines Unterprogramms aus einem anderen Reiter', () => {
expect(parse('int y = quadrat(4);', new Set(['quadrat'])).elements[0].type).toBe(ElementType.aufruf)
expect(parse('int y = quadrat(4);').elements[0].type).toBe(ElementType.anweisung)
Expand Down

Large diffs are not rendered by default.

Loading
Loading