diff --git a/src/components/Languages/MoLang.ts b/src/components/Languages/MoLang.ts index 5de42480d..7157961a7 100644 --- a/src/components/Languages/MoLang.ts +++ b/src/components/Languages/MoLang.ts @@ -1,7 +1,15 @@ -import type { editor, languages } from 'monaco-editor' +import type { + CancellationToken, + editor, + languages, + Position, +} from 'monaco-editor' import { Language } from './Language' import { CustomMoLang } from 'molang' -import { useMonaco } from '../../utils/libs/useMonaco' +import { useMonaco } from '/@/utils/libs/useMonaco' +import { tokenProvider } from './Molang/TokenProvider' +import { App } from '/@/App' +import { BedrockProject } from '/@/components/Projects/Project/BedrockProject' export const config: languages.LanguageConfiguration = { comments: { @@ -32,56 +40,105 @@ export const config: languages.LanguageConfiguration = { ], } -export const tokenProvider = { - ignoreCase: true, - brackets: [ - ['(', ')', 'delimiter.parenthesis'], - ['[', ']', 'delimiter.square'], - ['{', '}', 'delimiter.curly'], - ], - keywords: [ - 'return', - 'loop', - 'for_each', - 'break', - 'continue', - 'this', - 'function', - ], - identifiers: [ - 'v', - 't', - 'c', - 'q', - 'f', - 'a', - 'arg', - 'variable', - 'temp', - 'context', - 'query', - ], - tokenizer: { - root: [ - [/#.*/, 'comment'], - [/'[^']'/, 'string'], - [/[0-9]+(\.[0-9]+)?/, 'number'], - [/true|false/, 'number'], - [/\=|\,|\!|%=|\*=|\+=|-=|\/=|<|=|>|<>/, 'definition'], - [ - /[a-z_$][\w$]*/, - { - cases: { - '@keywords': 'keyword', - '@identifiers': 'type.identifier', - '@default': 'identifier', - }, - }, - ], - ], +// TODO - dynamic completions for custom molang functions +const completionItemProvider: languages.CompletionItemProvider = { + triggerCharacters: ['.', '(', ',', "'"], + provideCompletionItems: async ( + model: editor.ITextModel, + position: Position, + context: languages.CompletionContext, + token: CancellationToken + ) => { + const { Range } = await useMonaco() + + const project = await App.getApp().then((app) => app.project) + if (!(project instanceof BedrockProject)) return + + const molangData = project.molangData + await molangData.fired + + let completionItems: languages.CompletionItem[] = [] + + // Get the entire line + const line = model.getLineContent(position.lineNumber) + // Attempt to look behind the cursor to get context on what to propose + const lineUntilCursor = line.slice(0, position.column - 1) + + // Function argument auto-completions + // const withinBrackets + + // Namespace property auto-compeltions + // TODO - place cursor in brackets after inserting text + let isAccessingNamespace = false + if (lineUntilCursor.endsWith('.')) { + const strippedLine = lineUntilCursor.slice(0, -1) + const schema = await molangData.getSchema() + for (const entry of schema) { + if ( + entry.namespace.some((namespace) => + strippedLine.endsWith(namespace) + ) + ) { + isAccessingNamespace = true + completionItems = completionItems.concat( + ( + await molangData.getCompletionItemFromValues( + entry.values + ) + ).map((suggestion) => ({ + ...suggestion, + range: new Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ), + })) + ) + } + } + } + + // Global instance namespace auto-completions + if (!isAccessingNamespace) + completionItems = completionItems.concat( + (await molangData.getNamespaceSuggestions()).map( + (suggestion) => ({ + ...suggestion, + range: new Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ), + }) + ) + ) + + return { + suggestions: completionItems, + } }, } +const loadValues = async (lang: MoLangLanguage) => { + const app = await App.getApp() + await app.projectManager.fired + + const project = app.project + if (!(project instanceof BedrockProject)) return + + await project.molangData.fired + + const values = await project.molangData.allValues() + tokenProvider.root = values + .map((value) => [value, 'variable']) + .concat(tokenProvider.tokenizer.root) + + // TODO - not working? + lang.updateTokenProvider(tokenProvider) +} + export class MoLangLanguage extends Language { protected molang = new CustomMoLang({}) constructor() { @@ -90,9 +147,19 @@ export class MoLangLanguage extends Language { extensions: ['molang'], config, tokenProvider, + completionItemProvider, }) } + onModelAdded(model: editor.ITextModel) { + const isLangFor = super.onModelAdded(model) + if (!isLangFor) return false + + loadValues(this) + + return true + } + async validate(model: editor.IModel) { const { editor, MarkerSeverity } = await useMonaco() diff --git a/src/components/Languages/Molang/Data.ts b/src/components/Languages/Molang/Data.ts new file mode 100644 index 000000000..fc0dc7f66 --- /dev/null +++ b/src/components/Languages/Molang/Data.ts @@ -0,0 +1,184 @@ +import { markRaw } from 'vue' +import { Signal } from '/@/components/Common/Event/Signal' +import { App } from '/@/App' +import { + IRequirements, + RequiresMatcher, +} from '/@/components/Data/RequiresMatcher/RequiresMatcher' +import type { languages } from 'monaco-editor' +import { useMonaco } from '/@/utils/libs/useMonaco' + +export interface MolangValueDefinition { + valueName: string + description?: string + isProperty?: boolean + arguments?: MolangFunctionArgument[] + isDeprecated?: boolean + deprecationMessage: string +} + +export interface MolangDefinition { + requires?: IRequirements + namespace: string[] + values: MolangValueDefinition[] +} + +export interface MolangFunctionArgument { + argumentName: string + type: 'string' | 'number' + additionalData?: { + values?: string[] + schemaReference?: string + } +} + +export interface MolangContextDefinition { + fileType: string + data: MolangDefinition[] +} + +export class MolangData extends Signal { + protected _baseData?: any + protected _contextData?: any + + async loadCommandData(packageName: string) { + const app = await App.getApp() + + this._baseData = markRaw( + await app.dataLoader.readJSON( + `data/packages/${packageName}/language/molang/main.json` + ) + ) + this._contextData = markRaw( + await app.dataLoader.readJSON( + `data/packages/${packageName}/language/molang/context.json` + ) + ) + + this.dispatch() + } + + async getSchema(fileType?: string) { + if (!this._baseData) + throw new Error(`Acessing base molangData before it was loaded.`) + + let validEntries: MolangDefinition[] = [] + const requiresMatcher = new RequiresMatcher() + await requiresMatcher.setup() + + // Get file type specific schemas if needed + let contextData + if (fileType) contextData = this.getContextSchema(fileType) + + // Combine the context schemas with the base schemas + const allDefinitions: MolangDefinition[] = ( + this._baseData?.vanilla as MolangDefinition[] + ).concat(contextData ?? []) + + // Validate each schema by "requires" property and return valid entries + for (const entry of allDefinitions) { + if (!entry.requires || requiresMatcher.isValid(entry.requires)) + validEntries = validEntries.concat(entry) + } + + return validEntries + } + + getContextSchema(fileType?: string) { + if (!this._contextData) + throw new Error(`Acessing context molangData before it was loaded.`) + + // Find context schema for the specified file type, or return all context schemas if no file type is defined + const contextData = ( + this._contextData?.contexts as MolangContextDefinition[] + ).find((entry) => (fileType ? entry.fileType === fileType : true)) + + return contextData?.data ?? [] + } + + async getCompletionItemFromValues(values: MolangValueDefinition[]) { + const { languages } = await useMonaco() + return values + .filter((value) => !value.isDeprecated) + .map((value) => { + return { + insertText: `${value.valueName}${ + value.isProperty ? '' : '()' + }`, + kind: value.isProperty + ? languages.CompletionItemKind.Variable + : languages.CompletionItemKind.Function, + label: value.valueName, + documentation: value.description, + } + }) + } + + async getNamespaceSuggestions() { + const { languages } = await useMonaco() + return [ + { + label: 'math', + kind: languages.CompletionItemKind.Variable, + insertText: 'math', + }, + { + label: 'variable', + kind: languages.CompletionItemKind.Variable, + insertText: 'variable', + }, + { + label: 'v', + kind: languages.CompletionItemKind.Variable, + insertText: 'v', + }, + { + label: 'context', + kind: languages.CompletionItemKind.Variable, + insertText: 'context', + }, + { + label: 'c', + kind: languages.CompletionItemKind.Variable, + insertText: 'c', + }, + { + label: 'query', + kind: languages.CompletionItemKind.Variable, + insertText: 'query', + }, + { + label: 'q', + kind: languages.CompletionItemKind.Variable, + insertText: 'q', + }, + { + label: 'temp', + kind: languages.CompletionItemKind.Variable, + insertText: 'temp', + }, + { + label: 't', + kind: languages.CompletionItemKind.Variable, + insertText: 't', + }, + ] + } + + /** + * Returns a list of all keywords from the schema value names + */ + async allValues() { + return this.getSchema().then((schema) => + [ + ...new Set( + schema + .concat(this.getContextSchema()) + .map((def) => def.values) + ), + ] + .map((def) => def.map((def) => def.valueName)) + .flat() + ) + } +} diff --git a/src/components/Languages/Molang/TokenProvider.ts b/src/components/Languages/Molang/TokenProvider.ts new file mode 100644 index 000000000..445dded67 --- /dev/null +++ b/src/components/Languages/Molang/TokenProvider.ts @@ -0,0 +1,50 @@ +export const tokenProvider: any = { + ignoreCase: true, + brackets: [ + ['(', ')', 'delimiter.parenthesis'], + ['[', ']', 'delimiter.square'], + ['{', '}', 'delimiter.curly'], + ], + keywords: [ + 'return', + 'loop', + 'for_each', + 'break', + 'continue', + 'this', + 'function', + ], + identifiers: [ + 'v', + 't', + 'c', + 'q', + 'f', + 'a', + 'arg', + 'variable', + 'temp', + 'context', + 'query', + 'math', + ], + tokenizer: { + root: [ + [/#.*/, 'comment'], + [/'[^']'/, 'string'], + [/[0-9]+(\.[0-9]+)?/, 'number'], + [/true|false/, 'number'], + [/\=|\,|\!|%=|\*=|\+=|-=|\/=|<|=|>|<>/, 'definition'], + [ + /[a-z_$][\w$]*/, + { + cases: { + '@keywords': 'keyword', + '@identifiers': 'type.identifier', + '@default': 'identifier', + }, + }, + ], + ], + }, +} diff --git a/src/components/Languages/Molang/WithinJson.ts b/src/components/Languages/Molang/WithinJson.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/Projects/Project/BedrockProject.ts b/src/components/Projects/Project/BedrockProject.ts index 673c34c09..c91b7bff1 100644 --- a/src/components/Projects/Project/BedrockProject.ts +++ b/src/components/Projects/Project/BedrockProject.ts @@ -11,6 +11,7 @@ import { CommandData } from '/@/components/Languages/Mcfunction/Data' import { FileTab } from '../../TabSystem/FileTab' import { HTMLPreviewTab } from '../../Editors/HTMLPreview/HTMLPreview' import { LangData } from '/@/components/Languages/Lang/Data' +import { MolangData } from '../../Languages/Molang/Data' import { ColorData } from '../../Languages/Json/ColorPicker/Data' const bedrockPreviews: ITabPreviewConfig[] = [ @@ -57,6 +58,7 @@ const bedrockPreviews: ITabPreviewConfig[] = [ export class BedrockProject extends Project { commandData = new CommandData() langData = new LangData() + molangData = new MolangData() colorData = new ColorData() onCreate() { @@ -89,6 +91,7 @@ export class BedrockProject extends Project { this.commandData.loadCommandData('minecraftBedrock') this.langData.loadLangData('minecraftBedrock') + this.molangData.loadCommandData('minecraftBedrock') this.colorData.loadColorData() }