Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved Molang auto-completions #660

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
165 changes: 116 additions & 49 deletions src/components/Languages/MoLang.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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(<any>tokenProvider.tokenizer.root)

// TODO - not working?
lang.updateTokenProvider(tokenProvider)
}

export class MoLangLanguage extends Language {
protected molang = new CustomMoLang({})
constructor() {
Expand All @@ -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()

Expand Down
184 changes: 184 additions & 0 deletions src/components/Languages/Molang/Data.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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()
)
}
}
Loading