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 00000000..bef6071c --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/CodeEditor.vue @@ -0,0 +1,55 @@ + + + + + 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 00000000..f8ff23e3 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -0,0 +1,140 @@ +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 + +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 Command = string +export type CommandHandler = (...args: A) => Promise +export type CommandInfo = { + icon: Icon + title: string + handler: CommandHandler +} + +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 00000000..66ff94cb --- /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 00000000..260213e3 --- /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 00000000..3c7e1e98 --- /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 00000000..bca8172d --- /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/api-reference.ts b/spx-gui/src/components/editor/code-editor/ui/api-reference.ts new file mode 100644 index 00000000..83dae350 --- /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 00000000..75915c88 --- /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 00000000..29eb3d28 --- /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 00000000..fc4c1662 --- /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 00000000..af0a4634 --- /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 00000000..808c8874 --- /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 00000000..36839c2c --- /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 00000000..ff34ddd0 --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/ui/index.ts @@ -0,0 +1,79 @@ +import type { Project } from '@/models/project' +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 interface ICodeEditorUI { + registerProject(project: Project): void + 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 implements ICodeEditorUI { + registerProject(project: Project): void { + console.warn('TODO', project) + } + 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) + } +} 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 00000000..3aa0cc8c --- /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/utils/emitter.ts b/spx-gui/src/utils/emitter.ts new file mode 100644 index 00000000..b5c223ab --- /dev/null +++ b/spx-gui/src/utils/emitter.ts @@ -0,0 +1,48 @@ +/** + * @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() + } + +} \ No newline at end of file