Skip to content

Commit 1827669

Browse files
authored
Get code-editor diagnostics from runtime (#1346)
1 parent 2370a30 commit 1827669

File tree

7 files changed

+81
-29
lines changed

7 files changed

+81
-29
lines changed

spx-gui/src/components/editor/code-editor/code-editor.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Emitter from '@/utils/emitter'
44
import { insertSpaces, tabSize } from '@/utils/spx/highlighter'
55
import type { I18n } from '@/utils/i18n'
66
import { packageSpx } from '@/utils/spx'
7-
import type { Runtime } from '@/models/runtime'
7+
import { RuntimeOutputKind, type Runtime } from '@/models/runtime'
88
import type { Project } from '@/models/project'
99
import { Copilot } from './copilot'
1010
import { DocumentBase } from './document-base'
@@ -59,7 +59,9 @@ import {
5959
type WorkspaceDiagnostics,
6060
type TextDocumentDiagnostics,
6161
fromLSPDiagnostic,
62-
isTextDocumentStageCode
62+
isTextDocumentStageCode,
63+
DiagnosticSeverity,
64+
textDocumentIdEq
6365
} from './common'
6466
import { TextDocument, createTextDocument } from './text-document'
6567
import { type Monaco } from './monaco'
@@ -127,17 +129,54 @@ class ResourceReferencesProvider implements IResourceReferencesProvider {
127129

128130
class DiagnosticsProvider
129131
extends Emitter<{
130-
didChangeDiagnostics: []
132+
didChangeDiagnostics: void
131133
}>
132134
implements IDiagnosticsProvider
133135
{
134136
constructor(
135137
private runtime: Runtime,
136-
private lspClient: SpxLSPClient
138+
private lspClient: SpxLSPClient,
139+
private project: Project
137140
) {
138141
super()
142+
143+
this.addDisposer(
144+
this.runtime.on('didChangeOutput', () => {
145+
this.emit('didChangeDiagnostics')
146+
})
147+
)
139148
}
140-
private adaptDiagnosticRange({ start, end }: Range, textDocument: ITextDocument) {
149+
150+
private adaptRuntimeDiagnosticRange({ start, end }: Range, textDocument: ITextDocument) {
151+
// Expand the range to whole line because the range from runtime is not accurate.
152+
// TODO: it's a workaround, should be fixed in the runtime (ispx) side
153+
if (start.line !== end.line) return { start, end }
154+
const line = textDocument.getLineContent(start.line)
155+
const leadingSpaces = line.match(/^\s*/)?.[0] ?? ''
156+
const lineStartPos: Position = { line: start.line, column: leadingSpaces.length + 1 }
157+
const lineEndPos: Position = { line: start.line, column: line.length + 1 }
158+
return { start: lineStartPos, end: lineEndPos }
159+
}
160+
161+
private getRuntimeDiagnostics(ctx: DiagnosticsContext) {
162+
const { outputs, filesHash } = this.runtime
163+
if (filesHash !== this.project.filesHash) return []
164+
const diagnostics: Diagnostic[] = []
165+
for (const output of outputs) {
166+
if (output.kind !== RuntimeOutputKind.Error) continue
167+
if (output.source == null) continue
168+
if (!textDocumentIdEq(output.source.textDocument, ctx.textDocument.id)) continue
169+
const range = this.adaptRuntimeDiagnosticRange(output.source.range, ctx.textDocument)
170+
diagnostics.push({
171+
message: output.message,
172+
range,
173+
severity: DiagnosticSeverity.Error
174+
})
175+
}
176+
return diagnostics
177+
}
178+
179+
private adaptLSDiagnosticRange({ start, end }: Range, textDocument: ITextDocument) {
141180
// make sure the range is not empty, so that the diagnostic info can be displayed as inline decorations
142181
// TODO: it's a workaround, should be fixed in the server side
143182
if (positionEq(start, end)) {
@@ -165,8 +204,8 @@ class DiagnosticsProvider
165204
}
166205
return { start, end }
167206
}
168-
async provideDiagnostics(ctx: DiagnosticsContext): Promise<Diagnostic[]> {
169-
// TODO: get diagnostics from runtime. https://github.com/goplus/builder/issues/1256
207+
208+
private async getLSDiagnostics(ctx: DiagnosticsContext) {
170209
const diagnostics: Diagnostic[] = []
171210
const report = await this.lspClient.textDocumentDiagnostic({
172211
textDocument: ctx.textDocument.id
@@ -175,11 +214,15 @@ class DiagnosticsProvider
175214
throw new Error(`Report kind ${report.kind} not supported`)
176215
for (const item of report.items) {
177216
const diagnostic = fromLSPDiagnostic(item)
178-
const range = this.adaptDiagnosticRange(diagnostic.range, ctx.textDocument)
217+
const range = this.adaptLSDiagnosticRange(diagnostic.range, ctx.textDocument)
179218
diagnostics.push({ ...diagnostic, range })
180219
}
181220
return diagnostics
182221
}
222+
223+
async provideDiagnostics(ctx: DiagnosticsContext): Promise<Diagnostic[]> {
224+
return [...this.getRuntimeDiagnostics(ctx), ...(await this.getLSDiagnostics(ctx))]
225+
}
183226
}
184227

185228
class HoverProvider implements IHoverProvider {
@@ -466,7 +509,7 @@ export class CodeEditor extends Disposable {
466509
this.completionProvider = new CompletionProvider(this.lspClient, this.documentBase)
467510
this.contextMenuProvider = new ContextMenuProvider(this.lspClient, this.documentBase)
468511
this.resourceReferencesProvider = new ResourceReferencesProvider(this.lspClient)
469-
this.diagnosticsProvider = new DiagnosticsProvider(this.runtime, this.lspClient)
512+
this.diagnosticsProvider = new DiagnosticsProvider(this.runtime, this.lspClient, this.project)
470513
this.hoverProvider = new HoverProvider(this.lspClient, this.documentBase)
471514
}
472515

spx-gui/src/components/editor/code-editor/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export interface ITextDocument
198198
getOffsetAt(position: Position): number
199199
getPositionAt(offset: number): Position
200200
getValueInRange(range: Range): string
201+
getLineContent(line: number): string
201202
getWordAtPosition(position: Position): WordAtPosition | null
202203
getDefaultRange(position: Position): Range
203204
pushEdits(edits: TextEdit[]): void

spx-gui/src/components/editor/code-editor/ui/diagnostics/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type DiagnosticsContext = BaseContext
1010

1111
export interface IDiagnosticsProvider
1212
extends Emitter<{
13-
didChangeDiagnostics: []
13+
didChangeDiagnostics: void
1414
}> {
1515
provideDiagnostics(ctx: DiagnosticsContext): Promise<Diagnostic[]>
1616
}
@@ -55,7 +55,7 @@ export class DiagnosticsController extends Disposable {
5555

5656
this.addDisposer(
5757
watch(
58-
() => this.ui.project.filesHash,
58+
() => [this.ui.project.filesHash, this.ui.activeTextDocument],
5959
() => refreshDiagnostics(),
6060
{ immediate: true }
6161
)

spx-gui/src/components/editor/preview/InPlaceRunner.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ watch(
113113
const projectRunner = await untilNotNull(projectRunnerRef)
114114
signal.throwIfAborted()
115115
editorCtx.runtime.clearOutputs()
116-
projectRunner.run().then(() => {
117-
editorCtx.runtime.setRunning({ mode: 'debug', initializing: false })
116+
projectRunner.run().then((filesHash) => {
117+
editorCtx.runtime.setRunning({ mode: 'debug', initializing: false }, filesHash)
118118
})
119119
signal.addEventListener('abort', () => {
120120
projectRunner.stop()
@@ -128,8 +128,8 @@ defineExpose({
128128
editorCtx.runtime.setRunning({ mode: 'debug', initializing: true })
129129
const projectRunner = await untilNotNull(projectRunnerRef)
130130
editorCtx.runtime.clearOutputs()
131-
await projectRunner.rerun()
132-
editorCtx.runtime.setRunning({ mode: 'debug', initializing: false })
131+
const filesHash = await projectRunner.rerun()
132+
editorCtx.runtime.setRunning({ mode: 'debug', initializing: false }, filesHash)
133133
}
134134
})
135135
</script>

spx-gui/src/components/project/runner/v1/ProjectRunnerV1.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,27 @@ onUnmounted(() => {
6565
registered.onStopped()
6666
})
6767
68-
async function getProjectZipData(signal?: AbortSignal) {
68+
async function getProjectData(signal?: AbortSignal) {
6969
const zip = new JSZip()
70-
const [, files] = await props.project.export()
70+
const [{ filesHash }, files] = await props.project.export()
7171
signal?.throwIfAborted()
7272
Object.entries(files).forEach(([path, file]) => {
7373
if (file == null) return
7474
zip.file(path, getZipEntry(file))
7575
})
76-
return zip.generateAsync({ type: 'arraybuffer' })
76+
const zipped = await zip.generateAsync({ type: 'arraybuffer' })
77+
return { filesHash, zipped }
7778
}
7879
7980
defineExpose({
8081
async run(signal?: AbortSignal) {
8182
loading.value = true
8283
registered.onStart()
83-
zipData.value = await getProjectZipData(signal)
84+
const projectData = await getProjectData(signal)
85+
zipData.value = projectData.zipped
8486
signal?.throwIfAborted()
8587
await until(() => !loading.value)
88+
return projectData.filesHash
8689
},
8790
stop() {
8891
zipData.value = null

spx-gui/src/components/project/runner/v2/ProjectRunnerV2.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ watch(iframeRef, (iframe) => {
5656
5757
async function getProjectData() {
5858
const zip = new JSZip()
59-
const [, files] = await props.project.export()
59+
const [{ filesHash }, files] = await props.project.export()
6060
Object.entries(files).forEach(([path, file]) => {
6161
if (file != null) zip.file(path, toNativeFile(file))
6262
})
63-
return zip.generateAsync({ type: 'arraybuffer' })
63+
const zipped = await zip.generateAsync({ type: 'arraybuffer' })
64+
return { filesHash, zipped }
6465
}
6566
6667
function withLog(methodName: string, promise: Promise<unknown>) {
@@ -113,9 +114,10 @@ defineExpose({
113114
signal?.throwIfAborted()
114115
const projectData = await getProjectData()
115116
signal?.throwIfAborted()
116-
await withLog('startGame', iframeWindow.startGame(projectData, assetURLs))
117+
await withLog('startGame', iframeWindow.startGame(projectData.zipped, assetURLs))
117118
signal?.throwIfAborted()
118119
loading.value = false
120+
return projectData.filesHash
119121
},
120122
async stop() {
121123
const iframeWindow = iframeWindowRef.value
@@ -125,7 +127,7 @@ defineExpose({
125127
},
126128
async rerun() {
127129
await this.stop()
128-
await this.run()
130+
return this.run()
129131
}
130132
})
131133
</script>

spx-gui/src/models/runtime.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,18 @@ export class Runtime extends Emitter<{
3939
}> {
4040
running: RunningState = { mode: 'none' }
4141

42-
setRunning(running: RunningState) {
42+
setRunning(running: RunningState, filesHash?: string) {
4343
this.running = running
44+
if (running.mode === 'debug' && running.initializing === false) {
45+
if (filesHash == null) throw new Error('filesHash is required when running in debug mode')
46+
this.filesHash = filesHash
47+
}
4448
}
4549

50+
/** Outputs of last debugging */
4651
outputs: RuntimeOutput[] = []
47-
48-
getOutputs(): RuntimeOutput[] {
49-
return this.outputs
50-
}
52+
/** Project files' hash of last debugging */
53+
filesHash: string | null = null
5154

5255
addOutput(output: RuntimeOutput) {
5356
this.outputs.push(output)
@@ -75,7 +78,7 @@ export class Runtime extends Emitter<{
7578
() => reactiveThis.project.filesHash,
7679
async (_, oldHash) => {
7780
if (oldHash == null) return
78-
await until(() => reactiveThis.running.mode === 'none')
81+
await until(() => reactiveThis.running.mode !== 'debug')
7982
reactiveThis.clearOutputs()
8083
}
8184
)

0 commit comments

Comments
 (0)