From 7fadd7e1e11cc83c8585e24c00a6365311f1cdea Mon Sep 17 00:00:00 2001 From: nighca Date: Thu, 9 Jan 2025 17:13:25 +0800 Subject: [PATCH] Error diagnostics handling for renaming, formating & running --- spx-gui/src/components/asset/index.ts | 26 ++++++--- .../asset/library/AssetAddModal.vue | 2 +- spx-gui/src/components/common/RenameModal.vue | 21 +++++++- .../editor/code-editor/FormatButton.vue | 4 +- .../editor/code-editor/code-editor.ts | 30 +++++++---- .../components/editor/code-editor/common.ts | 21 +++++++- .../components/editor/code-editor/context.ts | 32 +++++++++-- .../code-editor/document-base/spx/index.ts | 12 ++--- .../editor/code-editor/lsp/index.ts | 5 ++ .../editor/code-editor/ui/CodeEditorUI.vue | 8 +-- .../editor/preview/EditorPreview.vue | 49 ++++++++++++----- spx-gui/src/components/ui/UIButton.vue | 10 +++- .../components/ui/dialog/UIConfirmDialog.vue | 15 +++++- spx-gui/src/utils/utils.test.ts | 54 ++++++++++++++++++- spx-gui/src/utils/utils.ts | 16 ++++++ tools/spxls/internal/server/command.go | 3 -- tools/spxls/internal/server/rename.go | 3 -- 17 files changed, 254 insertions(+), 57 deletions(-) diff --git a/spx-gui/src/components/asset/index.ts b/spx-gui/src/components/asset/index.ts index a5659dea1..62796a431 100644 --- a/spx-gui/src/components/asset/index.ts +++ b/spx-gui/src/components/asset/index.ts @@ -20,7 +20,7 @@ import type { Widget } from '@/models/widget' import RenameModal from '../common/RenameModal.vue' import SoundRecorderModal from '../editor/sound/SoundRecorderModal.vue' import { useEditorCtx } from '../editor/EditorContextProvider.vue' -import { useCodeEditorCtx } from '../editor/code-editor/context' +import { useCodeEditorCtx, useRenameWarning } from '../editor/code-editor/context' import { getResourceIdentifier } from '../editor/code-editor/common' import AssetLibraryModal from './library/AssetLibraryModal.vue' import AssetAddModal from './library/AssetAddModal.vue' @@ -199,6 +199,7 @@ export function useRenameSprite() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameSprite(sprite: Sprite) { return invokeRenameModal({ target: { @@ -213,7 +214,8 @@ export function useRenameSprite() { sprite.setName(newName) }) }, - inputTip: assetName.spriteNameTip + inputTip: assetName.spriteNameTip, + warning: await getRenameWarning() } }) } @@ -223,6 +225,7 @@ export function useRenameSound() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameSound(sound: Sound) { return invokeRenameModal({ target: { @@ -237,7 +240,8 @@ export function useRenameSound() { sound.setName(newName) }) }, - inputTip: assetName.soundNameTip + inputTip: assetName.soundNameTip, + warning: await getRenameWarning() } }) } @@ -247,6 +251,7 @@ export function useRenameCostume() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameCostume(costume: Costume) { return invokeRenameModal({ target: { @@ -261,7 +266,8 @@ export function useRenameCostume() { costume.setName(newName) }) }, - inputTip: assetName.costumeNameTip + inputTip: assetName.costumeNameTip, + warning: await getRenameWarning() } }) } @@ -271,6 +277,7 @@ export function useRenameBackdrop() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameBackdrop(backdrop: Backdrop) { return invokeRenameModal({ target: { @@ -285,7 +292,8 @@ export function useRenameBackdrop() { backdrop.setName(newName) }) }, - inputTip: assetName.backdropNameTip + inputTip: assetName.backdropNameTip, + warning: await getRenameWarning() } }) } @@ -295,6 +303,7 @@ export function useRenameAnimation() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameAnimation(animation: Animation) { return invokeRenameModal({ target: { @@ -309,7 +318,8 @@ export function useRenameAnimation() { animation.setName(newName) }) }, - inputTip: assetName.animationNameTip + inputTip: assetName.animationNameTip, + warning: await getRenameWarning() } }) } @@ -319,6 +329,7 @@ export function useRenameWidget() { const editorCtx = useEditorCtx() const codeEditorCtx = useCodeEditorCtx() const invokeRenameModal = useModal(RenameModal) + const getRenameWarning = useRenameWarning() return async function renameWidget(widget: Widget) { return invokeRenameModal({ target: { @@ -333,7 +344,8 @@ export function useRenameWidget() { widget.setName(newName) }) }, - inputTip: assetName.widgetNameTip + inputTip: assetName.widgetNameTip, + warning: await getRenameWarning() } }) } diff --git a/spx-gui/src/components/asset/library/AssetAddModal.vue b/spx-gui/src/components/asset/library/AssetAddModal.vue index 9d1522501..b438a47f0 100644 --- a/spx-gui/src/components/asset/library/AssetAddModal.vue +++ b/spx-gui/src/components/asset/library/AssetAddModal.vue @@ -146,7 +146,7 @@ const handleSubmit = useMessageHandle( }), content: t({ en: `The ${assetTypeName} you uploaded [${form.value.name}] is the same as the existing ${assetTypeName} [${assets[0].displayName}] in the asset library. Are you sure you want to add this ${assetTypeName} to the asset library?`, - zh: `您上传的${assetTypeName}「${form.value.name}」与已存在于素材库中的${assetTypeName}「${assets[0].displayName}」内容相同。是否确认需要将此${assetTypeName}添加到素材库中?` + zh: `你上传的${assetTypeName}「${form.value.name}」与已存在于素材库中的${assetTypeName}「${assets[0].displayName}」内容相同。是否确认需要将此${assetTypeName}添加到素材库中?` }) }) } diff --git a/spx-gui/src/components/common/RenameModal.vue b/spx-gui/src/components/common/RenameModal.vue index ce65afdc2..be9dc6883 100644 --- a/spx-gui/src/components/common/RenameModal.vue +++ b/spx-gui/src/components/common/RenameModal.vue @@ -8,12 +8,14 @@ export interface IRenameTarget { applyName(newName: string): Promise /** Tip for new name input */ inputTip: LocaleMessage + /** Extra warning message */ + warning: LocaleMessage | null } diff --git a/spx-gui/src/components/editor/code-editor/code-editor.ts b/spx-gui/src/components/editor/code-editor/code-editor.ts index df229109a..3f58d1406 100644 --- a/spx-gui/src/components/editor/code-editor/code-editor.ts +++ b/spx-gui/src/components/editor/code-editor/code-editor.ts @@ -41,7 +41,6 @@ import { selection2Range, toLSPPosition, fromLSPRange, - fromLSPSeverity, positionEq, type ITextDocument, type Range, @@ -55,7 +54,10 @@ import { type CommandArgs, getTextDocumentId, containsPosition, - makeBasicMarkdownString + makeBasicMarkdownString, + type WorkspaceDiagnostics, + type TextDocumentDiagnostics, + fromLSPDiagnostic } from './common' import { TextDocument, createTextDocument } from './text-document' import { type Monaco } from './monaco' @@ -123,14 +125,9 @@ class DiagnosticsProvider if (report.kind !== lsp.DocumentDiagnosticReportKind.Full) throw new Error(`Report kind ${report.kind} not supported`) for (const item of report.items) { - const severity = item.severity == null ? null : fromLSPSeverity(item.severity) - if (severity === null) continue - const range = this.adaptDiagnosticRange(fromLSPRange(item.range), ctx.textDocument) - diagnostics.push({ - range, - severity, - message: item.message - }) + const diagnostic = fromLSPDiagnostic(item) + const range = this.adaptDiagnosticRange(diagnostic.range, ctx.textDocument) + diagnostics.push({ ...diagnostic, range }) } return diagnostics } @@ -470,6 +467,19 @@ export class CodeEditor extends Disposable { } } + async diagnosticWorkspace(): Promise { + const diagnosticReport = await this.lspClient.workspaceDiagnostic({ previousResultIds: [] }) + const items: TextDocumentDiagnostics[] = [] + for (const report of diagnosticReport.items) { + if (report.kind === 'unchanged') continue // For now, we support 'full' reports only + items.push({ + textDocument: { uri: report.uri }, + diagnostics: report.items.map(fromLSPDiagnostic) + }) + } + return { items } + } + /** Update code for renaming */ async rename(id: TextDocumentIdentifier, position: Position, newName: string) { const edit = await this.lspClient.textDocumentRename({ diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts index 8c6db8b5a..4ca776cb0 100644 --- a/spx-gui/src/components/editor/code-editor/common.ts +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -165,6 +165,15 @@ export type Diagnostic = { message: string } +export type TextDocumentDiagnostics = { + textDocument: TextDocumentIdentifier + diagnostics: Diagnostic[] +} + +export type WorkspaceDiagnostics = { + items: TextDocumentDiagnostics[] +} + export interface WordAtPosition { word: string startColumn: number @@ -422,14 +431,22 @@ export function fromLSPRange(range: lsp.Range): Range { } } -export function fromLSPSeverity(severity: lsp.DiagnosticSeverity): DiagnosticSeverity | null { +export function fromLSPSeverity(severity: lsp.DiagnosticSeverity | undefined): DiagnosticSeverity { switch (severity) { case lsp.DiagnosticSeverity.Error: return DiagnosticSeverity.Error case lsp.DiagnosticSeverity.Warning: return DiagnosticSeverity.Warning default: - return null + return DiagnosticSeverity.Error + } +} + +export function fromLSPDiagnostic(diagnostic: lsp.Diagnostic): Diagnostic { + return { + range: fromLSPRange(diagnostic.range), + severity: fromLSPSeverity(diagnostic.severity), + message: diagnostic.message } } diff --git a/spx-gui/src/components/editor/code-editor/context.ts b/spx-gui/src/components/editor/code-editor/context.ts index 392e60af3..06ed5a3bd 100644 --- a/spx-gui/src/components/editor/code-editor/context.ts +++ b/spx-gui/src/components/editor/code-editor/context.ts @@ -5,7 +5,13 @@ import { getHighlighter } from '@/utils/spx/highlighter' import { composeQuery, useQuery, type QueryRet } from '@/utils/query' import type { Project } from '@/models/project' import type { Runtime } from '@/models/runtime' -import { type Position, type ResourceIdentifier, type TextDocumentIdentifier } from './common' +import { + DiagnosticSeverity, + type Position, + type ResourceIdentifier, + type TextDocumentIdentifier, + type WorkspaceDiagnostics +} from './common' import { type ICodeEditorUI } from './ui/code-editor-ui' import { TextDocument } from './text-document' import { getMonaco, type monaco, type Monaco } from './monaco' @@ -19,9 +25,10 @@ export type CodeEditorCtx = { getTextDocument: (id: TextDocumentIdentifier) => TextDocument | null formatTextDocument(id: TextDocumentIdentifier): Promise formatWorkspace(): Promise - /** Update code for renaming */ + diagnosticWorkspace(): Promise + /** Update code for (symbol) renaming */ rename(id: TextDocumentIdentifier, position: Position, newName: string): Promise - /** Update code for resource renaming, should be called before model name update */ + /** Update code for resource renaming, should be called before model name update. */ renameResource(resource: ResourceIdentifier, newName: string): Promise } @@ -33,6 +40,21 @@ export function useCodeEditorCtx() { return ctx } +// There may be errors in the project code when renaming resource/symbol, which may cause some references to be updated incorrectly. +// This function is used to provide warning for errors in the project code when renaming: we notify the user to check the code and update the references manually. +export function useRenameWarning() { + const codeEditorCtx = useCodeEditorCtx() + return async function getRenameWarning() { + const r = await codeEditorCtx.diagnosticWorkspace() + const hasError = r.items.some((i) => i.diagnostics.some((d) => d.severity === DiagnosticSeverity.Error)) + if (!hasError) return null + return { + en: 'There are errors in the project code. Some references may not be updated automatically. You can check the code and update them manually after renaming.', + zh: '当前项目代码中存在错误,部分引用可能不会自动更新,你可以在重命名后检查代码并手动修改。' + } + } +} + // copied from https://github.com/goplus/vscode-gop/blob/dc065c1701ec54a719747ff41d2054e9ed200eb8/languages/gop.language-configuration.json const spxLanguageConfiguration: monaco.languages.LanguageConfiguration = { comments: { @@ -182,6 +204,10 @@ export function useProvideCodeEditorCtx( if (editorRef.value == null) throw new Error('Code editor not initialized') return editorRef.value.formatWorkspace() }, + diagnosticWorkspace() { + if (editorRef.value == null) throw new Error('Code editor not initialized') + return editorRef.value.diagnosticWorkspace() + }, rename(id: TextDocumentIdentifier, position: Position, newName: string) { if (editorRef.value == null) throw new Error('Code editor not initialized') return editorRef.value.rename(id, position, newName) diff --git a/spx-gui/src/components/editor/code-editor/document-base/spx/index.ts b/spx-gui/src/components/editor/code-editor/document-base/spx/index.ts index 35e812d6d..83be6e15d 100644 --- a/spx-gui/src/components/editor/code-editor/document-base/spx/index.ts +++ b/spx-gui/src/components/editor/code-editor/document-base/spx/index.ts @@ -616,7 +616,7 @@ export const setXYpos: DefinitionDocumentationItem = { insertText: 'setXYpos ${1:x}, ${2:y}', overview: 'setXYpos x, y', detail: makeBasicMarkdownString({ - en: 'Set the sprite\'s position, e.g., `setXYpos 100, 100`', + en: "Set the sprite's position, e.g., `setXYpos 100, 100`", zh: '设置精灵位置,如:`setXYpos 100, 100`' }) } @@ -631,7 +631,7 @@ export const changeXYpos: DefinitionDocumentationItem = { insertText: 'changeXYpos ${1:dX}, ${2:dY}', overview: 'changeXYpos dX, dY', detail: makeBasicMarkdownString({ - en: 'Change the sprite\'s position, e.g., `changeXYpos 10, 20` changing X position by 10 and Y position by 20', + en: "Change the sprite's position, e.g., `changeXYpos 10, 20` changing X position by 10 and Y position by 20", zh: '改变精灵位置,如:`changeXYpos 10, 10` 使水平位置增加 10,垂直位置增加 10' }) } @@ -661,7 +661,7 @@ export const setXpos: DefinitionDocumentationItem = { insertText: 'setXpos ${1:x}', overview: 'setXpos x', detail: makeBasicMarkdownString({ - en: 'Set the sprite\'s X position, e.g., `setXpos 100`', + en: "Set the sprite's X position, e.g., `setXpos 100`", zh: '设置精灵的水平位置,如:`setXpos 100`' }) } @@ -676,7 +676,7 @@ export const changeXpos: DefinitionDocumentationItem = { insertText: 'changeXpos ${1:dX}', overview: 'changeXpos dX', detail: makeBasicMarkdownString({ - en: 'Change the sprite\'s X position, e.g., `changeXpos 10` changing X position by 10', + en: "Change the sprite's X position, e.g., `changeXpos 10` changing X position by 10", zh: '改变精灵的水平位置,如:`changeXpos 10` 使水平位置增加 10' }) } @@ -706,7 +706,7 @@ export const setYpos: DefinitionDocumentationItem = { insertText: 'setYpos ${1:y}', overview: 'setYpos y', detail: makeBasicMarkdownString({ - en: 'Set the sprite\'s Y position, e.g., `setYpos 100`', + en: "Set the sprite's Y position, e.g., `setYpos 100`", zh: '设置精灵的垂直位置,如:`setYpos 100`' }) } @@ -721,7 +721,7 @@ export const changeYpos: DefinitionDocumentationItem = { insertText: 'changeYpos ${1:dY}', overview: 'changeYpos dY', detail: makeBasicMarkdownString({ - en: 'Change the sprite\'s Y position, e.g., `changeYpos 10` changing Y position by 10', + en: "Change the sprite's Y position, e.g., `changeYpos 10` changing Y position by 10", zh: '改变精灵的垂直位置,如:`changeYpos 10` 使垂直位置增加 10' }) } diff --git a/spx-gui/src/components/editor/code-editor/lsp/index.ts b/spx-gui/src/components/editor/code-editor/lsp/index.ts index b21eb6c73..b9ad30aef 100644 --- a/spx-gui/src/components/editor/code-editor/lsp/index.ts +++ b/spx-gui/src/components/editor/code-editor/lsp/index.ts @@ -129,6 +129,11 @@ export class SpxLSPClient extends Disposable { return spxlc.request(lsp.DocumentDiagnosticRequest.method, params) } + async workspaceDiagnostic(params: lsp.WorkspaceDiagnosticParams): Promise { + const spxlc = await this.prepareRequest() + return spxlc.request(lsp.WorkspaceDiagnosticRequest.method, params) + } + async textDocumentHover(params: lsp.HoverParams): Promise { const spxlc = await this.prepareRequest() return spxlc.request(lsp.HoverRequest.method, params) diff --git a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue index 12d69ab9b..f3644fef9 100644 --- a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue +++ b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue @@ -34,7 +34,7 @@ import { isWidget } from '@/models/widget' import { useModal } from '@/components/ui' import RenameModal from '@/components/common/RenameModal.vue' import { useEditorCtx } from '../../EditorContextProvider.vue' -import { useCodeEditorCtx } from '../context' +import { useCodeEditorCtx, useRenameWarning } from '../context' import { getResourceModel, getTextDocumentId, @@ -70,8 +70,9 @@ const renameCostume = useRenameCostume() const renameBackdrop = useRenameBackdrop() const renameAnimation = useRenameAnimation() const renameWidget = useRenameWidget() +const getRenameWarning = useRenameWarning() -function rename(textDocumentId: TextDocumentIdentifier, position: Position, range: Range): Promise { +async function rename(textDocumentId: TextDocumentIdentifier, position: Position, range: Range): Promise { const textDocument = codeEditorCtx.getTextDocument(textDocumentId) if (textDocument == null) throw new Error(`Text document (${textDocumentId.uri}) not found`) const name = textDocument.getValueInRange(range) @@ -83,7 +84,8 @@ function rename(textDocumentId: TextDocumentIdentifier, position: Position, rang editorCtx.project.history.doAction({ name: { en: 'Rename', zh: '重命名' } }, () => codeEditorCtx.rename(textDocumentId, position, newName) ), - inputTip: getGopIdentifierNameTip() + inputTip: getGopIdentifierNameTip(), + warning: await getRenameWarning() } }) } diff --git a/spx-gui/src/components/editor/preview/EditorPreview.vue b/spx-gui/src/components/editor/preview/EditorPreview.vue index 46a76980e..760aaebc8 100644 --- a/spx-gui/src/components/editor/preview/EditorPreview.vue +++ b/spx-gui/src/components/editor/preview/EditorPreview.vue @@ -91,10 +91,13 @@