diff --git a/spx-gui/src/components/editor/code-editor/CodeEditor.vue b/spx-gui/src/components/editor/code-editor/CodeEditor.vue index d817ad844..ceffe7b27 100644 --- a/spx-gui/src/components/editor/code-editor/CodeEditor.vue +++ b/spx-gui/src/components/editor/code-editor/CodeEditor.vue @@ -16,6 +16,7 @@ import { type ResourceReference, type ResourceReferencesContext } from './ui' +import type { DefinitionDocumentationItem, DefinitionIdentifier } from './common' const editorCtx = useEditorCtx() @@ -27,8 +28,14 @@ function initialize(ui: ICodeEditorUI) { ui.registerAPIReferenceProvider({ async provideAPIReference(ctx, position) { - console.warn('TODO', ctx, position, documentBase, spxlc) - return [] + console.warn('TODO: get api references from LS', ctx, position, spxlc) + const ids: DefinitionIdentifier[] = [ + { package: 'github.com/goplus/spx', name: 'onStart' }, + { package: 'github.com/goplus/spx', name: 'Sprite.setXYpos' }, + { name: 'for_iterate (TODO)' } + ] + const documentations = await Promise.all(ids.map((id) => documentBase.getDocumentation(id))) + return documentations as DefinitionDocumentationItem[] } }) diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts index 99e32fb0b..f8e9e37bf 100644 --- a/spx-gui/src/components/editor/code-editor/common.ts +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -1,3 +1,5 @@ +import type { LocaleMessage } from '@/utils/i18n' + export type Position = { line: number column: number @@ -36,8 +38,8 @@ export enum DefinitionKind { Function, /** Function or method for reading data */ Read, - /** Function or method for causing effect, e.g., writing data */ - Effect, + /** Function or method for executing commands, e.g., move a sprite */ + Command, /** Function or method for listening to event */ Listen, /** Language defined statements, e.g., `for { ... }` */ @@ -75,40 +77,85 @@ export type DefinitionIdentifier = { overloadIndex?: number } +export function stringifyDefinitionId(defId: DefinitionIdentifier): string { + if (defId.name == null) { + if (defId.package == null) throw new Error('package expected for ' + defId) + return defId.package + } + const suffix = defId.overloadIndex == null ? '' : `[${defId.overloadIndex}]` + if (defId.package == null) return defId.name + suffix + return defId.package + '|' + defId.name + suffix +} + /** * Model for text document * Similar to https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.ITextModel.html */ -interface TextDocument { +export interface ITextDocument { id: TextDocumentIdentifier getOffsetAt(position: Position): number getPositionAt(offset: number): Position getValueInRange(range: IRange): string } -export type MarkdownString = { +export type MarkdownStringFlag = 'basic' | 'advanced' + +/** + * Markdown string with MDX support. + * We use flag to distinguish different types of Markdown string. + * Different types of Markdown string expect different rendering behaviors, especially for custom components support in MDX. + */ +export type MarkdownString = { + flag?: F /** Markdown string with MDX support. */ - value: string + value: string | LocaleMessage } +/** + * Markdown string with support of basic MDX components. + * Typically, it is used in `DefinitionDocumentationItem.detail`. + */ +export type BasicMarkdownString = MarkdownString<'basic'> + +export function makeBasicMarkdownString(value: string | LocaleMessage): BasicMarkdownString { + return { value, flag: 'basic' } +} + +/** + * Markdown string with support of advanced MDX components, e.g., `OverviewWrapper`, `Detail` (which reads data from `DocumentBase` & render with `DetailWrapper`). + * Typically, it is used in `CompletionItem.documentation` or `Hover.contents`. + */ +export type AdvancedMarkdownString = MarkdownString<'advanced'> + export type Icon = string /** - * Documentation for a definition. Typically: + * Documentation string for a definition. Typically: * ```mdx - * func turn(dDirection float64) - * - * Turn with given direction change. - * + * func turn(dDirection float64) + * * ``` */ -export type Documentation = MarkdownString - -export type DocumentationItem = { +export type DefinitionDocumentationString = AdvancedMarkdownString + +export const categoryEvent = 'event' +export const categoryEventGame = [categoryEvent, 'game'] +export const categoryMotion = 'motion' +export const categoryMotionPosition = [categoryMotion, 'position'] +export const categoryControl = 'control' +export const categoryControlFlow = [categoryControl, 'flow'] + +export type DefinitionDocumentationItem = { + /** For classification when listed in a group, e.g., `[["event", "game"]]` */ + categories: string[][] kind: DefinitionKind definition: DefinitionIdentifier + /** Text to insert when completion / snippet is applied */ insertText: string - documentation: Documentation + /** Brief explanation for the definition, typically the signature string */ + overview: string + /** Detailed explanation for the definition, overview not included */ + detail: BasicMarkdownString } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -130,7 +177,7 @@ export type Action = { export type BaseContext = { /** Current active text document */ - textDocument: TextDocument + textDocument: ITextDocument /** Signal to abort long running operations */ signal: AbortSignal } diff --git a/spx-gui/src/components/editor/code-editor/document-base.ts b/spx-gui/src/components/editor/code-editor/document-base.ts deleted file mode 100644 index 7ac055690..000000000 --- a/spx-gui/src/components/editor/code-editor/document-base.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DefinitionIdentifier, DocumentationItem } from './common' - -export class DocumentBase { - async getDocumentation(definition: DefinitionIdentifier): Promise { - console.warn('TODO', definition) - return null - } -} diff --git a/spx-gui/src/components/editor/code-editor/document-base/gop.ts b/spx-gui/src/components/editor/code-editor/document-base/gop.ts new file mode 100644 index 000000000..e0e7da5ad --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/document-base/gop.ts @@ -0,0 +1,15 @@ +import { + DefinitionKind, + type DefinitionDocumentationItem, + makeBasicMarkdownString, + categoryControlFlow +} from '../common' + +export const forIterate: DefinitionDocumentationItem = { + categories: [categoryControlFlow], + kind: DefinitionKind.Statement, + definition: { name: 'for_iterate (TODO)' }, + insertText: 'for ${1:i}, ${2:v} <- ${3:set} {\n\t${4:}\n}', + overview: 'for i, v <- set {} (TODO)', + detail: makeBasicMarkdownString({ en: 'Iterate within given set', zh: '遍历指定集合' }) +} diff --git a/spx-gui/src/components/editor/code-editor/document-base/index.ts b/spx-gui/src/components/editor/code-editor/document-base/index.ts new file mode 100644 index 000000000..54cd8b56b --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/document-base/index.ts @@ -0,0 +1,23 @@ +import { type DefinitionIdentifier, type DefinitionDocumentationItem, stringifyDefinitionId } from '../common' +import * as gopDefinitions from './gop' +import * as spxDefinitions from './spx' + +export class DocumentBase { + private storage = new Map() + + constructor() { + ;[...Object.values(gopDefinitions), ...Object.values(spxDefinitions)].forEach((d) => { + this.addDocumentation(d) + }) + } + + private addDocumentation(documentation: DefinitionDocumentationItem) { + const key = stringifyDefinitionId(documentation.definition) + this.storage.set(key, documentation) + } + + async getDocumentation(defId: DefinitionIdentifier): Promise { + const key = stringifyDefinitionId(defId) + return this.storage.get(key) ?? null + } +} diff --git a/spx-gui/src/components/editor/code-editor/document-base/spx.ts b/spx-gui/src/components/editor/code-editor/document-base/spx.ts new file mode 100644 index 000000000..767e6b051 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/document-base/spx.ts @@ -0,0 +1,33 @@ +import { + DefinitionKind, + type DefinitionDocumentationItem, + makeBasicMarkdownString, + categoryEventGame, + categoryMotionPosition +} from '../common' + +const packageSpx = 'github.com/goplus/spx' + +export const onStart: DefinitionDocumentationItem = { + categories: [categoryEventGame], + kind: DefinitionKind.Listen, + definition: { + package: packageSpx, + name: 'onStart' + }, + insertText: 'onStart => {\n\t${1}\n}', + overview: 'func onStart(callback func())', + detail: makeBasicMarkdownString({ en: 'Listen to game start', zh: '游戏开始时执行' }) +} + +export const setXYpos: DefinitionDocumentationItem = { + categories: [categoryMotionPosition], + kind: DefinitionKind.Command, + definition: { + package: packageSpx, + name: 'Sprite.setXYpos' + }, + insertText: 'setXYpos ${1:X}, ${2:Y}', + overview: 'func setXYpos(x, y float64)', + detail: makeBasicMarkdownString({ en: 'Move to given position', zh: '移动到指定位置' }) +} diff --git a/spx-gui/src/components/editor/code-editor/ui/APIReference.vue b/spx-gui/src/components/editor/code-editor/ui/APIReference.vue new file mode 100644 index 000000000..4380d82e1 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/APIReference.vue @@ -0,0 +1,33 @@ + + + + + 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 54d36226a..25a8f8ccc 100644 --- a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue +++ b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue @@ -3,6 +3,7 @@ import { onUnmounted } from 'vue' import type { Project } from '@/models/project' import { type ICodeEditorUI, CodeEditorUI } from '.' import MonacoEditor, { type Editor, type Monaco } from './MonacoEditor.vue' +import APIReference from './APIReference.vue' const props = defineProps<{ project: Project @@ -14,14 +15,19 @@ const emit = defineEmits<{ const ui = new CodeEditorUI(props.project) +const ctrl = new AbortController() +onUnmounted(() => { + ctrl.abort() +}) + function handleMonaco(monaco: Monaco) { - ui.initializeMonaco(monaco) + ui.initMonaco(monaco) } function handleEditor(editor: Editor) { - ui.initializeEditor(editor) + ui.initEditor(editor) emit('initialize', ui) - ui.initialize() + ui.init(ctrl.signal) } onUnmounted(() => { @@ -31,6 +37,7 @@ onUnmounted(() => { @@ -44,6 +51,10 @@ onUnmounted(() => { justify-content: stretch; } +.api-reference { + flex: 0 0 160px; +} + .monaco-editor { flex: 1 1 0; } diff --git a/spx-gui/src/components/editor/code-editor/ui/MarkdownView.vue b/spx-gui/src/components/editor/code-editor/ui/MarkdownView.vue new file mode 100644 index 000000000..6bd7c23bb --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/MarkdownView.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/spx-gui/src/components/editor/code-editor/ui/api-reference.ts b/spx-gui/src/components/editor/code-editor/ui/api-reference.ts index c92d3d518..c3a13820a 100644 --- a/spx-gui/src/components/editor/code-editor/ui/api-reference.ts +++ b/spx-gui/src/components/editor/code-editor/ui/api-reference.ts @@ -1,9 +1,34 @@ -import { type BaseContext, type Position, type DocumentationItem } from '../common' +import { shallowReactive } from 'vue' +import { Disposable } from '@/utils/disposable' +import { type BaseContext, type Position, type DefinitionDocumentationItem } from '../common' +import type { CodeEditorUI } from '.' -export type APIReferenceItem = DocumentationItem +export type APIReferenceItem = DefinitionDocumentationItem export type APIReferenceContext = BaseContext export interface IAPIReferenceProvider { provideAPIReference(ctx: APIReferenceContext, position: Position): Promise } + +export class APIReference extends Disposable { + items: APIReferenceItem[] = shallowReactive([]) + + private provider: IAPIReferenceProvider | null = null + registerProvider(provider: IAPIReferenceProvider) { + this.provider = provider + } + + constructor(private ui: CodeEditorUI) { + super() + } + + async init(signal: AbortSignal) { + const context: APIReferenceContext = { + textDocument: this.ui.activeTextDocument!, + signal + } + const items = await this.provider!.provideAPIReference(context, { line: 0, column: 0 }) + this.items.splice(0, this.items.length, ...items) + } +} diff --git a/spx-gui/src/components/editor/code-editor/ui/completion.ts b/spx-gui/src/components/editor/code-editor/ui/completion.ts index 75915c888..e3a30425a 100644 --- a/spx-gui/src/components/editor/code-editor/ui/completion.ts +++ b/spx-gui/src/components/editor/code-editor/ui/completion.ts @@ -1,4 +1,4 @@ -import { DefinitionKind, type BaseContext, type Documentation, type Position } from '../common' +import { DefinitionKind, type BaseContext, type DefinitionDocumentationString, type Position } from '../common' export type CompletionContext = BaseContext @@ -7,7 +7,7 @@ export type CompletionItemKind = DefinitionKind export type CompletionItem = { label: string kind: CompletionItemKind - documentation: Documentation + documentation: DefinitionDocumentationString } export interface ICompletionProvider { diff --git a/spx-gui/src/components/editor/code-editor/ui/copilot.ts b/spx-gui/src/components/editor/code-editor/ui/copilot.ts index fc4c16620..5b366deef 100644 --- a/spx-gui/src/components/editor/code-editor/ui/copilot.ts +++ b/spx-gui/src/components/editor/code-editor/ui/copilot.ts @@ -4,7 +4,7 @@ import { type DefinitionIdentifier, type CodeSegment, type TextDocumentRange, - type MarkdownString + type BasicMarkdownString } from '../common' import type { Diagnostic } from './diagnostics' @@ -63,7 +63,7 @@ export enum MessageRole { export type ChatMessage = { role: MessageRole - content: MarkdownString + content: BasicMarkdownString } export type Chat = { diff --git a/spx-gui/src/components/editor/code-editor/ui/hover.ts b/spx-gui/src/components/editor/code-editor/ui/hover.ts index 36839c2c3..c8f715ae6 100644 --- a/spx-gui/src/components/editor/code-editor/ui/hover.ts +++ b/spx-gui/src/components/editor/code-editor/ui/hover.ts @@ -1,7 +1,7 @@ -import { type Action, type BaseContext, type Documentation, type Position } from '../common' +import { type Action, type BaseContext, type DefinitionDocumentationString, type Position } from '../common' export type Hover = { - contents: Documentation[] + contents: DefinitionDocumentationString[] actions: Action[] } diff --git a/spx-gui/src/components/editor/code-editor/ui/index.ts b/spx-gui/src/components/editor/code-editor/ui/index.ts index 097f8f736..0e400e23d 100644 --- a/spx-gui/src/components/editor/code-editor/ui/index.ts +++ b/spx-gui/src/components/editor/code-editor/ui/index.ts @@ -3,13 +3,20 @@ import { Disposable } from '@/utils/disposable' import type { Project } from '@/models/project' import { Stage } from '@/models/stage' import type { Sprite } from '@/models/sprite' -import { type Command, type CommandInfo, type IRange, type Position, type TextDocumentIdentifier } from '../common' +import { + type Command, + type CommandInfo, + type IRange, + type Position, + type TextDocumentIdentifier, + type ITextDocument +} from '../common' import type { IHoverProvider } from './hover' import type { ICompletionProvider } from './completion' import type { IResourceReferencesProvider } from './resource-reference' import type { IContextMenuProvider } from './context-menu' import type { IDiagnosticsProvider } from './diagnostics' -import type { IAPIReferenceProvider } from './api-reference' +import { APIReference, type IAPIReferenceProvider } from './api-reference' import type { ICopilot } from './copilot' import type { IFormattingEditProvider } from './formatting' @@ -47,6 +54,25 @@ export interface ICodeEditorUI { open(textDocument: TextDocumentIdentifier, range: IRange): void } +class TextDocument implements ITextDocument { + constructor(public id: TextDocumentIdentifier) {} + + getOffsetAt(position: Position): number { + console.warn('TODO', position) + return 0 + } + + getPositionAt(offset: number): Position { + console.warn('TODO', offset) + return { line: 0, column: 0 } + } + + getValueInRange(range: IRange): string { + console.warn('TODO', range) + return '' + } +} + export class CodeEditorUI extends Disposable implements ICodeEditorUI { registerHoverProvider(provider: IHoverProvider): void { console.warn('TODO', provider) @@ -64,7 +90,7 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { console.warn('TODO', provider) } registerAPIReferenceProvider(provider: IAPIReferenceProvider): void { - console.warn('TODO', provider) + this.apiReference.registerProvider(provider) } registerCopilot(copilot: ICopilot): void { console.warn('TODO', copilot) @@ -88,25 +114,29 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { console.warn('TODO', textDocument, positionOrRange) } + apiReference = new APIReference(this) + constructor(private project: Project) { super() } - monaco?: typeof import('monaco-editor') + monaco: typeof import('monaco-editor') | null = null - initializeMonaco(monaco: typeof import('monaco-editor')) { + initMonaco(monaco: typeof import('monaco-editor')) { // TODO: do monaco configuration here this.monaco = monaco } - editor?: editor.IStandaloneCodeEditor + editor: editor.IStandaloneCodeEditor | null = null - initializeEditor(editor: editor.IStandaloneCodeEditor) { + initEditor(editor: editor.IStandaloneCodeEditor) { // TODO: do editor configuration here this.editor = editor } - initialize() { + activeTextDocument: ITextDocument | null = null + + init(signal: AbortSignal) { const { project, editor } = this if (editor == null) throw new Error('editor expected') @@ -130,5 +160,13 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { selected.setCode(newValue) }) }) + + this.activeTextDocument = new TextDocument({ uri: 'TODO' }) + this.apiReference.init(signal) + } + + dispose() { + this.apiReference.dispose() + super.dispose() } } diff --git a/spx-gui/src/utils/disposable.ts b/spx-gui/src/utils/disposable.ts index 117e584dc..95fce4fad 100644 --- a/spx-gui/src/utils/disposable.ts +++ b/spx-gui/src/utils/disposable.ts @@ -19,7 +19,7 @@ export class Disposable { this.disposers.push(disposer) } - dispose = () => { + dispose() { if (this._isDisposed) return this._isDisposed = true while (this.disposers.length > 0) {