diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..dfec171d0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug in Edge", + "type": "msedge", + "request": "launch", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + }, + { + "name": "Debug in Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/package-lock.json b/package-lock.json index c1f65e170..c237f879a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bridge", - "version": "2.3.1", + "version": "2.3.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bridge", - "version": "2.3.1", + "version": "2.3.2", "dependencies": { "@mdi/font": "^6.9.96", "@types/lz-string": "^1.3.34", @@ -19,7 +19,7 @@ "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.9.28", + "dash-compiler": "^0.9.29", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", @@ -2837,9 +2837,9 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "node_modules/dash-compiler": { - "version": "0.9.28", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.9.28.tgz", - "integrity": "sha512-/l2GIn+AvUxRsl0oYizNH7Bi+IdfOyi5P6v21A/HjSNDcFuKPpYGHz6xfvzCE9g6rNloN862q8dQ0CavgdEtOQ==", + "version": "0.9.29", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.9.29.tgz", + "integrity": "sha512-BxDcF7MtTWAginxD5UO5PBs4BZQIOpkIxJ4sztM8sL3vjdv3FH/maKndqjhVGc+bXW44rOYpnu6U64iGXzmmFg==", "dependencies": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", @@ -7159,9 +7159,9 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "dash-compiler": { - "version": "0.9.28", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.9.28.tgz", - "integrity": "sha512-/l2GIn+AvUxRsl0oYizNH7Bi+IdfOyi5P6v21A/HjSNDcFuKPpYGHz6xfvzCE9g6rNloN862q8dQ0CavgdEtOQ==", + "version": "0.9.29", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.9.29.tgz", + "integrity": "sha512-BxDcF7MtTWAginxD5UO5PBs4BZQIOpkIxJ4sztM8sL3vjdv3FH/maKndqjhVGc+bXW44rOYpnu6U64iGXzmmFg==", "requires": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", diff --git a/package.json b/package.json index d1139adc0..77c2e25cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bridge", - "version": "2.3.2", + "version": "2.3.3", "private": true, "scripts": { "dev": "vite", @@ -23,7 +23,7 @@ "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.9.28", + "dash-compiler": "^0.9.29", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", diff --git a/public/packages.zip b/public/packages.zip index ce3ad4c19..2ed9d5d3f 100644 Binary files a/public/packages.zip and b/public/packages.zip differ diff --git a/src/App.ts b/src/App.ts index a285b47d5..1b8941e4a 100644 --- a/src/App.ts +++ b/src/App.ts @@ -319,6 +319,8 @@ export class App { public readonly bridgeFolderSetup = new Signal() async setupBridgeFolder(forceReselect = false) { + if (!forceReselect && this.bridgeFolderSetup.hasFired) return true + let fileHandle = await get( 'bridgeBaseDir' ) diff --git a/src/components/Data/JSONDefaults.ts b/src/components/Data/JSONDefaults.ts index 19b4a3206..ea0c69157 100644 --- a/src/components/Data/JSONDefaults.ts +++ b/src/components/Data/JSONDefaults.ts @@ -10,6 +10,7 @@ import { AnyFileHandle } from '../FileSystem/Types' import { Tab } from '../TabSystem/CommonTab' import { ComponentSchemas } from '../Compiler/Worker/Plugins/CustomComponent/ComponentSchemas' import { loadMonaco, useMonaco } from '../../utils/libs/useMonaco' +import { Task } from '../TaskManager/Task' let globalSchemas: Record = {} let loadedGlobalSchemas = false @@ -19,6 +20,7 @@ export class JsonDefaults extends EventDispatcher { protected localSchemas: Record = {} protected disposables: IDisposable[] = [] public readonly componentSchemas = new ComponentSchemas() + protected task: Task | null = null constructor(protected project: Project) { super() @@ -60,12 +62,14 @@ export class JsonDefaults extends EventDispatcher { this.disposables.forEach((disposable) => disposable.dispose()) this.componentSchemas.dispose() this.disposables = [] + this.task?.complete() + this.task = null } async loadAllSchemas() { this.localSchemas = {} const app = await App.getApp() - const task = app.taskManager.create({ + this.task = app.taskManager.create({ icon: 'mdi-book-open-outline', name: 'taskManager.tasks.loadingSchemas.name', description: 'taskManager.tasks.loadingSchemas.description', @@ -73,9 +77,9 @@ export class JsonDefaults extends EventDispatcher { }) await app.dataLoader.fired - task.update(1) + this.task?.update(1) const packages = await app.dataLoader.readdir('data/packages') - task.update(2) + this.task?.update(2) // Static schemas for (const packageName of packages) { @@ -92,11 +96,11 @@ export class JsonDefaults extends EventDispatcher { } } loadedGlobalSchemas = true - task.update(3) + this.task?.update(3) // Schema scripts await this.runSchemaScripts(app) - task.update(5) + this.task?.update(5) const tab = this.project.tabSystem?.selectedTab if (tab && tab instanceof FileTab) { const fileType = App.fileType.getId(tab.getPath()) @@ -111,11 +115,11 @@ export class JsonDefaults extends EventDispatcher { // Schemas generated from lightning cache this.addSchemas(await this.getDynamicSchemas()) - task.update(4) + this.task?.update(4) this.loadedSchemas = true - task.update(6) - task.complete() + this.task?.update(6) + this.task?.complete() } async setJSONDefaults(validate = true) { diff --git a/src/components/Data/TypeLoader.ts b/src/components/Data/TypeLoader.ts index 616404d26..63b03dd25 100644 --- a/src/components/Data/TypeLoader.ts +++ b/src/components/Data/TypeLoader.ts @@ -111,7 +111,13 @@ export class TypeLoader { const app = await App.getApp() await app.project.packIndexer.fired - const allFiles = await app.project.packIndexer.service.getAllFiles() + let allFiles + try { + allFiles = await app.project.packIndexer.service.getAllFiles() + } catch { + // We failed to access the pack indexer service -> fail silently + return + } const typeScriptFiles = allFiles.filter( (filePath) => filePath.endsWith('.ts') || filePath.endsWith('.js') diff --git a/src/components/Editors/FunctionValidator/Error.vue b/src/components/Editors/FunctionValidator/Error.vue deleted file mode 100644 index ec9c89d11..000000000 --- a/src/components/Editors/FunctionValidator/Error.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/src/components/Editors/FunctionValidator/Tab.ts b/src/components/Editors/FunctionValidator/Tab.ts deleted file mode 100644 index d7fa5f0f2..000000000 --- a/src/components/Editors/FunctionValidator/Tab.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { FileTab } from '/@/components/TabSystem/FileTab' -import FunctionValidatorTabComponent from './Tab.vue' -import { Tab } from '../../TabSystem/CommonTab' -import { TabSystem } from '../../TabSystem/TabSystem' -import Error from './Error.vue' -import Warning from './Warning.vue' -import Vue from 'vue' -import { FunctionValidator } from '/@/components/Languages/Mcfunction/Validation/Validator' -import { App } from '/@/App' -import { translate } from '../../Locales/Manager' - -export class FunctionValidatorTab extends Tab { - protected fileTab: FileTab | undefined - protected currentLine = 0 - protected content: string = '' - - protected stopped = false - - protected app: App | undefined - - constructor(protected parent: TabSystem, protected tab: FileTab) { - super(parent) - this.fileTab = tab - } - - get name(): string { - return translate('functionValidator.tabName') - } - - isFor(fileHandle: FileSystemFileHandle): Promise { - return Promise.resolve(false) - } - - component = FunctionValidatorTabComponent - - get icon() { - return 'mdi-cog-box' - } - - get iconColor() { - return 'primary' - } - - save() {} - - //Load function content - protected async LoadFileContent() { - if (this.fileTab) { - let file = await this.fileTab.getFile() - this.content = await file?.text() - } - } - - protected translateError(errorName: string) { - return translate('functionValidator.errors.' + errorName) - } - - protected translateWarning(errorName: string) { - return translate('functionValidator.warnings.' + errorName) - } - - protected async UpdateLoadedState() { - let dataLoadingElement = document.getElementById('data-loading') - let loadedDataElement = document.getElementById('loaded-content') - - if (this.app!.languageManager.mcfunction.validator.blockStateData) { - if (dataLoadingElement) { - dataLoadingElement.classList.add('hidden') - } - - if (loadedDataElement) { - loadedDataElement.classList.remove('hidden') - } - - await this.LoadCurrentLine() - } else { - setTimeout(() => { - this.UpdateLoadedState() - }, 500) - } - } - - //Displays data - protected async LoadCurrentLine() { - let lines = this.content.split('\n') - - if (this.currentLine < lines.length) { - let fullCommand = lines[this.currentLine].substring( - 0, - lines[this.currentLine].length - 1 - ) - - if ( - lines[this.currentLine].substring( - lines[this.currentLine].length - 1 - ) != '\r' - ) { - fullCommand = lines[this.currentLine].substring( - 0, - lines[this.currentLine].length - ) - } - - let command = fullCommand.split(' ')[0] - - let lineCounterElement = document.getElementById('line-counter') - - if (lineCounterElement) { - lineCounterElement.textContent = - 'Line: ' + (this.currentLine + 1).toString() - } - - let fullCommmandDisplayElement = document.getElementById( - 'full-command-display' - ) - - if (fullCommmandDisplayElement) { - fullCommmandDisplayElement.innerHTML = - 'Full Command: ' + lines[this.currentLine] - } - - let alertsElement = document.getElementById('alerts') - let docsElement = document.getElementById('docs') - - if (alertsElement && docsElement) { - let alertCount = alertsElement.children.length - - for (let i = 0; i < alertCount; i++) { - alertsElement.children[0].remove() - } - - if ( - lines[this.currentLine] == '\r' || - lines[this.currentLine].length == 0 - ) { - docsElement.textContent = 'No documentation.' - } else { - let data = - await this.app!.languageManager.mcfunction.validator.ValidateCommand( - fullCommand - ) - - let currentErrorLines = [] - - for (let i = 0; i < data[0].length; i++) { - const start = data[0][i].start - const end = data[0][i].end - - currentErrorLines.push([start, end]) - - let translated = '' - - if (typeof data[0][i].value === 'string') { - translated = this.translateError(data[0][i].value) - } else { - for (let j = 0; j < data[0][i].value.length; j++) { - console.log(data[0][i].value[j]) - console.log(data[0][i].value[j].startsWith('$')) - - if (data[0][i].value[j].startsWith('$')) { - translated += - data[0][i].value[j].substring(1) - } else { - translated += this.translateError( - data[0][i].value[j] - ) - } - } - } - - console.log(translated) - - var ComponentClass = Vue.extend(Error) - var instance = new ComponentClass({ - propsData: { - alertText: translated, - }, - }) - - instance.$mount() - alertsElement.appendChild(instance.$el) - } - - let currentWarningLines = [] - - for (let i = 0; i < data[1].length; i++) { - const start = data[1][i].start - const end = data[1][i].end - - currentWarningLines.push([start, end]) - - let translated = '' - - if (typeof data[1][i].value === 'string') { - translated = this.translateWarning(data[1][i].value) - } else { - for (let j = 0; j < data[1][i].value.length; j++) { - if (data[1][i].value[j].startsWith('$')) { - translated += - data[1][i].value[j].substring(1) - } else { - translated += this.translateWarning( - data[1][i].value[j] - ) - } - } - } - - var ComponentClass = Vue.extend(Warning) - var instance = new ComponentClass({ - propsData: { - alertText: translated, - }, - }) - - instance.$mount() - alertsElement.appendChild(instance.$el) - } - - if (fullCommmandDisplayElement) { - if (fullCommmandDisplayElement.innerHTML) { - let writeOffset = 0 - let writeIndex = 0 - - for ( - let i = 0; - i < fullCommmandDisplayElement.innerHTML.length; - i++ - ) { - for ( - let j = 0; - j < currentErrorLines.length; - j++ - ) { - if (currentErrorLines[j][0] == writeIndex) { - fullCommmandDisplayElement.innerHTML = - 'Full Command: ' + - fullCommmandDisplayElement.innerHTML.substring( - 14, - 14 + i - ) + - '' + - fullCommmandDisplayElement.innerHTML.substring( - 14 + i, - fullCommmandDisplayElement - .innerHTML.length - ) - - i += 25 - } - - if (currentErrorLines[j][1] == writeIndex) { - fullCommmandDisplayElement.innerHTML = - 'Full Command: ' + - fullCommmandDisplayElement.innerHTML.substring( - 14, - 14 + i - ) + - '' + - fullCommmandDisplayElement.innerHTML.substring( - 14 + i, - fullCommmandDisplayElement - .innerHTML.length - ) - - i += 7 - } - } - - for ( - let j = 0; - j < currentWarningLines.length; - j++ - ) { - if ( - currentWarningLines[j][0] == writeIndex - ) { - fullCommmandDisplayElement.innerHTML = - 'Full Command: ' + - fullCommmandDisplayElement.innerHTML.substring( - 14, - 14 + i - ) + - '' + - fullCommmandDisplayElement.innerHTML.substring( - 14 + i, - fullCommmandDisplayElement - .innerHTML.length - ) - - i += 27 - } - - if ( - currentWarningLines[j][1] == writeIndex - ) { - fullCommmandDisplayElement.innerHTML = - 'Full Command: ' + - fullCommmandDisplayElement.innerHTML.substring( - 14, - 14 + i - ) + - '' + - fullCommmandDisplayElement.innerHTML.substring( - 14 + i, - fullCommmandDisplayElement - .innerHTML.length - ) - - i += 7 - } - } - - writeIndex++ - } - } - } - - docsElement.textContent = - await this.app!.languageManager.mcfunction.validator.GetDocs( - command - ) - - if (data[0].length > 0) { - return true - } - } - } - } else { - return true - } - - return false - } - - protected SlowStepLine() { - setTimeout(() => { - if (!this.stopped) { - this.currentLine += 1 - this.LoadCurrentLine().then((shouldStop) => { - if (!shouldStop && !this.stopped) { - this.SlowStepLine() - } - - this.stopped = false - }) - } - }, 0) - } - - protected async Play() { - this.stopped = false - - await this.LoadFileContent() - - this.currentLine = 0 - - let shouldStop = false - - shouldStop = await this.LoadCurrentLine() - - if (!shouldStop) { - this.SlowStepLine() - } - } - - protected async StepLine() { - this.stopped = false - - await this.LoadFileContent() - - this.currentLine += 1 - this.LoadCurrentLine() - } - - protected async Restart() { - await this.LoadFileContent() - - this.currentLine = 0 - this.LoadCurrentLine() - - this.stopped = true - } - - async onActivate() { - await super.onActivate() - - this.app = await App.getApp() - - if (!this.app.languageManager.mcfunction.validator.loadedCommandData) { - this.app.languageManager.mcfunction.validator.LoadCommandData() - } - - await this.LoadFileContent() - - await this.app.languageManager.mcfunction.validator.LoadCommandData() - - await this.UpdateLoadedState() - } - - async onDeactivate() { - this.stopped = true - } -} diff --git a/src/components/Editors/FunctionValidator/Tab.vue b/src/components/Editors/FunctionValidator/Tab.vue deleted file mode 100644 index 407e9dea4..000000000 --- a/src/components/Editors/FunctionValidator/Tab.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - diff --git a/src/components/Editors/FunctionValidator/Warning.vue b/src/components/Editors/FunctionValidator/Warning.vue deleted file mode 100644 index 3f554ccfc..000000000 --- a/src/components/Editors/FunctionValidator/Warning.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/src/components/Editors/HTMLPreview/HTMLPreview.ts b/src/components/Editors/HTMLPreview/HTMLPreview.ts index 603945194..011427093 100644 --- a/src/components/Editors/HTMLPreview/HTMLPreview.ts +++ b/src/components/Editors/HTMLPreview/HTMLPreview.ts @@ -51,10 +51,10 @@ export class HTMLPreviewTab extends IframeTab { await super.setup() } async onActivate() { + await super.onActivate() await this.api.loaded.fired - this.api.trigger('loadScrollPosition', this.scrollY) - await super.onActivate() + this.api.trigger('loadScrollPosition', this.scrollY) } onDeactivate() { this.messageListener?.dispose() diff --git a/src/components/Editors/IframeTab/IframeTab.ts b/src/components/Editors/IframeTab/IframeTab.ts index 7dd1e5208..d74661478 100644 --- a/src/components/Editors/IframeTab/IframeTab.ts +++ b/src/components/Editors/IframeTab/IframeTab.ts @@ -57,13 +57,17 @@ export class IframeTab extends Tab { await super.setup() } async onActivate() { + await super.onActivate() + this.isLoading = true await this.loaded this.isLoading = false - this.iframe.style.display = 'block' + // Only show iframe if tab is still active + if (this.isActive) this.iframe.style.display = 'block' } onDeactivate() { + super.onDeactivate() this.iframe.style.display = 'none' } onDestroy() { diff --git a/src/components/Editors/TreeEditor/Highlight.vue b/src/components/Editors/TreeEditor/Highlight.vue index d6bc35202..d7ad870b6 100644 --- a/src/components/Editors/TreeEditor/Highlight.vue +++ b/src/components/Editors/TreeEditor/Highlight.vue @@ -53,7 +53,6 @@ export default { mixins: [ HighlighterMixin([ 'string', - 'number', 'variable', 'definition', 'keyword', diff --git a/src/components/Editors/TreeEditor/Tree/CommonTree.vue b/src/components/Editors/TreeEditor/Tree/CommonTree.vue index 229dec2b4..25b709a86 100644 --- a/src/components/Editors/TreeEditor/Tree/CommonTree.vue +++ b/src/components/Editors/TreeEditor/Tree/CommonTree.vue @@ -27,21 +27,22 @@ > mdi-chevron-right - + s: {{ tree.type }} p: {{ tree.parent.type }} - : {{ hideBracketsWithinTreeEditor ? undefined : ':' }} @@ -110,7 +111,18 @@ export default { } }, computed: { + hideBracketsWithinTreeEditor() { + if (!settingsState.editor) return false + return settingsState.editor.hideBracketsWithinTreeEditor || false + }, + showArrayIndices() { + if (!settingsState.editor) return false + return settingsState.editor.showArrayIndices || false + }, + openingBracket() { + if (this.hideBracketsWithinTreeEditor) return + if (this.tree.isOpen) return brackets[this.tree.type][0] else if (Object.keys(this.tree.children).length > 0) return `${brackets[this.tree.type][0]}...${ @@ -121,6 +133,8 @@ export default { }` }, closingBracket() { + if (this.hideBracketsWithinTreeEditor) return + return this.tree.isOpen ? brackets[this.tree.type][1] : undefined }, }, diff --git a/src/components/Editors/TreeEditor/Tree/TreeChildren.vue b/src/components/Editors/TreeEditor/Tree/TreeChildren.vue index 758823982..0734208b4 100644 --- a/src/components/Editors/TreeEditor/Tree/TreeChildren.vue +++ b/src/components/Editors/TreeEditor/Tree/TreeChildren.vue @@ -16,7 +16,10 @@ :treeEditor="treeEditor" @setActive="$emit('setActive')" > - + {{ + key + }} + @@ -29,9 +32,11 @@ import { pointerDevice } from '/@/utils/pointerDevice.ts' import Draggable from 'vuedraggable' import { settingsState } from '/@/components/Windows/Settings/SettingsState.ts' import { MoveEntry } from '../History/MoveEntry.ts' +import { HighlighterMixin } from '/@/components/Mixins/Highlighter' export default { name: 'TreeChildren', + mixins: [HighlighterMixin(['number'])], components: { Highlight, Draggable, diff --git a/src/components/Extensions/InstallFiles.ts b/src/components/Extensions/InstallFiles.ts index 425048fc7..f682b553d 100644 --- a/src/components/Extensions/InstallFiles.ts +++ b/src/components/Extensions/InstallFiles.ts @@ -45,10 +45,11 @@ export class InstallFiles { to.pack, to.path ) - const newFileHandle = await app.fileSystem.getFileHandle( - join(target, filePath), - true - ) + const newFileHandle = + await app.fileSystem.getFileHandle( + join(target, filePath), + true + ) await app.fileSystem.copyFileHandle( fileHandle, newFileHandle @@ -63,5 +64,8 @@ export class InstallFiles { continue } } + + // Refresh the pack explorer once files have been added + app.actionManager.trigger('bridge.action.refreshProject') } } diff --git a/src/components/Extensions/Scripts/Modules/Tab.ts b/src/components/Extensions/Scripts/Modules/Tab.ts index b94fbfa6f..e3212633a 100644 --- a/src/components/Extensions/Scripts/Modules/Tab.ts +++ b/src/components/Extensions/Scripts/Modules/Tab.ts @@ -44,6 +44,7 @@ export const TabModule = async ({ disposables }: IModuleConfig) => { const tab = new FileTabClass(tabSystem) if (splitScreen) tabSystem.setActive(true) + tabSystem.add(tab, true) disposables.push({ dispose: () => tabSystem.remove(tab), diff --git a/src/components/Extensions/Scripts/Modules/project.ts b/src/components/Extensions/Scripts/Modules/project.ts index 28754784c..caa1296a8 100644 --- a/src/components/Extensions/Scripts/Modules/project.ts +++ b/src/components/Extensions/Scripts/Modules/project.ts @@ -41,6 +41,7 @@ export const ProjectModule = async ({ ? undefined : `${app.project.projectPath}/.bridge/compiler/${configFile}` ) + await service.setup() await service.build() }, diff --git a/src/components/FileSystem/FileSystem.ts b/src/components/FileSystem/FileSystem.ts index 6784b7adc..999f16a93 100644 --- a/src/components/FileSystem/FileSystem.ts +++ b/src/components/FileSystem/FileSystem.ts @@ -214,6 +214,15 @@ export class FileSystem extends Signal { throw new Error(`Invalid JSON: ${path}`) } } + async readJsonHandle(fileHandle: AnyFileHandle) { + const file = await fileHandle.getFile() + + try { + return await json5.parse(await file.text()) + } catch { + throw new Error(`Invalid JSON: ${fileHandle.name}`) + } + } writeJSON(path: string, data: any, beautify = false) { return this.writeFile( path, diff --git a/src/components/Languages/Common/ColorCodes.ts b/src/components/Languages/Common/ColorCodes.ts index aa2feffe7..02ede1fe8 100644 --- a/src/components/Languages/Common/ColorCodes.ts +++ b/src/components/Languages/Common/ColorCodes.ts @@ -1,22 +1,22 @@ export const colorCodes = [ - [/§4/, 'colorCode.darkRed'], - [/§c/, 'colorCode.red'], - [/§6/, 'colorCode.gold'], - [/§e/, 'colorCode.yellow'], - [/§2/, 'colorCode.darkGreen'], - [/§a/, 'colorCode.green'], - [/§b/, 'colorCode.aqua'], - [/§3/, 'colorCode.darkAqua'], - [/§1/, 'colorCode.darkBlue'], - [/§9/, 'colorCode.blue'], - [/§d/, 'colorCode.lightPurple'], - [/§5/, 'colorCode.darkPurple'], - [/§[fr]/, 'colorCode.white'], - [/§7/, 'colorCode.gray'], - [/§8/, 'colorCode.darkGray'], - [/§0/, 'colorCode.black'], - [/§g/, 'colorCode.minecoinGold'], - [/§o/, 'colorCode.italic'], - [/§l/, 'colorCode.bold'], - [/§n/, 'colorCode.underline'], + [/§4[^§]*/, 'colorCode.darkRed'], + [/§c[^§]*/, 'colorCode.red'], + [/§6[^§]*/, 'colorCode.gold'], + [/§e[^§]*/, 'colorCode.yellow'], + [/§2[^§]*/, 'colorCode.darkGreen'], + [/§a[^§]*/, 'colorCode.green'], + [/§b[^§]*/, 'colorCode.aqua'], + [/§3[^§]*/, 'colorCode.darkAqua'], + [/§1[^§]*/, 'colorCode.darkBlue'], + [/§9[^§]*/, 'colorCode.blue'], + [/§d[^§]*/, 'colorCode.lightPurple'], + [/§5[^§]*/, 'colorCode.darkPurple'], + [/§f[^§]*/, 'colorCode.white'], + [/§7[^§]*/, 'colorCode.gray'], + [/§8[^§]*/, 'colorCode.darkGray'], + [/§0[^§]*/, 'colorCode.black'], + [/§g[^§]*/, 'colorCode.minecoinGold'], + [/§o[^§]*/, 'colorCode.italic'], + [/§l[^§]*/, 'colorCode.bold'], + [/§n[^§]*/, 'colorCode.underline'], ] diff --git a/src/components/Languages/Lang.ts b/src/components/Languages/Lang.ts index 2dd7e7d02..e5df113d0 100644 --- a/src/components/Languages/Lang.ts +++ b/src/components/Languages/Lang.ts @@ -1,8 +1,12 @@ -import type { languages } from 'monaco-editor' +import type { languages, editor, Range } from 'monaco-editor' +import { BedrockProject } from '/@/components/Projects/Project/BedrockProject' import { colorCodes } from './Common/ColorCodes' import { Language } from './Language' import { App } from '/@/App' import { useMonaco } from '/@/utils/libs/useMonaco' +import { guessValue } from './Lang/guessValue' +import { translate } from '/@/components/Locales/Manager' +import { Project } from '../Projects/Project/Project' export const config: languages.LanguageConfiguration = { comments: { @@ -16,43 +20,18 @@ export const tokenProvider = { }, } -async function getValidLangKeys() { - const app = await App.getApp() - const packIndexer = app.project.packIndexer - - await packIndexer.fired - - const entityIdentifiers: string[] = - (await packIndexer.service.getCacheDataFor( - 'entity', - undefined, - 'identifier' - )) ?? [] - const blockIdentifiers: string[] = - (await packIndexer.service.getCacheDataFor( - 'block', - undefined, - 'identifier' - )) ?? [] - const itemIdentifiers: string[] = - (await packIndexer.service.getCacheDataFor( - 'item', - undefined, - 'identifier' - )) ?? [] - - return [ - ...entityIdentifiers.map((id) => `entity.${id}.name`), - ...entityIdentifiers.map((id) => `item.spawn_egg.entity.${id}.name`), - ...itemIdentifiers.map((id) => `item.${id}.name`), - ...blockIdentifiers.map((id) => `tile.${id}.name`), - ] -} - const completionItemProvider: languages.CompletionItemProvider = { + triggerCharacters: ['='], provideCompletionItems: async (model, position) => { + const project = await App.getApp().then((app) => app.project) + if (!(project instanceof BedrockProject)) return + const { Range, languages } = await useMonaco() + const langData = project.langData + await langData.fired + const app = await App.getApp() + // Only auto-complete in a client lang file const isClientLang = App.fileType.getId( app.project.tabSystem?.selectedTab?.getPath()! @@ -74,6 +53,7 @@ const completionItemProvider: languages.CompletionItemProvider = { const suggestions: languages.CompletionItem[] = [] if (!isValueSuggestion) { + // Get the lang keys that are already set in the file const currentLangKeys = new Set( model .getValue() @@ -81,7 +61,7 @@ const completionItemProvider: languages.CompletionItemProvider = { .map((line) => line.split('=')[0].trim()) ) - const validLangKeys = (await getValidLangKeys()).filter( + const validLangKeys = (await langData.getValidLangKeys()).filter( (key) => !currentLangKeys.has(key) ) @@ -98,6 +78,34 @@ const completionItemProvider: languages.CompletionItemProvider = { insertText: key, })) ) + } else { + // Generate a value based on the key + const line = model + .getValueInRange( + new Range( + position.lineNumber, + 0, + position.lineNumber, + position.column + ) + ) + .toLowerCase() + + // Check whether the cursor is after a key and equals sign, but no value yet (e.g. "tile.minecraft:dirt.name=") + if (line[line.length - 1] === '=') { + const translation = (await guessValue(line)) ?? '' + suggestions.push({ + label: translation, + insertText: translation, + kind: languages.CompletionItemKind.Text, + range: new Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ), + }) + } } return { @@ -105,6 +113,48 @@ const completionItemProvider: languages.CompletionItemProvider = { } }, } +const codeActionProvider: languages.CodeActionProvider = { + provideCodeActions: async ( + model: editor.ITextModel, + range: Range, + context: languages.CodeActionContext + ) => { + const { Range } = await useMonaco() + + const actions: languages.CodeAction[] = [] + for (const marker of context.markers) { + const line = model.getLineContent(marker.startLineNumber) + const val = await guessValue(line) + + actions.push({ + title: translate('editors.langValidation.noValue.quickFix'), + diagnostics: [marker], + kind: 'quickfix', + edit: { + edits: [ + { + resource: model.uri, + edit: { + range: new Range( + marker.startLineNumber, + marker.startColumn, + marker.endLineNumber, + marker.endColumn + ), + text: `${line}=${val}`, + }, + }, + ], + }, + isPreferred: true, + }) + } + return { + actions: actions, + dispose: () => {}, + } + }, +} export class LangLanguage extends Language { constructor() { @@ -114,8 +164,49 @@ export class LangLanguage extends Language { config, tokenProvider, completionItemProvider, + codeActionProvider, }) + + // Highlight namespaces + this.disposables.push( + App.eventSystem.on('projectChanged', (project: Project) => { + const tokenizer = { + root: [ + ...new Set( + [ + 'minecraft', + 'bridge', + project.config.get().namespace, + ].filter((k) => k !== undefined) + ), + ] + .map((word) => [word, 'keyword']) + .concat(tokenProvider.tokenizer.root), + } + + this.updateTokenProvider({ tokenizer }) + }) + ) } - validate() {} + async validate(model: editor.IModel) { + const { editor, MarkerSeverity } = await useMonaco() + + const markers: editor.IMarkerData[] = [] + for (let l = 1; l <= model.getLineCount(); l++) { + const line = model.getLineContent(l) + if (line && !line.includes('=') && !line.startsWith('##')) + markers.push({ + startColumn: 1, + endColumn: line.length + 1, + startLineNumber: l, + endLineNumber: l, + message: translate( + 'editors.langValidation.noValue.errorMessage' + ), + severity: MarkerSeverity.Error, + }) + } + editor.setModelMarkers(model, this.id, markers) + } } diff --git a/src/components/Languages/Lang/Data.ts b/src/components/Languages/Lang/Data.ts new file mode 100644 index 000000000..0854520c7 --- /dev/null +++ b/src/components/Languages/Lang/Data.ts @@ -0,0 +1,65 @@ +import { inject, markRaw } from 'vue' +import { App } from '/@/App' +import { Signal } from '/@/components/Common/Event/Signal' + +interface ILangKey { + formats: string[] + inject: { + name: string + fileType: string + cacheKey: string + }[] +} + +export class LangData extends Signal { + protected _data?: any + + async loadLangData(packageName: string) { + const app = await App.getApp() + + this._data = markRaw( + await app.dataLoader.readJSON( + `data/packages/${packageName}/language/lang/main.json` + ) + ) + + this.dispatch() + } + + async getValidLangKeys() { + if (!this._data) return [] + + let keys: string[] = [] + for (const keyDef of this._data.keys as ILangKey[]) { + keys = keys.concat(await this.generateKeys(keyDef)) + } + return keys + } + + async generateKeys(key: ILangKey) { + const app = await App.getApp() + const packIndexer = app.project.packIndexer + + await packIndexer.fired + + // Find out what data to use for these keys + let keys: string[] = [] + for (const fromCache of key.inject) { + const fetchedData = + (await packIndexer.service.getCacheDataFor( + fromCache.fileType, + undefined, + fromCache.cacheKey + )) ?? [] + for (const format of key.formats) { + keys = keys.concat( + fetchedData.map((data: string) => + format.replace(`{{${fromCache.name}}}`, data) + ) + ) + } + } + + return keys + } +} diff --git a/src/components/Languages/Lang/guessValue.ts b/src/components/Languages/Lang/guessValue.ts new file mode 100644 index 000000000..aa2a24f2e --- /dev/null +++ b/src/components/Languages/Lang/guessValue.ts @@ -0,0 +1,31 @@ +export async function guessValue(line: string) { + // 1. Find the part of the key that isn't a common key prefix/suffix (e.g. the identifier) + const commonParts = ['name', 'tile', 'item', 'entity', 'action'] + const key = line.substring(0, line.length - 1) + let uniqueParts = key + .split('.') + .filter((part) => !commonParts.includes(part)) + + // 2. If there are 2 parts and one is spawn_egg, then state that "Spawn " should be added to the front of the value + const spawnEggIndex = uniqueParts.indexOf('spawn_egg') + const isSpawnEgg = uniqueParts.length === 2 && spawnEggIndex >= 0 + if (isSpawnEgg) uniqueParts.slice(spawnEggIndex, spawnEggIndex + 1) + + // 3. If there is still multiple parts left, search for the part with a namespaced identifier, as that is commonly the bit being translated (e.g. "minecraft:pig" -> "Pig") + if (uniqueParts.length > 1) { + const id = uniqueParts.find((part) => part.includes(':')) + if (id) uniqueParts = [id] + } + + // 4. Hopefully there is only one part left now, if there isn't, the first value will be used. If the value is a namespace (contains a colon), remove the namespace, then capitalise and propose + if (!uniqueParts[0]) return '' + + if (uniqueParts[0].includes(':')) + uniqueParts[0] = uniqueParts[0].split(':').pop() ?? '' + const translation = `${isSpawnEgg ? 'Spawn ' : ''}${uniqueParts[0] + .split('_') + .map((val) => `${val[0].toUpperCase()}${val.slice(1)}`) + .join(' ')}` + + return translation +} diff --git a/src/components/Languages/Language.ts b/src/components/Languages/Language.ts index 9d17094d2..dc82d9829 100644 --- a/src/components/Languages/Language.ts +++ b/src/components/Languages/Language.ts @@ -9,6 +9,7 @@ export interface IAddLanguageOptions { config: languages.LanguageConfiguration tokenProvider: any completionItemProvider?: languages.CompletionItemProvider + codeActionProvider?: languages.CodeActionProvider } export abstract class Language { @@ -23,6 +24,7 @@ export abstract class Language { config, tokenProvider, completionItemProvider, + codeActionProvider, }: IAddLanguageOptions) { this.id = id @@ -49,6 +51,10 @@ export abstract class Language { completionItemProvider ) ) + if (codeActionProvider) + this.disposables.push( + languages.registerCodeActionProvider(id, codeActionProvider) + ) }) } diff --git a/src/components/Languages/LanguageManager.ts b/src/components/Languages/LanguageManager.ts index 24ceef43f..b1e0d6d33 100644 --- a/src/components/Languages/LanguageManager.ts +++ b/src/components/Languages/LanguageManager.ts @@ -4,10 +4,9 @@ import { McfunctionLanguage } from './Mcfunction' import { MoLangLanguage } from './MoLang' export class LanguageManager { - public readonly mcfunction = new McfunctionLanguage() - protected otherLanguages = new Set([ new MoLangLanguage(), new LangLanguage(), + new McfunctionLanguage(), ]) } diff --git a/src/components/Languages/Mcfunction.ts b/src/components/Languages/Mcfunction.ts index f8f3ad155..2a0dd36ef 100644 --- a/src/components/Languages/Mcfunction.ts +++ b/src/components/Languages/Mcfunction.ts @@ -12,9 +12,9 @@ import './Mcfunction/WithinJson' import { tokenProvider } from './Mcfunction/TokenProvider' import type { Project } from '/@/components/Projects/Project/Project' import { isWithinTargetSelector } from './Mcfunction/TargetSelector/isWithin' -import { FunctionValidator } from '/@/components/Languages/Mcfunction/Validation/Validator' import { proxy } from 'comlink' import { useMonaco } from '../../utils/libs/useMonaco' +import { CommandValidator } from './Mcfunction/Validator' export const config: languages.LanguageConfiguration = { wordPattern: /[aA-zZ]+/, @@ -162,14 +162,15 @@ const loadCommands = async (lang: McfunctionLanguage) => { ) tokenProvider.keywords = commands.map((command) => command) - const targetSelectorArguments = await project.commandData.allSelectorArguments() + const targetSelectorArguments = + await project.commandData.allSelectorArguments() tokenProvider.targetSelectorArguments = targetSelectorArguments lang.updateTokenProvider(tokenProvider) } export class McfunctionLanguage extends Language { - public validator = new FunctionValidator() + protected validator: CommandValidator | undefined constructor() { super({ @@ -216,6 +217,11 @@ export class McfunctionLanguage extends Language { }), disposable ) + + const project = app.project + if (!(project instanceof BedrockProject)) return + + this.validator = new CommandValidator(project.commandData) }) } @@ -228,5 +234,13 @@ export class McfunctionLanguage extends Language { return true } - validate() {} + async validate(model: editor.IModel) { + if (this.validator == undefined) return + + const { editor } = await useMonaco() + + const diagnostics = await this.validator.parse(model.getValue()) + + editor.setModelMarkers(model, this.id, diagnostics) + } } diff --git a/src/components/Languages/Mcfunction/Data.ts b/src/components/Languages/Mcfunction/Data.ts index c69d9463a..32af2555e 100644 --- a/src/components/Languages/Mcfunction/Data.ts +++ b/src/components/Languages/Mcfunction/Data.ts @@ -43,6 +43,7 @@ export type TArgumentType = | 'command' | 'scoreData' | 'subcommand' + | 'integerRange' | `$${string}` /** @@ -57,6 +58,7 @@ export interface ICommandArgument { schemaReference?: string values?: string[] } + isOptional: boolean } export interface ICompletionItem { @@ -152,7 +154,8 @@ export class CommandData extends Signal { (selectorArgument: unknown) => selectorArgument !== undefined ) } - protected async getSubcommands(commandName: string): Promise { + + async getSubcommands(commandName: string): Promise { const schemas = await this.getSchema() return schemas @@ -436,7 +439,7 @@ export class CommandData extends Signal { /** * Given an argument type, test whether a string matches the type */ - protected async isArgumentType( + async isArgumentType( testStr: string, commandArgument: ICommandArgument, commandName?: string @@ -548,7 +551,8 @@ export class CommandData extends Signal { case 'selector': return this.toCompletionItem( ['@a', '@e', '@p', '@s', '@r', '@initiator'], - commandArgument.description + commandArgument.description, + languages.CompletionItemKind.TypeParameter ) case 'boolean': return this.toCompletionItem( @@ -572,7 +576,8 @@ export class CommandData extends Signal { if (commandArgument.additionalData?.values) return this.toCompletionItem( commandArgument.additionalData.values, - commandArgument.description + commandArgument.description, + languages.CompletionItemKind.Constant ) else if (commandArgument.additionalData?.schemaReference) return this.toCompletionItem( @@ -581,7 +586,8 @@ export class CommandData extends Signal { commandArgument.additionalData.schemaReference ).map(({ value }) => value) ), - commandArgument.description + commandArgument.description, + languages.CompletionItemKind.Constant ) else return [] } @@ -609,12 +615,15 @@ export class CommandData extends Signal { ? (await this.getSubcommands(commandName)).map( (command) => command.commandName ) - : [] + : [], + undefined, + languages.CompletionItemKind.Constant ) case 'integerRange': return this.toCompletionItem( ['0', '1', '2', '3', '..0', '0..', '0..1'], - commandArgument.description + commandArgument.description, + languages.CompletionItemKind.Value ) } diff --git a/src/components/Languages/Mcfunction/Validation/Error.ts b/src/components/Languages/Mcfunction/Validation/Error.ts deleted file mode 100644 index 800283ae5..000000000 --- a/src/components/Languages/Mcfunction/Validation/Error.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class SmartError { - constructor( - public value: string | string[], - public start: number = 0, - public end: number = 0 - ) {} -} diff --git a/src/components/Languages/Mcfunction/Validation/Token.ts b/src/components/Languages/Mcfunction/Validation/Token.ts deleted file mode 100644 index 48bb96d25..000000000 --- a/src/components/Languages/Mcfunction/Validation/Token.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class Token { - constructor( - public value: string, - public type: string, - public start: number = 0, - public end: number = 0 - ) {} -} diff --git a/src/components/Languages/Mcfunction/Validation/Validator.ts b/src/components/Languages/Mcfunction/Validation/Validator.ts deleted file mode 100644 index 11818a14e..000000000 --- a/src/components/Languages/Mcfunction/Validation/Validator.ts +++ /dev/null @@ -1,1786 +0,0 @@ -import { SmartError } from './Error' -import { SmartWarning } from './Warning' -import { RefSchema } from '/@/components/JSONSchema/Schema/Ref' -import { Token } from './Token' -import { BedrockProject } from '/@/components/Projects/Project/BedrockProject' -import { markRaw } from 'vue' -import { SchemaManager } from '/@/components/JSONSchema/Manager' -import { App } from '/@/App' - -export class FunctionValidator { - protected validCommands: Array = [] - protected validSelectorArgs: Array = [] - protected commandData: any | undefined - protected generalCommandData: any | undefined - protected generalSelectorArgsData: any | undefined - - public loadedCommandData = false - - public blockStateData: any | undefined - - constructor() {} - - protected DoesStringArrayMatchArray( - array1: Array, - array2: Array - ) { - if (array1.length != array2.length) { - return false - } - - for (let i = 0; i < array1.length; i++) { - if (array1[i] != array2[i]) { - return false - } - } - - return true - } - - protected LateLoadData() { - setTimeout(() => { - if (!this.blockStateData) { - let blockStateDataLoaded = markRaw( - SchemaManager.request( - 'file:///data/packages/minecraftBedrock/schema/general/vanilla/blockState.json' - ) - ) - - if (blockStateDataLoaded.type) { - this.blockStateData = blockStateDataLoaded - } else { - this.LateLoadData() - } - } - }, 1000) - } - - public async LoadCommandData() { - this.loadedCommandData = true - - const app = await App.getApp() - const project = await app.project - - if (project instanceof BedrockProject) { - this.commandData = project.commandData - } - - this.validCommands = [] - this.validSelectorArgs = [] - - this.generalCommandData = [] - this.generalSelectorArgsData = [] - - let foundTypes: string[] = [] - - if (this.commandData) { - for (let v = 0; v < this.commandData._data.vanilla.length; v++) { - const vanilla = this.commandData._data.vanilla[v] - - for (let i = 0; i < vanilla.commands.length; i++) { - if ( - !this.validCommands.includes( - vanilla.commands[i].commandName - ) - ) { - this.validCommands.push(vanilla.commands[i].commandName) - } - - this.generalCommandData.push(markRaw(vanilla.commands[i])) - } - - if (vanilla.selectorArguments) { - for (let i = 0; i < vanilla.selectorArguments.length; i++) { - if ( - !this.validSelectorArgs.includes( - vanilla.selectorArguments[i].argumentName - ) - ) { - this.validSelectorArgs.push( - vanilla.selectorArguments[i].argumentName - ) - } - - this.generalSelectorArgsData.push( - markRaw(vanilla.selectorArguments[i]) - ) - } - } - } - } else { - console.error('Unable to load commands.json') - } - - let blockStateDataLoaded = markRaw( - SchemaManager.request( - 'file:///data/packages/minecraftBedrock/schema/general/vanilla/blockState.json' - ) - ) - - if (blockStateDataLoaded.type) { - this.blockStateData = blockStateDataLoaded - } else { - this.LateLoadData() - } - } - - public async GetDocs(command: string = '') { - if (this.commandData) { - for (let i = 0; i < this.generalCommandData.length; i++) { - if (this.generalCommandData[i].commandName == command) { - return this.generalCommandData[i].description - } - } - } else { - console.error('Unable to load commands.json') - } - - return 'Unable to get docs for this command!' - } - - protected isInt(n: string) { - return /^-?\d+$/.test(n) - } - - protected isFloat(n: string) { - return /^\d+\.\d+$/.test(n) - } - - specialSymbols = [ - '[', - ']', - '!', - '=', - '@', - '~', - '^', - '!', - ',', - '{', - '}', - '.', - ':', - ] - - whiteSpace = [' ', '\t', '\n', '\r'] - - selectorTargets = ['a', 's', 'p', 'r', 'e'] - - protected ParseDirty(dirtyString: Token) { - let readStart = 0 - let readEnd = dirtyString.value.length - - let foundTokens: Token[] = [] - - let lastUnexpected = -1 - - while (readStart < dirtyString.value.length) { - let found = false - let added = true - let shouldCombine = false - - while (readEnd > readStart && !found) { - let read = dirtyString.value.substring(readStart, readEnd) - - if (this.specialSymbols.includes(read)) { - foundTokens.push( - new Token(read, 'Symbol', readStart, readEnd) - ) - found = true - } else if (this.isInt(read)) { - foundTokens.push( - new Token(read, 'Integer', readStart, readEnd) - ) - found = true - shouldCombine = true - } else if (this.isFloat(read)) { - foundTokens.push( - new Token(read, 'Float', readStart, readEnd) - ) - found = true - } else if (read == 'true' || read == 'false') { - foundTokens.push( - new Token(read, 'Boolean', readStart, readEnd) - ) - found = true - } else if (read == ' ') { - foundTokens.push( - new Token(read, 'Space', readStart, readEnd) - ) - found = true - } - - readEnd-- - } - - if (found) { - if (lastUnexpected != -1) { - if (added) { - if (shouldCombine) { - let whatToAdd = - foundTokens[foundTokens.length - 1].value - - foundTokens.splice( - foundTokens.length - 1, - 1, - new Token( - dirtyString.value.substring( - lastUnexpected, - readStart - ) + whatToAdd, - 'String', - lastUnexpected, - readStart + whatToAdd.length - ) - ) - - if (foundTokens.length - 3 >= 0) { - if ( - foundTokens[foundTokens.length - 3].type == - 'Integer' - ) { - foundTokens.splice( - foundTokens.length - 3, - 2, - new Token( - foundTokens[foundTokens.length - 3] - .value + - foundTokens[ - foundTokens.length - 2 - ].value, - 'String', - foundTokens[ - foundTokens.length - 3 - ].start, - foundTokens[ - foundTokens.length - 2 - ].end - ) - ) - } - } - } else { - foundTokens.splice( - foundTokens.length - 1, - 0, - new Token( - dirtyString.value.substring( - lastUnexpected, - readStart - ), - 'String', - lastUnexpected, - readStart - ) - ) - - if (foundTokens.length - 3 >= 0) { - if ( - foundTokens[foundTokens.length - 3].type == - 'Integer' - ) { - foundTokens.splice( - foundTokens.length - 3, - 2, - new Token( - foundTokens[foundTokens.length - 3] - .value + - foundTokens[ - foundTokens.length - 2 - ].value, - 'String', - foundTokens[ - foundTokens.length - 3 - ].start, - foundTokens[ - foundTokens.length - 2 - ].end - ) - ) - } - } - } - } else { - foundTokens.push( - new Token( - dirtyString.value.substring( - lastUnexpected, - readEnd - ), - 'String', - lastUnexpected, - readStart - ) - ) - - if (foundTokens.length - 3 >= 0) { - if ( - foundTokens[foundTokens.length - 3].type == - 'Integer' - ) { - foundTokens.splice( - foundTokens.length - 3, - 2, - new Token( - foundTokens[foundTokens.length - 3] - .value + - foundTokens[foundTokens.length - 2] - .value, - 'String', - foundTokens[ - foundTokens.length - 3 - ].start, - foundTokens[foundTokens.length - 2].end - ) - ) - } - } - } - - lastUnexpected = -1 - } - } else { - if (lastUnexpected == -1) { - lastUnexpected = readStart - } - } - - readStart = readEnd + 1 - readEnd = dirtyString.value.length - } - - if (lastUnexpected != -1) { - foundTokens.push( - new Token( - dirtyString.value.substring(lastUnexpected, readEnd), - 'String', - lastUnexpected, - readEnd - ) - ) - - if (foundTokens.length - 3 >= 0) { - if (foundTokens[foundTokens.length - 3].type == 'Integer') { - foundTokens.splice( - foundTokens.length - 3, - 2, - new Token( - foundTokens[foundTokens.length - 3].value + - foundTokens[foundTokens.length - 2].value, - 'String', - foundTokens[foundTokens.length - 3].start, - foundTokens[foundTokens.length - 2].end - ) - ) - } - } - - lastUnexpected = -1 - } - - return foundTokens - } - - protected MatchTypes(currentType: string, targetType: string) { - if (currentType == targetType) { - return true - } - - switch (targetType) { - case 'string': - if (currentType == 'String') { - return true - } - break - case 'number': - if (currentType == 'Integer') { - return true - } - break - case 'selector': - if ( - currentType == 'Selector' || - currentType == 'String' || - currentType == 'Long String' || - currentType == 'Complex Selector' - ) { - return true - } - break - case 'coordinate': - if (currentType == 'Integer' || currentType == 'Position') { - return true - } - break - case 'boolean': - if (currentType == 'Boolean') { - return true - } - break - case 'jsonData': - if (currentType == 'JSON') { - return true - } - break - case 'scoreData': - if (currentType == 'Score Data') { - return true - } - break - case 'Score': - if (currentType == 'Range' || currentType == 'Integer') { - return true - } - break - case 'blockState': - if (currentType == 'Block State') { - return true - } - break - } - - return false - } - - protected ValidateSelector( - tokens: Token[], - start: number, - end: number - ): any { - let errors: SmartError[] = [] - let warnings: SmartError[] = [] - - if (tokens.length == 0) { - errors.push(new SmartError('selectors.emptyComplex', start, end)) - - return [errors, warnings] - } - - let confirmedAtributes: string[] = [] - - for (let i = 0; i < tokens.length / 4; i++) { - const offset = i * 4 - - if (tokens[offset].type != 'String') { - errors.push( - new SmartError( - 'selectors.expectedStringAsAttribute', - tokens[offset].start, - tokens[offset].end - ) - ) - - return [errors, warnings] - } - - let targetAtribute = tokens[offset].value - let found = false - let argData = null - - for (let j = 0; j < this.generalSelectorArgsData.length; j++) { - const selectorArgument = this.generalSelectorArgsData[j] - - if (selectorArgument.argumentName == targetAtribute) { - argData = selectorArgument - found = true - break - } - } - - if (!found) { - errors.push( - new SmartError( - [ - 'selectors.invalidSelectorAttribute.part1', - '$' + targetAtribute, - 'selectors.invalidSelectorAttribute.part2', - ], - tokens[offset].start, - tokens[offset].end - ) - ) - - return [errors, warnings] - } - - if (1 + offset >= tokens.length) { - errors.push( - new SmartError( - 'common.expectedEquals', - tokens[offset].start, - tokens[offset].end - ) - ) - - return [errors, warnings] - } - - if ( - !(tokens[1 + offset].value == '=' && tokens[1].type == 'Symbol') - ) { - errors.push( - new SmartError( - 'common.expectedEquals', - tokens[offset + 1].start, - tokens[offset + 1].end - ) - ) - - return [errors, warnings] - } - - if (2 + offset >= tokens.length) { - errors.push( - new SmartError( - 'common.expectedValue', - tokens[offset + 1].start, - tokens[offset + 1].end - ) - ) - - return [errors, warnings] - } - - let negated = false - let value = tokens[2 + offset] - - if ( - tokens[2 + offset].value == '!' && - tokens[2 + offset].type == 'Symbol' - ) { - value = tokens[3 + offset] - negated = true - } - - if (argData.additionalData) { - if (!argData.additionalData.supportsNegation && negated) { - errors.push( - new SmartError( - [ - 'selectors.unsupportedNegation.part1', - '$' + targetAtribute, - 'selectors.unsupportedNegation.part2', - ], - tokens[2 + offset].start, - value.end - ) - ) - - return [errors, warnings] - } - - if ( - argData.additionalData.multipleInstancesAllowed == - 'never' && - confirmedAtributes.includes(targetAtribute) - ) { - errors.push( - new SmartError( - [ - 'selectors.multipleInstancesNever.part1', - '$' + targetAtribute, - 'selectors.multipleInstancesNever.part2', - ], - tokens[offset].start, - value.end - ) - ) - - return [errors, warnings] - } - - if ( - argData.additionalData.multipleInstancesAllowed == - 'whenNegated' && - !negated && - confirmedAtributes.includes(targetAtribute) - ) { - errors.push( - new SmartError( - [ - 'selectors.multipleInstancesNegated.part1', - '$' + targetAtribute, - 'selectors.multipleInstancesNegated.part1', - ], - tokens[offset].start, - value.end - ) - ) - - return [errors, warnings] - } - - let targetType = argData.type - - if (targetAtribute == 'name') { - targetType = 'selector' - } - - if (!this.MatchTypes(value.type, targetType)) { - errors.push( - new SmartError( - [ - 'common.expectedType.part1', - '$' + targetType, - 'common.expectedType.part2', - '$' + value.type, - 'common.expectedType.part3', - ], - value.start, - value.end - ) - ) - - return [errors, warnings] - } - - if (argData.additionalData.values) { - if (!argData.additionalData.values.includes(value.value)) { - errors.push( - new SmartError( - [ - 'selectors.valueNotValid.part1', - '$' + value.value, - 'selectors.valueNotValid.part2', - ], - value.start, - value.end - ) - ) - - return [errors, warnings] - } - } - - if (argData.additionalData.schemaReference) { - let referencePath = argData.additionalData.schemaReference - - let refSchema = new RefSchema( - referencePath, - '$ref', - referencePath - ) - - let schemaReference = refSchema.getCompletionItems({}) - - let foundSchema = false - - for (let j = 0; j < schemaReference.length; j++) { - if (schemaReference[j].value == value.value) { - foundSchema = true - } - } - - if (!foundSchema) { - //Warning maybe from wrong addon - if (targetAtribute == 'family') { - warnings.push( - new SmartWarning( - [ - 'schema.familyNotFound.part1', - '$' + value.value, - 'schema.familyNotFound.part2', - ], - value.start, - value.end - ) - ) - } else if (targetAtribute == 'type') { - warnings.push( - new SmartWarning( - [ - 'schema.typeNotFound.part1', - '$' + value.value, - 'schema.typeNotFound.part2', - ], - value.start, - value.end - ) - ) - } else if (targetAtribute == 'tag') { - warnings.push( - new SmartWarning( - [ - 'schema.tagNotFound.part1', - '$' + value.value, - 'schema.tagNotFound.part2', - ], - value.start, - value.end - ) - ) - } else { - warnings.push( - new SmartWarning( - [ - 'schema.schemaValueNotFound.part1', - '$' + value.value, - 'schema.schemaValueNotFound.part2', - ], - value.start, - value.end - ) - ) - } - } - } - } - - let possibleCommaPos = 3 - - if (negated) { - possibleCommaPos = 4 - } - - if (possibleCommaPos + offset < tokens.length) { - if ( - !( - tokens[possibleCommaPos + offset].value == ',' && - tokens[possibleCommaPos + offset].type == 'Symbol' - ) - ) { - errors.push( - new SmartError( - 'common.expectedComma', - tokens[possibleCommaPos + offset].start, - tokens[possibleCommaPos + offset].end - ) - ) - return [errors, warnings] - } - } - } - - return [errors, warnings] - } - - protected ValidateJSON(tokens: Token[]) { - let errors: string[] = [] - let warnings: SmartError[] = [] - - let reconstructedJSON = '{' - - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].type == 'Long String') { - reconstructedJSON += '"' + tokens[i].value + '"' - } else { - reconstructedJSON += tokens[i].value - } - } - - reconstructedJSON += '}' - - try { - JSON.parse(reconstructedJSON) - } catch (e) { - errors.push('jsonNotValid') - } - - return [errors, warnings] - } - - protected ValidateScoreData( - tokens: Token[], - start: number, - end: number - ): any { - let errors: SmartError[] = [] - let warnings: SmartError[] = [] - - if (tokens.length == 0) { - //Unexpected empty score data - errors.push(new SmartError('scoreData.empty', start, end)) - return [errors, warnings] - } - - let confirmedValues: string[] = [] - - for (let i = 0; i < tokens.length / 4; i++) { - const offset = i * 4 - - let targetValue = tokens[offset].value - - if (tokens[offset].type != 'String') { - errors.push( - new SmartError( - 'scoreData.expectedStringAsAttribute', - tokens[offset].start, - tokens[offset].end - ) - ) - - return [errors, warnings] - } - - if (1 + offset >= tokens.length) { - errors.push( - new SmartError( - 'common.expectedEquals', - tokens[offset].start, - tokens[offset].end - ) - ) - - return [errors, warnings] - } - - if ( - !(tokens[1 + offset].value == '=' && tokens[1].type == 'Symbol') - ) { - errors.push( - new SmartError( - 'common.expectedEquals', - tokens[offset + 1].start, - tokens[offset + 1].end - ) - ) - - return [errors, warnings] - } - - if (2 + offset >= tokens.length) { - errors.push( - new SmartError( - 'common.expectedValue', - tokens[offset + 1].start, - tokens[offset + 1].end - ) - ) - - return [errors, warnings] - } - - let value = tokens[2 + offset] - - if (!this.MatchTypes(value.type, 'Score')) { - errors.push( - new SmartError( - [ - 'scoreData.invalidType.part1', - '$' + value.type, - 'scoreData.invalidType.part2', - ], - tokens[offset + 2].start, - tokens[offset + 2].end - ) - ) - - return [errors, warnings] - } - - if (confirmedValues.includes(value.value)) { - errors.push( - new SmartError( - 'scoreData.repeat', - tokens[offset + 2].start, - tokens[offset + 2].end - ) - ) - - return [errors, warnings] - } - - if (3 + offset < tokens.length) { - if ( - !( - tokens[3 + offset].value == ',' && - tokens[3 + offset].type == 'Symbol' - ) - ) { - errors.push( - new SmartError( - 'common.expectedComa', - tokens[offset + 3].start, - tokens[offset + 3].end - ) - ) - - return [errors, warnings] - } - } - - confirmedValues.push(targetValue) - } - - return [errors, warnings] - } - - protected ValidateBlockState(tokens: Token[]) { - let errors: string[] = [] - let warnings: SmartError[] = [] - - if (tokens.length == 0) { - //Empty Block States Are Supported! - return [errors, warnings] - } - - let confirmedValues: string[] = [] - - for (let i = 0; i < tokens.length / 4; i++) { - const offset = i * 4 - - let targetValue = tokens[0 + offset].value - - if (tokens[0 + offset].type != 'Long String') { - errors.push('blockStateExpectedLongStringAsValue') - return [errors, warnings] - } - - if (1 + offset >= tokens.length) { - //Error expected ':' - errors.push('expectedColonButNothing') - return [errors, warnings] - } - - if ( - !(tokens[1 + offset].value == ':' && tokens[1].type == 'Symbol') - ) { - //Error expected ':' - errors.push('expectedColon') - return [errors, warnings] - } - - if (2 + offset >= tokens.length) { - //Error expected value - errors.push('expectedValueButNothing') - return [errors, warnings] - } - - let value = tokens[2 + offset] - - if (this.blockStateData) { - let targetData - - let properties = Object.getOwnPropertyNames( - this.blockStateData.properties - ) - - for (let j = 0; j < properties.length; j++) { - if (properties[j] == targetValue) { - targetData = this.blockStateData.properties[ - properties[j] - ] - - targetData['name'] = properties[j] - break - } - } - - let targetValues = targetData.enum - - for (let j = 0; j < targetValues.length; j++) { - targetValues[j] = targetValues[j].toString() - } - - if (!targetValues.includes(value.value)) { - errors.push( - 'invalidBlockStateValue.part1' + - value.value + - 'invalidBlockStateValue.part2' - ) - return [errors, warnings] - } - } - - if (confirmedValues.includes(value.value)) { - errors.push('repeatOfBlockState') - return [errors, warnings] - } - - if (3 + offset < tokens.length) { - if ( - tokens[3 + offset].value == ',' && - tokens[3 + offset].type == 'Symbol' - ) { - //Error expected ',' - errors.push('expectedComa') - return [errors, warnings] - } - } - - confirmedValues.push(targetValue) - } - - return [errors, warnings] - } - - public ValidateCommand( - command: string | null, - commandTokens: Token[] | null = null - ) { - let errors: SmartError[] = [] - let warnings: any[] = [] - - if (!this.blockStateData) { - warnings.push(new SmartWarning('data.missingData', 0, 0)) - } - - //Seperate into strings by quotes for parsing - - let splitStrings - - let inString = false - - let tokens: Token[] = [] - - let baseCommand: Token - - if (!commandTokens) { - if (command?.startsWith('/')) command = command.substring(1) - - splitStrings = command!.split('"') - - let lastChange = -1 - - for (let i = 0; i < splitStrings.length; i++) { - if (!inString) { - let tokenStart = 0 - - if (tokens.length > 0) { - tokenStart = tokens[tokens.length - 1].end + 1 - } - - tokens.push( - new Token( - splitStrings[i], - 'Dirty', - tokenStart, - tokenStart + splitStrings[i].length - ) - ) - } else { - let tokenStart = 0 - - if (tokens.length > 0) { - tokenStart = tokens[tokens.length - 1].end + 1 - } - - tokens.push( - new Token( - splitStrings[i], - 'Long String', - tokenStart, - tokenStart + splitStrings[i].length - ) - ) - } - - inString = !inString - lastChange = i - } - - if (!inString) { - errors.push( - new SmartError( - 'common.unclosedString', - tokens[lastChange].start, - tokens[tokens.length - 1].end - ) - ) - return [errors, warnings] - } - - //Tokenize strings for validation - - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].type == 'Dirty') { - let parsedTokens: Token[] = this.ParseDirty(tokens[i]) - - for (let j = 0; j < parsedTokens.length; j++) { - tokens.splice(i + j + 1, 0, parsedTokens[j]) - } - - tokens.splice(i, 1) - - i += parsedTokens.length - 1 - } - } - - if (tokens.length > 0) { - if (tokens[0].type == 'Space') { - errors.push( - new SmartError( - 'common.spaceAtStart', - tokens[lastChange].start, - tokens[lastChange].end - ) - ) - - return [errors, warnings] - } - - if (tokens.length == 0) { - errors.push(new SmartError('commands.empty')) - - return [errors, warnings] - } - - baseCommand = tokens[0] - - //Test for basic command - baseCommand = tokens.shift()! - - if (!this.validCommands.includes(baseCommand.value)) { - errors.push( - new SmartError( - [ - 'command.invalid.part1', - '$' + baseCommand.value, - 'command.invalid.part2', - ], - baseCommand.start, - baseCommand.end - ) - ) - - return [errors, warnings] - } - - //Construct Identifiers - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == ':') { - if (i + 1 >= tokens.length) { - errors.push( - new SmartError( - 'common.exptectedColon', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if (i - 1 < 0) { - errors.push( - new SmartError( - 'identifiers.missingNamespace', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if (tokens[i - 1].type == 'Space') { - errors.push( - new SmartError( - 'identifiers.missingNamespace', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - let firstValue = tokens[i - 1] - let secondValue = tokens[i + 1] - - if ( - firstValue.type == 'String' && - secondValue.type == 'String' - ) { - tokens[i - 1] = new Token( - firstValue.value + ':' + secondValue.value, - 'String' - ) - - tokens.splice(i, 2) - } - } - } - - //Remove Spaces - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Space') { - tokens.splice(i, 1) - i-- - } - } - - //Construct Positions - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if ( - token.type == 'Symbol' && - (token.value == '~' || token.value == '^') - ) { - if (i + 1 < tokens.length) { - let numberTarget = tokens[i + 1] - - if (numberTarget.type == 'Integer') { - tokens[i] = new Token( - token.value + numberTarget.value, - 'Position' - ) - - tokens.splice(i + 1, 1) - } else { - tokens[i] = new Token(token.value, 'Position') - } - } - - tokens[i] = new Token(token.value, 'Position') - } - } - - //Construct json - let inJSON = false - let JSONToReconstruct: Token[] = [] - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '{') { - if (inJSON) { - errors.push( - new SmartError( - 'common.unexpectedOpenBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } else { - inJSON = true - } - } else if (token.type == 'Symbol' && token.value == '}') { - if (inJSON) { - inJSON = false - - let result = this.ValidateJSON(JSONToReconstruct) - - if (result[0].length == 0) { - let startingPoint = - i - JSONToReconstruct.length - 1 - - tokens.splice( - startingPoint, - JSONToReconstruct.length + 2, - (tokens[startingPoint] = new Token( - tokens[startingPoint].value, - 'JSON' - )) - ) - } - - JSONToReconstruct = [] - } else { - errors.push( - new SmartError( - 'common.unexpectedClosedCurlyBracket', - token.start, - token.end - ) - ) - return [errors, warnings] - } - } else { - if (inJSON) { - JSONToReconstruct.push(token) - } - } - } - - //Construct ranges - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '.') { - if (i + -1 < 0) { - errors.push( - new SmartError( - 'ranges.missingFirstNumber', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if (i + 1 >= tokens.length) { - errors.push( - new SmartError( - 'ranges.missingDot', - tokens[i - 1].start, - token.end - ) - ) - - return [errors, warnings] - } - - if (i + 2 >= tokens.length) { - errors.push( - new SmartError( - 'ranges.missingSecondNumber', - tokens[i - 1].start, - tokens[i + 1].end - ) - ) - - return [errors, warnings] - } - - let firstNum = tokens[i - 1] - let secondNum = tokens[i + 2] - let dot = tokens[i + 1] - - if (firstNum.type != 'Integer') { - errors.push( - new SmartError( - 'ranges.missingFirstNumber', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if (!(dot.value == '.' && dot.type == 'Symbol')) { - errors.push( - new SmartError( - 'ranges.missingDot', - tokens[i - 1].start, - token.end - ) - ) - - return [errors, warnings] - } - - if (secondNum.type != 'Integer') { - errors.push( - new SmartError( - 'ranges.missingSecondNumber', - tokens[i - 1].start, - tokens[i + 1].end - ) - ) - - return [errors, warnings] - } - - tokens[i - 1] = new Token( - firstNum.value + ' ' + secondNum.value, - 'Range' - ) - - tokens.splice(i, 3) - } - } - - //Construct Score Data - let inScoreData = false - let scoreDataToReconstruct: Token[] = [] - - let startBracketPos = 0 - let endBracketPos = 0 - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '{') { - if (inScoreData) { - errors.push( - new SmartError( - 'common.unexpectedOpenCurlyBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } else { - inScoreData = true - - startBracketPos = token.start - } - } else if (token.type == 'Symbol' && token.value == '}') { - if (inScoreData) { - inScoreData = false - - endBracketPos = token.end - - let result = this.ValidateScoreData( - scoreDataToReconstruct, - startBracketPos, - endBracketPos - ) - - errors = errors.concat(result[0]) - warnings = warnings.concat(result[1]) - - if (errors.length > 0) { - return [errors, warnings] - } - - let startingPoint = - i - scoreDataToReconstruct.length - 1 - - tokens.splice( - startingPoint, - scoreDataToReconstruct.length + 2, - (tokens[startingPoint] = new Token( - tokens[startingPoint].value, - 'Score Data' - )) - ) - - scoreDataToReconstruct = [] - } else { - errors.push( - new SmartError( - 'common.unexpectedClosedCurlyBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - } else { - if (inScoreData) { - scoreDataToReconstruct.push(token) - } - } - } - - //Construct Basic Selectors - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '@') { - if (i + 1 >= tokens.length) { - errors.push( - new SmartError( - 'selectors.expectedLetterAfterAt', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - let selectorTarget = tokens[i + 1] - - if (selectorTarget.type != 'String') { - errors.push( - new SmartError( - 'selectors.expectedLetterAfterAt', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if ( - !this.selectorTargets.includes(selectorTarget.value) - ) { - errors.push( - new SmartError( - [ - 'selectors.invalid.part1', - '$' + selectorTarget.value, - 'iselectors.invalid.part2', - ], - token.start, - tokens[i + 1].end - ) - ) - - return [errors, warnings] - } - - tokens[i] = new Token( - '@' + selectorTarget.value, - 'Selector' - ) - - tokens.splice(i + 1, 1) - } - } - - //Construct Block States - let inBlockState = false - let blockStateToReconstruct: Token[] = [] - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '[') { - if (inBlockState) { - errors.push( - new SmartError( - 'common.unexpectedOpenSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } else { - inBlockState = true - } - } else if (token.type == 'Symbol' && token.value == ']') { - if (inBlockState) { - inBlockState = false - - let result = this.ValidateBlockState( - blockStateToReconstruct - ) - - warnings = warnings.concat(result[1]) - - if (result[0].length == 0) { - let startingPoint = - i - blockStateToReconstruct.length - 1 - - tokens.splice( - startingPoint, - blockStateToReconstruct.length + 2, - (tokens[startingPoint] = new Token( - tokens[startingPoint].value, - 'Block State' - )) - ) - } - - blockStateToReconstruct = [] - } else { - errors.push( - new SmartError( - 'common.unexpectedClosedSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - } else { - if (inBlockState) { - blockStateToReconstruct.push(token) - } - } - } - - //Construct Complex Selectors - let inSelector = false - let selectorToReconstruct: Token[] = [] - - startBracketPos = 0 - endBracketPos = 0 - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i] - - if (token.type == 'Symbol' && token.value == '[') { - if (inSelector) { - errors.push( - new SmartError( - 'common.unexpectedOpenSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } else { - if (i - 1 < 0) { - errors.push( - new SmartError( - 'identifiers.selectorNotBeforeOpenSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - if (tokens[i - 1].type != 'Selector') { - errors.push( - new SmartError( - 'identifiers.selectorNotBeforeOpenSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - - inSelector = true - - startBracketPos = token.start - } - } else if (token.type == 'Symbol' && token.value == ']') { - if (inSelector) { - inSelector = false - endBracketPos = token.end - - let result = this.ValidateSelector( - selectorToReconstruct, - startBracketPos, - endBracketPos - ) - - errors = errors.concat(result[0]) - warnings = warnings.concat(result[1]) - - if (errors.length > 0) { - return [errors, warnings] - } - - let startingPoint = - i - selectorToReconstruct.length - 2 - - tokens.splice( - startingPoint, - selectorToReconstruct.length + 3, - (tokens[startingPoint] = new Token( - tokens[startingPoint].value, - 'Complex Selector' - )) - ) - - selectorToReconstruct = [] - } else { - errors.push( - new SmartError( - 'common.unexpectedClosedSquareBracket', - token.start, - token.end - ) - ) - - return [errors, warnings] - } - } else { - if (inSelector) { - selectorToReconstruct.push(token) - } - } - } - } - } else { - tokens = commandTokens! - - baseCommand = tokens.shift()! - } - - //Validate Command Argument Types - let possibleCommandVariations = [] - - for (let i = 0; i < this.generalCommandData.length; i++) { - if (this.generalCommandData[i].commandName == baseCommand!.value) { - possibleCommandVariations.push( - this.generalCommandData[i].arguments - ) - } - } - - let lastValidVariations: Token[] = Array.from(possibleCommandVariations) - - for (let i = 0; i < tokens.length; i++) { - const arg = tokens[i] - - for (let j = 0; j < possibleCommandVariations.length; j++) { - const variation = possibleCommandVariations[j] - - if (variation.length <= i) { - if (variation[variation.length - 1].type != 'command') { - possibleCommandVariations.splice(j, 1) - j-- - continue - } - } else { - if (variation[i].type == 'command') { - let commandSlicedTokens = tokens.slice(i, tokens.length) - - let result = this.ValidateCommand( - null, - commandSlicedTokens - ) - - if (result[0].length > 0) { - possibleCommandVariations.splice(j, 1) - j-- - } - } else { - if (!this.MatchTypes(arg.type, variation[i].type)) { - possibleCommandVariations.splice(j, 1) - j-- - } - } - } - } - - if (possibleCommandVariations.length == 0) { - errors.push( - new SmartError( - [ - 'arguments.noneValid.part1', - '$' + (i + 1), - 'arguments.noneValid.part2', - '$' + arg.type, - 'arguments.noneValid.part3', - ], - arg.start, - arg.end - ) - ) - - return [errors, warnings] - } - - lastValidVariations = Array.from(possibleCommandVariations) - } - - //Check for missing and optional parameters - for (let j = 0; j < possibleCommandVariations.length; j++) { - const variation = possibleCommandVariations[j] - - if (tokens.length < variation.length) { - if (!variation[tokens.length].isOptional) { - possibleCommandVariations.splice(j, 1) - j-- - } - - continue - } - } - - if (possibleCommandVariations.length == 0) { - errors.push( - new SmartError( - [ - 'arguments.noneValidEnd.part1', - '$' + tokens.length, - 'arguments.noneValidEnd.part2', - ], - tokens[tokens.length - 1].start, - tokens[tokens.length - 1].end - ) - ) - - return [errors, warnings] - } - - return [errors, warnings] - } -} diff --git a/src/components/Languages/Mcfunction/Validation/Warning.ts b/src/components/Languages/Mcfunction/Validation/Warning.ts deleted file mode 100644 index 7a55076f8..000000000 --- a/src/components/Languages/Mcfunction/Validation/Warning.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class SmartWarning { - constructor( - public value: string | string[], - public start: number, - public end: number - ) {} -} diff --git a/src/components/Languages/Mcfunction/Validator.ts b/src/components/Languages/Mcfunction/Validator.ts new file mode 100644 index 000000000..9d5c83fce --- /dev/null +++ b/src/components/Languages/Mcfunction/Validator.ts @@ -0,0 +1,1056 @@ +import { + tokenizeCommand, + tokenizeTargetSelector, + castType, +} from 'bridge-common-utils' +import { CommandData, ICommandArgument } from './Data' +import type { editor } from 'monaco-editor' +import { useMonaco } from '/@/utils/libs/useMonaco' +import { RefSchema } from '/@/components/JSONSchema/Schema/Ref' +import { + translateWithInsertions as twi, + translate as t, +} from '/@/components/Locales/Manager' + +export class CommandValidator { + protected commandData: CommandData + + constructor(commandData: CommandData) { + this.commandData = commandData + } + + protected async parseSubcommand( + baseCommandName: string, + leftTokens: { + startColumn: number + endColumn: number + word: string + }[] + ): Promise<{ + passed: boolean + argumentsConsumedCount?: number + warnings: editor.IMarkerData[] + }> { + const { MarkerSeverity } = await useMonaco() + + const subcommandName = leftTokens[0] + + let subcommandDefinitions = ( + await this.commandData.getSubcommands(baseCommandName) + ).filter((definition) => definition.commandName == subcommandName.word) + + if (subcommandDefinitions.length == 0) + return { + passed: false, + warnings: [], + } + + let passedSubcommandDefinition + + let warnings: editor.IMarkerData[] = [] + + // Loop over every subcommand definition to check for a matching one + for (const definition of subcommandDefinitions) { + let definitionWarnings = [] + + let failed = false + + // Fail if there is not enought tokens to satisfy the definition + if (leftTokens.length - 1 <= definition.arguments.length) { + continue + } + + // Loop over every argument + for (let j = 0; j < definition.arguments.length; j++) { + const argument = leftTokens[j + 1] + const targetArgument = definition.arguments[j] + + let argumentType = await this.commandData.isArgumentType( + argument.word, + targetArgument + ) + + if ( + targetArgument.type == 'blockState' && + argumentType == 'full' && + !(await this.parseBlockState(argument.word)) + ) + argumentType = 'none' + + // Fail if type does not match + if (argumentType != 'full') { + failed = true + + break + } + + if (targetArgument.additionalData) { + // Fail if there are additional values that are not met + if ( + targetArgument.additionalData.values && + !targetArgument.additionalData.values.includes( + argument.word + ) + ) { + failed = true + + break + } + + // Warn if unknown schema value + if (targetArgument.additionalData.schemaReference) { + const referencePath = + targetArgument.additionalData.schemaReference + + const schemaReference = new RefSchema( + referencePath, + '$ref', + referencePath + ).getCompletionItems({}) + + if ( + !schemaReference.find( + (reference) => reference.value == argument.word + ) + ) { + definitionWarnings.push({ + severity: MarkerSeverity.Warning, + message: twi( + 'validation.mcfunction.unknownSchema.name', + [`"${argument.word}"`] + ), + startLineNumber: -1, + startColumn: argument.startColumn + 1, + endLineNumber: -1, + endColumn: argument.endColumn + 1, + }) + } + } + } + } + + // Only add definition if it is longer since it's the most likely correct one + if ( + !failed && + (!passedSubcommandDefinition || + passedSubcommandDefinition.arguments.length < + definition.arguments.length) + ) { + passedSubcommandDefinition = definition + warnings = definitionWarnings + } + } + + if (!passedSubcommandDefinition) { + return { + passed: false, + warnings: [], + } + } else { + return { + passed: true, + argumentsConsumedCount: + passedSubcommandDefinition.arguments.length, + warnings, + } + } + } + + protected async parseScoreData(token: string): Promise { + if (!token.startsWith('{')) return false + + if (!token.endsWith('}')) return false + + let pieces = token.substring(1, token.length - 1).split(',') + + // Check for weird comma syntax ex: ,, + if (pieces.find((argument) => argument == '')) return false + + for (const piece of pieces) { + const scoreName = piece.split('=')[0] + const scoreValue = piece.split('=').slice(1).join('=') + + if (!scoreValue) return false + + let argumentType = await this.commandData.isArgumentType( + scoreValue, + { + argumentName: 'scoreData', + description: 'scoreDataParser', + type: 'integerRange', + isOptional: false, + } + ) + + if (argumentType != 'full') return false + } + + return true + } + + protected async parseBlockState(token: string): Promise { + if (!token.startsWith('[')) return false + + if (!token.endsWith(']')) return false + + const pieces = token.substring(1, token.length - 1).split(',') + + // Check for weird comma syntax ex: ,, + if (pieces.find((argument) => argument == '')) return false + + for (const piece of pieces) { + const scoreName = piece.split(':')[0] + const scoreValue = piece.split(':').slice(1).join(':') + + if (!scoreValue) return false + + const isString = + (await this.commandData.isArgumentType(scoreValue, { + argumentName: 'scoreData', + description: 'scoreDataParser', + type: 'string', + isOptional: false, + })) == 'full' && + (/([a-zA-Z])/.test(scoreValue) || scoreValue == '""') && + ((scoreValue.startsWith('"') && scoreValue.endsWith('"')) || + (!scoreValue.startsWith('"') && + !scoreValue.endsWith('"'))) && + (scoreValue.split('"').length - 1 == 2 || + scoreValue.split('"').length - 1 == 0) + + const isNumber = + (await this.commandData.isArgumentType(scoreValue, { + argumentName: 'scoreData', + description: 'scoreDataParser', + type: 'number', + isOptional: false, + })) == 'full' + if (!isString && !isNumber) return false + } + + return true + } + + protected async parseSelector(selectorToken: { + startColumn: number + endColumn: number + word: string + }): Promise<{ + passed: boolean + diagnostic?: editor.IMarkerData + warnings: editor.IMarkerData[] + }> { + const { MarkerSeverity } = await useMonaco() + + let warnings: editor.IMarkerData[] = [] + + let baseSelector = selectorToken.word.substring(0, 2) + + // Check for base selector, we later check @i to be @initiator + if (!['@a', '@p', '@r', '@e', '@s', '@i'].includes(baseSelector)) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.invalidSelectorBase.name', + [`"${baseSelector}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + if (baseSelector == '@i') { + if ( + selectorToken.word.substring(0, '@initiator'.length) != + '@initiator' + ) { + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.invalidSelectorBase.name', + [`"${baseSelector}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + } + + baseSelector = '@initiator' + } + + // If the selector is merely the base we can just pass + if (baseSelector == selectorToken.word) + return { + passed: true, + warnings: [], + } + + if (selectorToken.word[baseSelector.length] != '[') + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.unexpectedSymbol.name', + [`"${selectorToken.word[baseSelector.length]}"`, '"["'] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + if (!selectorToken.word.endsWith(']')) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.unexpectedSymbol.name', + [`"${selectorToken.word[baseSelector.length]}"`, '"]"'] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + let selectorArguments = selectorToken.word + .substring(baseSelector.length + 1, selectorToken.word.length - 1) + .split(',') + + // Check for weird comma syntax ex: ,, + if (selectorArguments.find((argument) => argument == '')) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.unexpectedSymbol.name', + [ + `"${selectorToken.word[baseSelector.length]}"`, + `"${t( + 'validation.mcfunction.tokens.selectorArgument' + )}"`, + ] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + const selectorArgumentsSchema = + await this.commandData.getSelectorArgumentsSchema() + + // Store argument names that can't be used multiple times or where not negated when they need to be + let canNotUseNames = [] + + for (const argument of selectorArguments) { + // Fail if there is for somereason no = + if (argument.split('=').length - 1 < 1) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.unexpectedSymbol.name', + [`"${argument}"`, `"="`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + let argumentName = argument.split('=')[0] + let argumentValue = argument.split('=').slice(1).join('=') + + const argumentSchema = selectorArgumentsSchema.find( + (schema) => schema.argumentName == argumentName + ) + + if (!argumentSchema) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.invalidSelectorArgument.name', + [`"${argumentName}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + const negated = argumentValue.startsWith('!') + const canNotUse = canNotUseNames.includes(argumentName) + + // Fail if negated and shouldn't be + if ( + negated && + (!argumentSchema.additionalData || + !argumentSchema.additionalData.supportsNegation) + ) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.argumentNoSupport.name', + [ + `"${argumentName}"`, + t('validation.mcfunction.conditions.negation'), + ] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + // Remove ! at the beginning + if (negated) + argumentValue = argumentValue.substring(1, argumentValue.length) + + // Check if this type should not be used again + if (canNotUse) { + if (!argumentSchema.additionalData) + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.argumentNoSupport.name', + [ + `"${argumentName}"`, + t( + 'validation.mcfunction.conditions.multipleInstances' + ), + ] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + + if ( + argumentSchema.additionalData.multipleInstancesAllowed == + 'whenNegated' + ) { + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.argumentNoSupport.bothConditions', + [`"${argumentName}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + } + + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.argumentNoSupport.name', + [ + `"${argumentName}"`, + t( + 'validation.mcfunction.conditions.multipleInstances' + ), + ] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + } + + let argumentType = await this.commandData.isArgumentType( + argumentValue, + argumentSchema + ) + + // We need to parse scoreData on its own because the normal argument type checker seems to not work on it + if ( + argumentSchema.type == 'scoreData' && + (await this.parseScoreData(argumentValue)) + ) + argumentType = 'full' + + // Fail if type does not match, NOTE: Should check scoredata in future when implemented + if (argumentType != 'full') { + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.invalidSelectorArgumentValue.name', + [`"${argumentValue}"`, `"${argumentName}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + } + + if (argumentSchema.additionalData) { + // Fail if there are additional values that are not met + if ( + argumentSchema.additionalData.values && + !argumentSchema.additionalData.values.includes( + argumentValue + ) + ) { + return { + passed: false, + diagnostic: { + severity: MarkerSeverity.Error, + message: twi( + 'validation.mcfunction.invalidSelectorArgumentValue.name', + [`"${argumentValue}"`, `"${argumentName}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }, + warnings: [], + } + } + + // Warn if unknown schema value + if (argumentSchema.additionalData.schemaReference) { + const referencePath = + argumentSchema.additionalData.schemaReference + + const schemaReference = new RefSchema( + referencePath, + '$ref', + referencePath + ).getCompletionItems({}) + + if ( + !schemaReference.find( + (reference) => reference.value == argumentValue + ) + ) { + warnings.push({ + severity: MarkerSeverity.Warning, + message: twi( + 'validation.mcfunction.unknownSchemaInArgument.name', + [`"${argumentValue}"`, `"${argumentName}"`] + ), + startLineNumber: -1, + startColumn: selectorToken.startColumn + 1, + endLineNumber: -1, + endColumn: selectorToken.endColumn + 1, + }) + } + } + } + + if ( + !argumentSchema.additionalData || + !argumentSchema.additionalData.multipleInstancesAllowed || + argumentSchema.additionalData.multipleInstancesAllowed == + 'never' || + (argumentSchema.additionalData.multipleInstancesAllowed == + 'whenNegated' && + !negated) + ) { + canNotUseNames.push(argumentName) + } + } + + return { + passed: true, + warnings, + } + } + + protected async parseCommand( + line: string | undefined, + tokens: any[], + offset: number + ): Promise { + const { MarkerSeverity } = await useMonaco() + + let diagnostics: editor.IMarkerData[] = [] + let warnings: editor.IMarkerData[] = [] + + if (line) tokens = tokenizeCommand(line).tokens + + // Reconstruct JSON because tokenizer doesn't handle this well + for (let i = 0; i < tokens.length; i++) { + if (tokens[i - 1]) { + // if we get a case where tokens are like "property", :"value" then we combine them + if ( + tokens[i].word.startsWith(':') && + tokens[i - 1].word[tokens[i - 1].word.length - 1] == '"' + ) { + tokens.splice(i - 1, 2, { + startColumn: tokens[i - 1].startColumn, + endColumn: tokens[i].endColumn, + word: tokens[i - 1].word + tokens[i].word, + }) + + i-- + + continue + } + + // if we get a case where tokens are like ["state":"a","state":"b" then we combine them + if ( + tokens[i].word.startsWith(',') && + tokens[i - 1].word[tokens[i - 1].word.length - 1] == '"' + ) { + tokens.splice(i - 1, 2, { + startColumn: tokens[i - 1].startColumn, + endColumn: tokens[i].endColumn, + word: tokens[i - 1].word + tokens[i].word, + }) + + i-- + + continue + } + + // add the beginning and ending of a json data or scoreData together + if ( + (tokens[i].word == '}' && + tokens[i - 1].word.startsWith('{')) || + (tokens[i].word == ']' && + tokens[i - 1].word.startsWith('[')) + ) { + tokens.splice(i - 1, 2, { + startColumn: tokens[i - 1].startColumn, + endColumn: tokens[i].endColumn, + word: tokens[i - 1].word + tokens[i].word, + }) + + i-- + + continue + } + } + } + + const commandName = tokens[0] + + // If first word is empty then this is an empty line + if (!commandName || commandName.word == '') return diagnostics + + if ( + !(await this.commandData.allCommands()).includes(commandName.word) + ) { + diagnostics.push({ + severity: MarkerSeverity.Error, + message: twi('validation.mcfunction.unknownCommand.name', [ + `"${commandName.word}"`, + ]), + startLineNumber: -1, + startColumn: commandName.startColumn + 1, + endLineNumber: -1, + endColumn: commandName.endColumn + 1, + }) + + // The command is not valid; it makes no sense to continue validating this line + return diagnostics + } + + // Remove empty tokens as to not confuse the argument checker + tokens = tokens.filter((token) => token.word != '') + + if (tokens.length < 2) { + diagnostics.push({ + severity: MarkerSeverity.Error, + message: twi('validation.mcfunction.missingArguments.name', [ + `"${commandName.word}"`, + ]), + startLineNumber: -1, + startColumn: commandName.startColumn + 1, + endLineNumber: -1, + endColumn: commandName.endColumn + 1, + }) + + // The command is not valid; it makes no sense to continue validating this line + return diagnostics + } + + // If this is a say command we ignore validating arguments since they are all strings + if (commandName.word == 'say') return diagnostics.concat(warnings) + + let definitions = await this.commandData.getCommandDefinitions( + commandName.word, + false + ) + + // We only need to record the error of the most farthest in token because that is the most likely variation the user was attempting to type + let lastTokenError = 0 + let lastTokenErrorReason = '' + + let longestPassLength = -1 + + // Loop over every definition and test for validness + for (let j = 0; j < definitions.length; j++) { + let requiredArgurmentsCount = 0 + + for ( + requiredArgurmentsCount = 0; + requiredArgurmentsCount < definitions[j].arguments.length; + requiredArgurmentsCount++ + ) { + if ( + definitions[j].arguments[requiredArgurmentsCount].isOptional + ) + break + } + + let failed = false + let failedLongest = false + + let definitionWarnings: editor.IMarkerData[] = [] + let definitionDiagnostics: editor.IMarkerData[] = [] + + // Loop over every token that is not the command name + let targetArgumentIndex = 0 + for (let k = 1; k < tokens.length; k++) { + // Fail if there are not enough arguments in definition + if (definitions[j].arguments.length <= targetArgumentIndex) { + definitions.splice(j, 1) + + j-- + + if (lastTokenError < k) { + failedLongest = true + + lastTokenError = k + + lastTokenErrorReason = twi( + 'validation.mcfunction.invalidArgument.name', + [`"${tokens[k].word}"`] + ) + } + + failed = true + + break + } + + const argument = tokens[k] + + const targetArgument = + definitions[j].arguments[targetArgumentIndex] + + if (targetArgument.type == 'subcommand') { + const result = await this.parseSubcommand( + commandName.word, + tokens.slice(k, tokens.length) + ) + + definitionWarnings = definitionWarnings.concat( + result.warnings + ) + + if (result.passed) { + // Skip over tokens consumed in the subcommand validation + k += result.argumentsConsumedCount! + + // If there allows multiple subcommands keep going untill a subcommand fails + if (targetArgument.allowMultiple) { + let nextResult: { + passed: boolean + argumentsConsumedCount?: number + warnings: editor.IMarkerData[] + } = { + passed: true, + argumentsConsumedCount: 0, + warnings: [], + } + + while (nextResult.passed) { + nextResult = await this.parseSubcommand( + commandName.word, + tokens.slice(k + 1, tokens.length) + ) + + if (nextResult.passed) { + definitionWarnings = + definitionWarnings.concat( + nextResult.warnings + ) + + k += nextResult.argumentsConsumedCount! + 1 + } + } + } + + targetArgumentIndex++ + + continue + } else { + // Fail because subcommand doesn't match any definitions + definitions.splice(j, 1) + + j-- + + if (lastTokenError < k) { + failedLongest = true + + lastTokenError = k + + lastTokenErrorReason = twi( + 'validation.mcfunction.invalidArgument.name', + [`"${tokens[k].word}"`] + ) + } + + failed = true + + break + } + } + + // If we need to validate a command we just validate all the other tokens and returns because we won't + // need to check any more tokens as they will be consumed within the new command + if (targetArgument.type == 'command') { + const leftTokens = tokens.slice(k, tokens.length) + + const result = await this.parseCommand( + undefined, + leftTokens, + offset + targetArgumentIndex + ) + + definitionDiagnostics = definitionDiagnostics.concat(result) + + targetArgumentIndex++ + + break + } + + let argumentType = await this.commandData.isArgumentType( + argument.word, + targetArgument, + commandName.word + ) + + if ( + targetArgument.type == 'blockState' && + argumentType == 'full' && + !(await this.parseBlockState(argument.word)) + ) + argumentType = 'none' + + // Fail if type does not match + if (argumentType != 'full') { + definitions.splice(j, 1) + + j-- + + if (lastTokenError < k) { + failedLongest = true + + lastTokenError = k + + lastTokenErrorReason = twi( + 'validation.mcfunction.invalidArgument.name', + [`"${tokens[k].word}"`] + ) + } + + failed = true + + break + } + + // Validate selector but don't completely fail if selector fail so rest of command can validate as well + if (targetArgument.type == 'selector') { + const result = await this.parseSelector(argument) + + if (result.diagnostic) + definitionDiagnostics.push(result.diagnostic) + + definitionWarnings = definitionWarnings.concat( + result.warnings + ) + } + + if (targetArgument.additionalData) { + // Fail if there are additional values that are not met + if ( + targetArgument.additionalData.values && + !targetArgument.additionalData.values.includes( + argument.word + ) + ) { + definitions.splice(j, 1) + + j-- + + if (lastTokenError < k) { + failedLongest = true + + lastTokenError = k + + lastTokenErrorReason = twi( + 'validation.mcfunction.invalidArgument.name', + [`"${tokens[k].word}"`] + ) + } + + failed = true + + break + } + + // Warn if unknown schema value + if (targetArgument.additionalData.schemaReference) { + const referencePath = + targetArgument.additionalData.schemaReference + + const schemaReference = new RefSchema( + referencePath, + '$ref', + referencePath + ).getCompletionItems({}) + + if ( + !schemaReference.find( + (reference) => reference.value == argument.word + ) + ) { + definitionWarnings.push({ + severity: MarkerSeverity.Warning, + message: twi( + 'validation.mcfunction.unknownSchema.name', + [`"${tokens[k].word}"`] + ), + startLineNumber: -1, + startColumn: argument.startColumn + 1, + endLineNumber: -1, + endColumn: argument.endColumn + 1, + }) + } + } + } + + targetArgumentIndex++ + } + + // Skip if already failed in case this leaves an undefined reference + if (failed) { + if (failedLongest) diagnostics = definitionDiagnostics + + continue + } + + // Fail if there are not enough tokens to satisfy definition + if (targetArgumentIndex < requiredArgurmentsCount) { + definitions.splice(j, 1) + + j-- + + if (lastTokenError < tokens.length - 1) { + lastTokenError = tokens.length - 1 + + lastTokenErrorReason = twi( + 'validation.mcfunction.missingArguments.name', + [`"${commandName.word}"`] + ) + } + + // Continue to not add warnings to the diagnostics + continue + } + + if (targetArgumentIndex < longestPassLength) break + + longestPassLength = targetArgumentIndex + + diagnostics = definitionDiagnostics + warnings = definitionWarnings + } + + if (definitions.length == 0) { + diagnostics.push({ + severity: MarkerSeverity.Error, + message: lastTokenErrorReason, + startLineNumber: -1, + startColumn: tokens[lastTokenError].startColumn + 1, + endLineNumber: -1, + endColumn: tokens[lastTokenError].endColumn + 1, + }) + + // Return here since we don't want warnings added + return diagnostics + } + + return diagnostics.concat(warnings) + } + + async parse(content: string) { + // Split content into lines + const lines = content.split('\n') + const diagnostics: editor.IMarkerData[] = [] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (line[0] == '#') continue + + const results = await this.parseCommand(line, [], 0) + + for (const diagnostic of results) { + diagnostic.startLineNumber = i + 1 + diagnostic.endLineNumber = i + 1 + + diagnostics.push(diagnostic) + } + } + + return diagnostics + } +} diff --git a/src/components/Locales/Manager.ts b/src/components/Locales/Manager.ts index a2ff2c334..9691c1a88 100644 --- a/src/components/Locales/Manager.ts +++ b/src/components/Locales/Manager.ts @@ -105,6 +105,17 @@ export function translate(key?: string) { return LocaleManager.translate(key) } +export function translateWithInsertions(key?: string, insert?: string[]) { + let translation = LocaleManager.translate(key) + if (!insert) return translation + + for (let i = 0; i <= insert.length; i++) { + translation = translation?.replace(`{{$${i + 1}}}`, insert[i]) + } + + return translation +} + function clone(obj: any) { if (typeof window.structuredClone === 'function') return window.structuredClone(obj) diff --git a/src/components/OutputFolders/ComMojang/ProjectLoader.ts b/src/components/OutputFolders/ComMojang/ProjectLoader.ts index 2532fca78..b9a8aef6c 100644 --- a/src/components/OutputFolders/ComMojang/ProjectLoader.ts +++ b/src/components/OutputFolders/ComMojang/ProjectLoader.ts @@ -1,7 +1,7 @@ import { compareVersions } from 'bridge-common-utils' import { AnyDirectoryHandle } from '../../FileSystem/Types' import { App } from '/@/App' -import { loadAsDataURL } from '/@/utils/loadAsDataUrl' +import { loadHandleAsDataURL } from '/@/utils/loadAsDataUrl' export interface IComMojangPack { type: 'behaviorPack' | 'resourcePack' @@ -17,6 +17,8 @@ export interface IComMojangProject { } export class ComMojangProjectLoader { + protected cachedProjects: IComMojangProject[] | null = null + constructor(protected app: App) {} get comMojang() { @@ -26,7 +28,13 @@ export class ComMojangProjectLoader { return this.app.comMojang.fileSystem } + clearCache() { + this.cachedProjects = null + } + async loadProjects() { + if (this.cachedProjects) return this.cachedProjects + if ( !this.comMojang.setup.hasFired || !this.comMojang.status.hasComMojang @@ -34,10 +42,13 @@ export class ComMojangProjectLoader { return [] const behaviorPacks = await this.loadPacks('development_behavior_packs') + + // Fast path: No need to load resource packs if there are no behavior packs + if (behaviorPacks.size === 0) return [] const resourcePacks = await this.loadPacks('development_resource_packs') const projects: IComMojangProject[] = [] - for (const behaviorPack of behaviorPacks) { + for (const behaviorPack of behaviorPacks.values()) { const dependencies = behaviorPack.manifest?.dependencies if (!dependencies) { projects.push({ @@ -47,11 +58,12 @@ export class ComMojangProjectLoader { continue } - const matchingRp = resourcePacks.find(({ uuid: rpUuuid }) => - dependencies.find( - ({ uuid: depUuid }: any) => !!depUuid && depUuid === rpUuuid - ) - ) + let matchingRp + for (const dep of dependencies) { + matchingRp = resourcePacks.get(dep.uuid) + if (matchingRp) break + } + if (!matchingRp) continue projects.push({ name: behaviorPack.directoryHandle.name, @@ -59,23 +71,28 @@ export class ComMojangProjectLoader { }) } + this.cachedProjects = projects return projects } async loadPacks( folderName: 'development_behavior_packs' | 'development_resource_packs' ) { + const packs = new Map() const storePackDir = await this.fileSystem .getDirectoryHandle(folderName) .catch(() => null) - if (!storePackDir) return [] - const packs: IComMojangPack[] = [] + if (!storePackDir) return packs for await (const packHandle of storePackDir.values()) { if (packHandle.kind === 'file') continue - const manifest = await this.fileSystem - .readJSON(`${folderName}/${packHandle.name}/manifest.json`) + + const manifest = await packHandle + .getFileHandle('manifest.json') + .then((fileHandle) => + this.fileSystem.readJsonHandle(fileHandle) + ) .catch(() => null) if (!manifest) continue @@ -84,12 +101,12 @@ export class ComMojangProjectLoader { // Check whether BP/RP is part of a v2 project if (this.isV2Project(manifest)) continue - const packIcon = await loadAsDataURL( - `${folderName}/${packHandle.name}/pack_icon.png`, - this.fileSystem - ).catch(() => null) + const packIcon = await packHandle + .getFileHandle('pack_icon.png') + .then((fileHandle) => loadHandleAsDataURL(fileHandle)) + .catch(() => null) - packs.push({ + packs.set(uuid, { type: folderName === 'development_behavior_packs' ? 'behaviorPack' @@ -108,7 +125,8 @@ export class ComMojangProjectLoader { isV2Project(manifest: any) { const uuid = manifest?.header?.uuid - const bridgeVersion = manifest?.metadata?.generated_with?.bridge?.pop?.() + const bridgeVersion = + manifest?.metadata?.generated_with?.bridge?.pop?.() if (bridgeVersion && compareVersions(bridgeVersion, '2.0.0', '>=')) return true diff --git a/src/components/PackIndexer/PackIndexer.ts b/src/components/PackIndexer/PackIndexer.ts index fce2df46c..2c04f34de 100644 --- a/src/components/PackIndexer/PackIndexer.ts +++ b/src/components/PackIndexer/PackIndexer.ts @@ -1,45 +1,51 @@ import { App } from '/@/App' -import { WorkerManager } from '/@/components/Worker/Manager' -import { proxy } from 'comlink' +import { proxy, Remote, wrap } from 'comlink' import { settingsState } from '/@/components/Windows/Settings/SettingsState' -import type { PackIndexerService } from './Worker/Main' +import { PackIndexerService as TPackIndexerService } from './Worker/Main' import PackIndexerWorker from './Worker/Main?worker' import { AnyDirectoryHandle } from '../FileSystem/Types' import type { Project } from '/@/components/Projects/Project/Project' import { Mutex } from '../Common/Mutex' +import { Signal } from '../Common/Event/Signal' +import { Task } from '../TaskManager/Task' -export class PackIndexer extends WorkerManager< - typeof PackIndexerService, - PackIndexerService, - boolean, - readonly [string[], string[]] -> { +const PackIndexerService = wrap( + new PackIndexerWorker() +) + +const taskOptions = { + icon: 'mdi-flash-outline', + name: 'taskManager.tasks.packIndexing.title', + description: 'taskManager.tasks.packIndexing.description', +} + +export class PackIndexer extends Signal<[string[], string[]]> { protected isPackIndexerFree = new Mutex() + protected _service: Remote | null = null + protected task?: Task + constructor( protected project: Project, protected baseDirectory: AnyDirectoryHandle ) { - super({ - icon: 'mdi-flash-outline', - name: 'taskManager.tasks.packIndexing.title', - description: 'taskManager.tasks.packIndexing.description', - }) - } - - createWorker() { - this.worker = new PackIndexerWorker() + super() } - deactivate() { - super.deactivate() + get service() { + if (!this._service) + throw new Error(`Accessing service without service being defined`) + return this._service } - protected async start(forceRefreshCache: boolean) { + async activate(forceRefreshCache: boolean) { console.time('[TASK] Indexing Packs (Total)') - await this.isPackIndexerFree.lock() + + this.isPackIndexerFree.lock() + const app = this.project.app + this.task = app.taskManager.create(taskOptions) // Instaniate the worker TaskService - this._service = await new this.workerClass!( + this._service = await new PackIndexerService( this.baseDirectory, this.project.app.fileSystem.baseDirectory, { @@ -55,7 +61,7 @@ export class PackIndexer extends WorkerManager< ) // Listen to task progress and update UI - await this.service.on( + await this._service?.on( proxy(([current, total]) => { this.task?.update(current, total) }), @@ -63,15 +69,27 @@ export class PackIndexer extends WorkerManager< ) // Start service - const [changedFiles, deletedFiles] = await this.service.start( + const [changedFiles, deletedFiles] = await this._service?.start( forceRefreshCache ) - await this.service.disposeListeners() + await this._service?.disposeListeners() + this.task.complete() this.isPackIndexerFree.unlock() + console.timeEnd('[TASK] Indexing Packs (Total)') + + // Only dispatch signal if service wasn't disposed in the meantime + if (this._service) this.dispatch([changedFiles, deletedFiles]) return [changedFiles, deletedFiles] } + async deactivate() { + this.resetSignal() + await this._service?.disposeListeners() + this._service = null + this.task?.complete() + } + async updateFile( filePath: string, fileContent?: string, diff --git a/src/components/Projects/Project/BedrockProject.ts b/src/components/Projects/Project/BedrockProject.ts index c22047143..5a2e6ceee 100644 --- a/src/components/Projects/Project/BedrockProject.ts +++ b/src/components/Projects/Project/BedrockProject.ts @@ -5,12 +5,12 @@ import { createFromGeometry } from '/@/components/Editors/EntityModel/create/fro import { createFromClientEntity } from '/@/components/Editors/EntityModel/create/fromClientEntity' import { createFromEntity } from '/@/components/Editors/EntityModel/create/fromEntity' import { ParticlePreviewTab } from '/@/components/Editors/ParticlePreview/ParticlePreview' -// import { FunctionValidatorTab } from '../../Editors/FunctionValidator/Tab' import { BlockModelTab } from '/@/components/Editors/BlockModel/Tab' import { CommandData } from '/@/components/Languages/Mcfunction/Data' // import { WorldTab } from '/@/components/BedrockWorlds/Render/Tab' import { FileTab } from '../../TabSystem/FileTab' import { HTMLPreviewTab } from '../../Editors/HTMLPreview/HTMLPreview' +import { LangData } from '/@/components/Languages/Lang/Data' const bedrockPreviews: ITabPreviewConfig[] = [ { @@ -55,6 +55,7 @@ const bedrockPreviews: ITabPreviewConfig[] = [ export class BedrockProject extends Project { commandData = new CommandData() + langData = new LangData() onCreate() { bedrockPreviews.forEach((tabPreview) => @@ -85,6 +86,7 @@ export class BedrockProject extends Project { }) this.commandData.loadCommandData('minecraftBedrock') + this.langData.loadLangData('minecraftBedrock') } getCurrentDataPackage() { diff --git a/src/components/Projects/Project/Project.ts b/src/components/Projects/Project/Project.ts index 7f6355433..b62b31c59 100644 --- a/src/components/Projects/Project/Project.ts +++ b/src/components/Projects/Project/Project.ts @@ -277,29 +277,27 @@ export abstract class Project { this.snippetLoader.activate() } - deactivate(isReload = false) { + async deactivate(isReload = false) { if (!isReload) this.tabSystems.forEach((tabSystem) => tabSystem.deactivate()) this.typeLoader.deactivate() - this.packIndexer.deactivate() this.jsonDefaults.deactivate() this.extensionLoader.disposeAll() - this.snippetLoader.deactivate() - } - disposeWorkers() { - this.packIndexer.dispose() + + await Promise.all([ + this.packIndexer.deactivate(), + this.snippetLoader.deactivate(), + ]) } dispose() { - this.disposeWorkers() this.tabSystems.forEach((tabSystem) => tabSystem.dispose()) this.extensionLoader.disposeAll() } async refresh() { this.app.packExplorer.refresh() - this.deactivate(true) - this.disposeWorkers() + await this.deactivate(true) await this.activate(true) } @@ -582,13 +580,15 @@ export abstract class Project { } async recompile(forceStartIfActive = true) { + if (this.isVirtualProject) return + this._compilerService = markRaw( await this.createDashService('development') ) + await this._compilerService.setup() if (forceStartIfActive && this.isActiveProject) { await this.fileSystem.writeFile('.bridge/.restartWatchMode', '') - await this.compilerReady.fired await this.compilerService.start([], []) } else { await this.fileSystem.writeFile('.bridge/.restartWatchMode', '') diff --git a/src/components/Projects/ProjectChooser/ProjectChooser.ts b/src/components/Projects/ProjectChooser/ProjectChooser.ts index 3db10add8..0f29b0aa8 100644 --- a/src/components/Projects/ProjectChooser/ProjectChooser.ts +++ b/src/components/Projects/ProjectChooser/ProjectChooser.ts @@ -21,6 +21,7 @@ export class ProjectChooserWindow extends NewBaseWindow { protected experimentalToggles: (IExperimentalToggle & { isActive: boolean })[] = [] + protected comMojangProjectLoader protected state: IProjectChooserState = reactive({ ...super.getState(), @@ -28,10 +29,19 @@ export class ProjectChooserWindow extends NewBaseWindow { currentProject: undefined, }) - constructor() { + constructor(app: App) { super(ProjectChooserComponent, false, true) this.state.actions.push( + new SimpleAction({ + icon: 'mdi-refresh', + name: 'general.reload', + color: 'accent', + isDisabled: () => this.state.isLoading, + onTrigger: () => { + this.reload() + }, + }), new SimpleAction({ icon: 'mdi-import', name: 'actions.importBrproject.name', @@ -52,6 +62,7 @@ export class ProjectChooserWindow extends NewBaseWindow { }, }) ) + this.comMojangProjectLoader = markRaw(new ComMojangProjectLoader(app)) this.defineWindow() } @@ -113,9 +124,9 @@ export class ProjectChooserWindow extends NewBaseWindow { }) ) - const comMojangProjects = await new ComMojangProjectLoader( - app - ).loadProjects() + console.time('Load com.mojang projects') + const comMojangProjects = + await this.comMojangProjectLoader.loadProjects() comMojangProjects.forEach((project) => this.addProject(`comMojang/${project.name}`, project.name, { name: project.name, @@ -143,6 +154,7 @@ export class ProjectChooserWindow extends NewBaseWindow { project: markRaw(project), }) ) + console.timeEnd('Load com.mojang projects') this.sidebar.resetSelected() if (app.isNoProjectSelected) this.sidebar.setDefaultSelected() @@ -151,8 +163,22 @@ export class ProjectChooserWindow extends NewBaseWindow { return app.projectManager.selectedProject } + async reload() { + this.state.isLoading = true + + this.comMojangProjectLoader.clearCache() + await this.loadProjects() + + this.state.isLoading = false + } + async open() { - this.state.currentProject = await this.loadProjects() super.open() + + this.state.isLoading = true + console.time('Load projects') + this.state.currentProject = await this.loadProjects() + console.timeEnd('Load projects') + this.state.isLoading = false } } diff --git a/src/components/Projects/ProjectChooser/ProjectChooser.vue b/src/components/Projects/ProjectChooser/ProjectChooser.vue index 036885078..d609f8fb1 100644 --- a/src/components/Projects/ProjectChooser/ProjectChooser.vue +++ b/src/components/Projects/ProjectChooser/ProjectChooser.vue @@ -9,6 +9,7 @@ @closeWindow="onClose" :sidebarItems="sidebar.elements" :actions="state.actions" + :isLoading="state.isLoading" v-model="sidebar.selected" >