From 78fc2e2fd64b1e9edd678bdaf295fa2bda6d0d5a Mon Sep 17 00:00:00 2001 From: nighca Date: Wed, 20 Nov 2024 14:51:04 +0800 Subject: [PATCH] add new code editor --- .../editor/code-editor/CodeEditor.vue | 114 ++++++++++++++ .../editor/code-editor/FormatButton.vue | 20 +++ .../components/editor/code-editor/common.ts | 143 ++++++++++++++++++ .../components/editor/code-editor/copilot.ts | 8 + .../editor/code-editor/document-base.ts | 9 ++ .../src/components/editor/code-editor/lsp.ts | 41 +++++ .../components/editor/code-editor/runtime.ts | 23 +++ .../editor/code-editor/ui/CodeEditorUI.vue | 50 ++++++ .../editor/code-editor/ui/MonacoEditor.vue | 108 +++++++++++++ .../editor/code-editor/ui/api-reference.ts | 20 +++ .../editor/code-editor/ui/completion.ts | 15 ++ .../editor/code-editor/ui/context-menu.ts | 14 ++ .../editor/code-editor/ui/copilot.ts | 78 ++++++++++ .../editor/code-editor/ui/diagnostics.ts | 22 +++ .../editor/code-editor/ui/formatting.ts | 8 + .../components/editor/code-editor/ui/hover.ts | 12 ++ .../components/editor/code-editor/ui/index.ts | 134 ++++++++++++++++ .../code-editor/ui/resource-reference.ts | 16 ++ .../components/editor/sprite/SpriteEditor.vue | 28 +--- .../components/editor/stage/StageEditor.vue | 25 +-- spx-gui/src/utils/emitter.ts | 46 ++++++ 21 files changed, 889 insertions(+), 45 deletions(-) create mode 100644 spx-gui/src/components/editor/code-editor/CodeEditor.vue create mode 100644 spx-gui/src/components/editor/code-editor/FormatButton.vue create mode 100644 spx-gui/src/components/editor/code-editor/common.ts create mode 100644 spx-gui/src/components/editor/code-editor/copilot.ts create mode 100644 spx-gui/src/components/editor/code-editor/document-base.ts create mode 100644 spx-gui/src/components/editor/code-editor/lsp.ts create mode 100644 spx-gui/src/components/editor/code-editor/runtime.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue create mode 100644 spx-gui/src/components/editor/code-editor/ui/MonacoEditor.vue create mode 100644 spx-gui/src/components/editor/code-editor/ui/api-reference.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/completion.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/context-menu.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/copilot.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/diagnostics.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/formatting.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/hover.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/index.ts create mode 100644 spx-gui/src/components/editor/code-editor/ui/resource-reference.ts create mode 100644 spx-gui/src/utils/emitter.ts diff --git a/spx-gui/src/components/editor/code-editor/CodeEditor.vue b/spx-gui/src/components/editor/code-editor/CodeEditor.vue new file mode 100644 index 000000000..d817ad844 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/CodeEditor.vue @@ -0,0 +1,114 @@ + + + diff --git a/spx-gui/src/components/editor/code-editor/FormatButton.vue b/spx-gui/src/components/editor/code-editor/FormatButton.vue new file mode 100644 index 000000000..50585b140 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/FormatButton.vue @@ -0,0 +1,20 @@ + + + diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts new file mode 100644 index 000000000..331567a81 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -0,0 +1,143 @@ +export type Position = { + line: number + column: number +} + +export type IRange = { + start: Position + end: Position +} + +export type ResourceIdentifier = { + uri: string +} + +export type TextDocumentIdentifier = { + uri: string +} + +export type TextDocumentPosition = { + textDocument: TextDocumentIdentifier + position: Position +} + +export type TextDocumentRange = { + textDocument: TextDocumentIdentifier + range: IRange +} + +export type CodeSegment = { + range: IRange + content: string +} + +export enum DefinitionKind { + /** General function or method */ + Function, + /** Function or method for reading data */ + Read, + /** Function or method for causing effect, e.g., writing data */ + Effect, + /** Function or method for listening to event */ + Listen, + /** Language defined statements, e.g., `for { ... }` */ + Statement, + /** Variable or field definition */ + Variable, + /** Constant definition */ + Constant, + /** Package definition */ + Package +} + +export type DefinitionIdentifier = { + /** + * Full name of source package. + * If not provided, it's assumed to be kind-statement. + * If `main`, it's the current user package. + * Exmples: + * - `fmt` + * - `github.com/goplus/spx` + * - `main` + */ + package?: string + /** + * Exported name of the definition. + * If not provided, it's assumed to be kind-package. + * Examples: + * - `Println` + * - `Sprite` + * - `Sprite.turn` + * - `for_statement_with_single_condition`: kind-statement + */ + name?: string + /** Index in overloads. */ + overloadIndex?: number +} + +/** + * Model for text document + * Similar to https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.ITextModel.html + */ +interface TextDocument { + id: TextDocumentIdentifier + getOffsetAt(position: Position): number + getPositionAt(offset: number): Position + getValueInRange(range: IRange): string +} + +export type MarkdownString = { + /** Markdown string with MDX support. */ + value: string +} + +export type Icon = string + +/** + * Documentation for an identifier, keyword, etc. Typically: + * ```mdx + * func turn(dDirection float64) + * + * Turn with given direction change. + * + * ``` + */ +export type Documentation = MarkdownString + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +interface CommandConstraint {} + +export type Command = string & CommandConstraint +export type CommandHandler = (...args: A) => Promise +export type CommandInfo = { + icon: Icon + title: string + handler: CommandHandler +} + +export type Action = { + title: string + command: Command + arguments: I +} + +export type BaseContext = { + /** Current active text document */ + textDocument: TextDocument + /** Signal to abort long running operations */ + signal: AbortSignal +} + +export type TextEdit = { + range: Range + newText: string +} + +export type WorkspaceEdit = { + changes?: { [uri: string]: TextEdit[] } +} + +// const builtInCommandCopilotChat: Command<[ChatTopic], void> = 'spx.copilot.chat' +// const builtInCommandGoToDefinition: Command<[TextDocumentPosition], void> = 'spx.goToDefinition' +// const builtInCommandRename: Command<[TextDocumentPosition], void> = 'spx.rename' +// const builtInCommandResourceReferenceModify: Command<[TextDocumentRange, ResourceIdentifier], void> = 'spx.resourceReference.modify' diff --git a/spx-gui/src/components/editor/code-editor/copilot.ts b/spx-gui/src/components/editor/code-editor/copilot.ts new file mode 100644 index 000000000..66ff94cb5 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/copilot.ts @@ -0,0 +1,8 @@ +import type { Chat, ChatContext, ChatMessage, ICopilot } from './ui/copilot' + +export class Copilot implements ICopilot { + async getChatCompletion(ctx: ChatContext, chat: Chat): Promise { + console.warn('TODO', ctx, chat) + return null + } +} diff --git a/spx-gui/src/components/editor/code-editor/document-base.ts b/spx-gui/src/components/editor/code-editor/document-base.ts new file mode 100644 index 000000000..260213e36 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/document-base.ts @@ -0,0 +1,9 @@ +import type { DefinitionIdentifier } from './common' +import type { APIReferenceItem } from './ui/api-reference' + +export class DocumentBase { + async getDocumentaion(definition: DefinitionIdentifier): Promise { + console.warn('TODO', definition) + return null + } +} diff --git a/spx-gui/src/components/editor/code-editor/lsp.ts b/spx-gui/src/components/editor/code-editor/lsp.ts new file mode 100644 index 000000000..3c7e1e98c --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/lsp.ts @@ -0,0 +1,41 @@ +import type { Disposer } from '@/utils/disposable' + +// TODO: use implementation from `/tools/spxls/client.ts` +export class Spxlc { + /** + * Sends a request to the language server and waits for response. + * @param method LSP method name. + * @param params Method parameters. + * @returns Promise that resolves with the response. + */ + request(method: string, params?: any): Promise { + console.warn('TODO', method, params) + return null as any + } + + /** + * Sends a notification to the language server (no response expected). + * @param method LSP method name. + * @param params Method parameters. + */ + notify(method: string, params?: any): void { + console.warn('TODO', method, params) + } + + /** + * Registers a handler for server notifications. + * @param method LSP method name. + * @param handler Function to handle the notification. + */ + onNotification(method: string, handler: (params: any) => void): Disposer { + console.warn('TODO', method, handler) + return () => {} + } + + /** + * Cleans up client resources. + */ + dispose(): void { + console.warn('TODO') + } +} diff --git a/spx-gui/src/components/editor/code-editor/runtime.ts b/spx-gui/src/components/editor/code-editor/runtime.ts new file mode 100644 index 000000000..bca8172d0 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/runtime.ts @@ -0,0 +1,23 @@ +import Emitter from '@/utils/emitter' +import type { TextDocumentRange } from './common' + +enum RuntimeOutputKind { + Error, + Log +} + +interface RuntimeOutput { + kind: RuntimeOutputKind + time: number + message: string + source: TextDocumentRange +} + +export class Runtime extends Emitter<{ + didChangeOutput: [] +}> { + async getOutput(): Promise { + console.warn('TODO') + return [] + } +} diff --git a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue new file mode 100644 index 000000000..54d36226a --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/spx-gui/src/components/editor/code-editor/ui/MonacoEditor.vue b/spx-gui/src/components/editor/code-editor/ui/MonacoEditor.vue new file mode 100644 index 000000000..b9159992b --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/MonacoEditor.vue @@ -0,0 +1,108 @@ + + + + + + + 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 new file mode 100644 index 000000000..83dae3500 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/api-reference.ts @@ -0,0 +1,20 @@ +import { + DefinitionKind, + type BaseContext, + type Documentation, + type Position, + type DefinitionIdentifier +} from '../common' + +export type APIReferenceItem = { + kind: DefinitionKind + definition: DefinitionIdentifier + insertText: string + documentation: Documentation +} + +export type APIReferenceContext = BaseContext + +export interface IAPIReferenceProvider { + provideAPIReference(ctx: APIReferenceContext, position: Position): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/completion.ts b/spx-gui/src/components/editor/code-editor/ui/completion.ts new file mode 100644 index 000000000..75915c888 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/completion.ts @@ -0,0 +1,15 @@ +import { DefinitionKind, type BaseContext, type Documentation, type Position } from '../common' + +export type CompletionContext = BaseContext + +export type CompletionItemKind = DefinitionKind + +export type CompletionItem = { + label: string + kind: CompletionItemKind + documentation: Documentation +} + +export interface ICompletionProvider { + provideCompletion(ctx: CompletionContext, position: Position): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/context-menu.ts b/spx-gui/src/components/editor/code-editor/ui/context-menu.ts new file mode 100644 index 000000000..29eb3d289 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/context-menu.ts @@ -0,0 +1,14 @@ +import { type Action, type BaseContext, type IRange, type Position } from '../common' + +export type ContextMenuContext = BaseContext + +export type Selection = IRange + +export type MenuItem = { + action: Action +} + +export interface IContextMenuProvider { + provideContextMenu(ctx: ContextMenuContext, position: Position): Promise + provideSelectionContextMenu(ctx: ContextMenuContext, selection: Selection): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/copilot.ts b/spx-gui/src/components/editor/code-editor/ui/copilot.ts new file mode 100644 index 000000000..fc4c16620 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/copilot.ts @@ -0,0 +1,78 @@ +import { + type BaseContext, + type IRange, + type DefinitionIdentifier, + type CodeSegment, + type TextDocumentRange, + type MarkdownString +} from '../common' +import type { Diagnostic } from './diagnostics' + +export enum ChatTopicKind { + Inspire, + Explain, + Review, + FixProblem +} + +export type ChatTopicInspire = { + kind: ChatTopicKind.Inspire + question: string +} + +export enum ChatExplainKind { + CodeSegment, + SymbolWithDefinition +} + +export type ChatExplainTargetCodeSegment = { + kind: ChatExplainKind.CodeSegment + codeSegment: CodeSegment +} + +export type SymbolWithDefinition = { + range: TextDocumentRange + definition: DefinitionIdentifier +} + +export type ChatExplainTargetSymbolWithDefinition = SymbolWithDefinition & { + kind: ChatExplainKind.SymbolWithDefinition +} + +export type ChatTopicExplain = { + kind: ChatTopicKind.Explain + target: ChatExplainTargetCodeSegment | ChatExplainTargetSymbolWithDefinition +} + +export type ChatTopicReview = { + kind: ChatTopicKind.Review + codeRange: IRange +} + +export type ChatTopicFixProblem = { + kind: ChatTopicKind.FixProblem + problem: Diagnostic +} + +export type ChatTopic = ChatTopicInspire | ChatTopicExplain | ChatTopicReview | ChatTopicFixProblem + +export enum MessageRole { + User, + Copilot +} + +export type ChatMessage = { + role: MessageRole + content: MarkdownString +} + +export type Chat = { + topic: ChatTopic + messages: ChatMessage[] +} + +export type ChatContext = BaseContext + +export interface ICopilot { + getChatCompletion(ctx: ChatContext, chat: Chat): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/diagnostics.ts b/spx-gui/src/components/editor/code-editor/ui/diagnostics.ts new file mode 100644 index 000000000..af0a46340 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/diagnostics.ts @@ -0,0 +1,22 @@ +import type Emitter from '@/utils/emitter' +import { type BaseContext, type IRange } from '../common' + +export type DiagnosticsContext = BaseContext + +export enum DiagnosticSeverity { + Error, + Warning +} + +export type Diagnostic = { + range: IRange + severity: DiagnosticSeverity + message: string +} + +export interface IDiagnosticsProvider + extends Emitter<{ + didChangeDiagnostics: [] + }> { + provideDiagnostics(ctx: DiagnosticsContext): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/formatting.ts b/spx-gui/src/components/editor/code-editor/ui/formatting.ts new file mode 100644 index 000000000..808c88741 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/formatting.ts @@ -0,0 +1,8 @@ +import { type BaseContext, type TextEdit } from '../common' + +export type FormattingContext = BaseContext + +export interface IFormattingEditProvider { + /** Get edits for formatting single text document */ + provideDocumentFormattingEdits(ctx: FormattingContext): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/hover.ts b/spx-gui/src/components/editor/code-editor/ui/hover.ts new file mode 100644 index 000000000..36839c2c3 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/hover.ts @@ -0,0 +1,12 @@ +import { type Action, type BaseContext, type Documentation, type Position } from '../common' + +export type Hover = { + contents: Documentation[] + actions: Action[] +} + +export type HoverContext = BaseContext + +export interface IHoverProvider { + provideHover(ctx: HoverContext, position: Position): Promise +} diff --git a/spx-gui/src/components/editor/code-editor/ui/index.ts b/spx-gui/src/components/editor/code-editor/ui/index.ts new file mode 100644 index 000000000..097f8f736 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/index.ts @@ -0,0 +1,134 @@ +import type { editor } from 'monaco-editor' +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 { 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 type { ICopilot } from './copilot' +import type { IFormattingEditProvider } from './formatting' + +export * from './hover' +export * from './completion' +export * from './resource-reference' +export * from './context-menu' +export * from './diagnostics' +export * from './api-reference' +export * from './copilot' +export * from './formatting' + +export { default as CodeEditorUIComp } from './CodeEditorUI.vue' + +export interface ICodeEditorUI { + registerHoverProvider(provider: IHoverProvider): void + registerCompletionProvider(provider: ICompletionProvider): void + registerResourceReferencesProvider(provider: IResourceReferencesProvider): void + registerContextMenuProvider(provider: IContextMenuProvider): void + registerDiagnosticsProvider(provider: IDiagnosticsProvider): void + registerAPIReferenceProvider(provider: IAPIReferenceProvider): void + registerCopilot(copilot: ICopilot): void + registerFormattingEditProvider(provider: IFormattingEditProvider): void + + /** Execute a command */ + executeCommand(command: Command, ...input: A): Promise + /** Register a command with given name & handler */ + registerCommand(command: Command, info: CommandInfo): void + + /** Open a text document in the editor. */ + open(textDocument: TextDocumentIdentifier): void + /** Open a text document in the editor,and scroll to given position */ + open(textDocument: TextDocumentIdentifier, position: Position): void + /** Open a text document in the editor, and select the given range */ + open(textDocument: TextDocumentIdentifier, range: IRange): void +} + +export class CodeEditorUI extends Disposable implements ICodeEditorUI { + registerHoverProvider(provider: IHoverProvider): void { + console.warn('TODO', provider) + } + registerCompletionProvider(provider: ICompletionProvider): void { + console.warn('TODO', provider) + } + registerResourceReferencesProvider(provider: IResourceReferencesProvider): void { + console.warn('TODO', provider) + } + registerContextMenuProvider(provider: IContextMenuProvider): void { + console.warn('TODO', provider) + } + registerDiagnosticsProvider(provider: IDiagnosticsProvider): void { + console.warn('TODO', provider) + } + registerAPIReferenceProvider(provider: IAPIReferenceProvider): void { + console.warn('TODO', provider) + } + registerCopilot(copilot: ICopilot): void { + console.warn('TODO', copilot) + } + registerFormattingEditProvider(provider: IFormattingEditProvider): void { + console.warn('TODO', provider) + } + + async executeCommand(command: Command, ...input: A): Promise { + console.warn('TODO', command, input) + return null as any + } + registerCommand(command: Command, info: CommandInfo): void { + console.warn('TODO', command, info) + } + + open(textDocument: TextDocumentIdentifier): void + open(textDocument: TextDocumentIdentifier, position: Position): void + open(textDocument: TextDocumentIdentifier, range: IRange): void + open(textDocument: TextDocumentIdentifier, positionOrRange?: Position | IRange): void { + console.warn('TODO', textDocument, positionOrRange) + } + + constructor(private project: Project) { + super() + } + + monaco?: typeof import('monaco-editor') + + initializeMonaco(monaco: typeof import('monaco-editor')) { + // TODO: do monaco configuration here + this.monaco = monaco + } + + editor?: editor.IStandaloneCodeEditor + + initializeEditor(editor: editor.IStandaloneCodeEditor) { + // TODO: do editor configuration here + this.editor = editor + } + + initialize() { + const { project, editor } = this + if (editor == null) throw new Error('editor expected') + + let selected: Stage | Sprite + if (project.selected?.type === 'stage') selected = project.stage + else if (project.selected?.type === 'sprite') selected = project.selectedSprite! + else return + + const actionName = + selected instanceof Stage + ? { en: 'Update stage code', zh: '修改舞台代码' } + : { en: `Update ${selected.name} code`, zh: `修改 ${selected.name} 代码` } + const action = { name: actionName, mergeable: true } + + // TODO: change of other code files + editor.setValue(selected.code) + editor.onDidChangeModelContent(() => { + const newValue = editor.getValue() + if (newValue === selected.code) return + project.history.doAction(action, () => { + selected.setCode(newValue) + }) + }) + } +} diff --git a/spx-gui/src/components/editor/code-editor/ui/resource-reference.ts b/spx-gui/src/components/editor/code-editor/ui/resource-reference.ts new file mode 100644 index 000000000..3aa0cc8cc --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/resource-reference.ts @@ -0,0 +1,16 @@ +import type Emitter from '@/utils/emitter' +import { type BaseContext, type IRange, type ResourceIdentifier } from '../common' + +export type ResourceReferencesContext = BaseContext + +export type ResourceReference = { + range: IRange + resource: ResourceIdentifier +} + +export interface IResourceReferencesProvider + extends Emitter<{ + didChangeResourceReferences: [] + }> { + provideResourceReferences(ctx: ResourceReferencesContext): Promise +} diff --git a/spx-gui/src/components/editor/sprite/SpriteEditor.vue b/spx-gui/src/components/editor/sprite/SpriteEditor.vue index 7610ee7ae..e749aa203 100644 --- a/spx-gui/src/components/editor/sprite/SpriteEditor.vue +++ b/spx-gui/src/components/editor/sprite/SpriteEditor.vue @@ -6,49 +6,29 @@ {{ $t({ en: 'Animations', zh: '动画' }) }} - + diff --git a/spx-gui/src/components/editor/stage/StageEditor.vue b/spx-gui/src/components/editor/stage/StageEditor.vue index 0899e1a60..8d25d9bd0 100644 --- a/spx-gui/src/components/editor/stage/StageEditor.vue +++ b/spx-gui/src/components/editor/stage/StageEditor.vue @@ -6,24 +6,17 @@ {{ $t({ en: 'Backdrops', zh: '背景' }) }} - + diff --git a/spx-gui/src/utils/emitter.ts b/spx-gui/src/utils/emitter.ts new file mode 100644 index 000000000..59f63f2cd --- /dev/null +++ b/spx-gui/src/utils/emitter.ts @@ -0,0 +1,46 @@ +/** + * @file Simple, Type-safe Event Emitter + */ + +export type EventType = string + +export type Handler = (event: T) => void + +export type Off = () => void + +export default class Emitter> { + private map = new Map() + + on(type: Type, handler: Handler): Off { + const handlers = this.map.get(type) ?? [] + const newHandlers = [...handlers, handler] + this.map.set(type, newHandlers as any) + return () => this.off(type, handler) + } + + once(type: Type, handler: Handler): Off { + const off = this.on(type, (e) => { + off() + handler(e) + }) + return off + } + + private off(type: Type, handler: Handler) { + const handlers = this.map.get(type) ?? [] + const newHandlers = handlers.filter((h) => h !== handler) + this.map.set(type, newHandlers as any) + } + + emit(...args: Events[Type] extends void ? [Type] : [Type, Events[Type]]) { + const [type, event] = args + const handlers = this.map.get(type) ?? [] + handlers.forEach((handler) => { + handler(event) + }) + } + + dispose() { + this.map.clear() + } +}