├── mxs.png ├── .gitattributes ├── images ├── feature-1.png ├── feature-2.gif └── comment-decor.png ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── src ├── backend │ ├── semanticListener.ts.bak │ ├── diagnostics │ │ ├── ContextLexerErrorListener.ts │ │ ├── CustomErrorStrategy.ts │ │ └── ContextErrorListener.ts │ ├── schemas │ │ ├── mxsTokenDefs.ts │ │ └── mxsSymbolDef.ts │ ├── symbolSearch.ts.bak │ ├── BackendUtils.ts │ ├── symbols │ │ ├── symbolUtils.ts │ │ ├── simpleSymbolProvider.ts │ │ ├── symbolTypes.ts │ │ └── codeCompletionProvider.ts │ └── semantic │ │ ├── simpleSemTokens.ts │ │ └── semanticTokenListener.ts ├── extension.ts ├── Diagnostics.ts ├── RenameProvider.ts ├── ReferenceProvider.ts ├── utils.ts ├── HoverProvider.ts ├── DefinitionProvider.ts ├── SymbolProvider.ts ├── parser │ ├── mxsLexerBase.ts │ ├── multiChannelTokenStream.ts │ ├── mxsParserBase.ts │ └── grammars │ │ └── mxsLexer.g4 ├── CodeLensProvider.ts ├── settings.ts ├── SemanticTokensProvider.ts ├── CompletionItemProvider.ts ├── types.ts ├── FormattingProvider.ts ├── Symbol.ts └── WorkspaceSymbolProvider.ts ├── .vscodeignore ├── .gitignore ├── eslint.config.js ├── LICENSE ├── language-configuration.json ├── esbuild.cjs ├── TextMate-scopes.md ├── snippets └── maxscript.json ├── CHANGELOG.md ├── tokenColorCustomizations-example.jsonc ├── README.md ├── package.json ├── dev-notes └── PERFORMANCE_OPTIMIZATION_GUIDE.md ├── tsconfig.json └── syntaxes └── maxscript.tmLanguage.yaml /mxs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAG87/vscode-maxscript-lsp/HEAD/mxs.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /images/feature-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAG87/vscode-maxscript-lsp/HEAD/images/feature-1.png -------------------------------------------------------------------------------- /images/feature-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAG87/vscode-maxscript-lsp/HEAD/images/feature-2.gif -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "connor4312.esbuild-problem-matchers" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /images/comment-decor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAG87/vscode-maxscript-lsp/HEAD/images/comment-decor.png -------------------------------------------------------------------------------- /src/backend/semanticListener.ts.bak: -------------------------------------------------------------------------------- 1 | //TODO: add semantic analysis support 2 | //references and dependencies 3 | //fileIn 4 | // globals... -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | src/** 6 | out/** 7 | !out/extension.cjs 8 | node_modules/** 9 | scripts/** 10 | .eslintrc.json 11 | tsconfig.json 12 | esbuild.cjs 13 | eslint.config.js 14 | *.md 15 | !README.md 16 | !CHANGELOG.md 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vsix 3 | out/* 4 | dist/* 5 | test/* 6 | /src/parser/grammars/.antlr 7 | .vscode/bookmarks.json 8 | src/parser/mxsLexer.interp 9 | src/parser/mxsLexer.tokens 10 | src/parser/mxsLexer.ts 11 | src/parser/mxsParser.interp 12 | src/parser/mxsParser.tokens 13 | src/parser/mxsParser.ts 14 | src/parser/mxsParserListener.ts 15 | src/parser/mxsParserVisitor.ts 16 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from 'vscode'; 2 | 3 | import { ExtensionHost } from './ExtensionHost.js'; 4 | 5 | // let extensionHost: ExtensionHost; 6 | export const activate = (context: ExtensionContext): void => { 7 | // extensionHost = new ExtensionHost(context); 8 | new ExtensionHost(context); 9 | } 10 | 11 | export const deactivate = () => { 12 | // fsw.dispose(); 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--disable-extensions", 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import tseslint from 'typescript-eslint'; 3 | 4 | import pluginJs from '@eslint/js'; 5 | 6 | export default [ 7 | { files: ["**/*.{js,mjs,cjs,ts}"] }, 8 | { languageOptions: { globals: globals.browser } }, 9 | { 10 | ignores: [ 11 | "src/parser/mxsParser.*", 12 | "src/parser/mxsLexer.*", 13 | "src/parser/mxsParserListener.*", 14 | "src/parser/mxsParserVisitor.*", 15 | "esbuild.cjs", // CommonJS build script 16 | "scripts/**", // Node.js scripts 17 | ] 18 | }, 19 | pluginJs.configs.recommended, 20 | ...tseslint.configs.recommended, 21 | { 22 | rules: { 23 | "no-unused-vars": "off", 24 | "@typescript-eslint/no-unused-vars": "off", 25 | "@typescript-eslint/no-explicit-any": "off", 26 | "no-undef": "warn" 27 | } 28 | } 29 | ]; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Atelier Bump 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Diagnostics.ts: -------------------------------------------------------------------------------- 1 | /* convert diagnostics information to vcode Diagnostic 2 | */ 3 | import { Diagnostic, DiagnosticSeverity } from 'vscode'; 4 | 5 | import { DiagnosticType, IDiagnosticEntry } from './types.js'; 6 | import { Utilities } from './utils.js'; 7 | 8 | const diagnosticTypeMap = new Map(); 9 | 10 | export function diagnosticAdapter(entries: IDiagnosticEntry[]): Diagnostic[] 11 | { 12 | const diagnostics: Diagnostic[] = []; 13 | // Avoid duplicate entries 14 | let tempDiagnostic: IDiagnosticEntry | null = null; 15 | 16 | for (const entry of entries) { 17 | if (tempDiagnostic) { 18 | if (JSON.stringify(entry) === JSON.stringify(tempDiagnostic)) { 19 | tempDiagnostic = entry; 20 | continue; 21 | } 22 | } 23 | 24 | const diagnostic = new Diagnostic( 25 | Utilities.lexicalRangeToRange(entry.range), 26 | entry.message, 27 | diagnosticTypeMap.get(entry.type) 28 | ); 29 | diagnostics.push(diagnostic); 30 | 31 | tempDiagnostic = entry; 32 | } 33 | return diagnostics; 34 | } 35 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "--", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"], 21 | ["~", "~"] 22 | ], 23 | // symbols that can be used to surround a selection 24 | "surroundingPairs": [ 25 | ["{", "}"], 26 | ["[", "]"], 27 | ["(", ")"], 28 | ["\"", "\""], 29 | ["'", "'"], 30 | ["~", "~"] 31 | ], 32 | "indentationRules": { 33 | "increaseIndentPattern": "^((?!--).)*(\\{[^}\"'`]*|\\([^)\"'`]*)$", 34 | "decreaseIndentPattern": "^((?!.*?--*).*\\*/)?\\s*[}\\])].*" 35 | } 36 | } -------------------------------------------------------------------------------- /src/backend/diagnostics/ContextLexerErrorListener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Mike Lischke. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | */ 5 | 6 | import { 7 | ATNSimulator, BaseErrorListener, RecognitionException, Recognizer, 8 | Token, 9 | } from 'antlr4ng'; 10 | 11 | import { DiagnosticType, IDiagnosticEntry } from '../../types.js'; 12 | 13 | export class ContextLexerErrorListener extends BaseErrorListener 14 | { 15 | public constructor(private errorList: IDiagnosticEntry[]) 16 | { 17 | super(); 18 | } 19 | 20 | public override syntaxError(_recognizer: Recognizer, 21 | _offendingSymbol: S | null, line: number, column: number, msg: string, _e: RecognitionException | null): void 22 | { 23 | const error: IDiagnosticEntry = { 24 | type: DiagnosticType.Error, 25 | message: msg, 26 | range: { 27 | start: { 28 | column, 29 | row: line, 30 | }, 31 | end: { 32 | column: column + 1, 33 | row: line, 34 | }, 35 | }, 36 | }; 37 | 38 | this.errorList.push(error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": ["npm: watch:tsc", "npm: watch:esbuild"], 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "watch:esbuild", 20 | "group": "build", 21 | "problemMatcher": "$esbuild-watch", 22 | "isBackground": true, 23 | "label": "npm: watch:esbuild", 24 | "presentation": { 25 | "group": "watch", 26 | "reveal": "never" 27 | } 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "watch:tsc", 32 | "group": "build", 33 | "problemMatcher": "$tsc-watch", 34 | "isBackground": true, 35 | "label": "npm: watch:tsc", 36 | "presentation": { 37 | "group": "watch", 38 | "reveal": "never" 39 | } 40 | }, 41 | { 42 | "type": "npm", 43 | "script": "watch-tests", 44 | "problemMatcher": "$tsc-watch", 45 | "isBackground": true, 46 | "presentation": { 47 | "reveal": "never", 48 | "group": "watchers" 49 | }, 50 | "group": "build" 51 | }, 52 | { 53 | "label": "tasks: watch-tests", 54 | "dependsOn": [ 55 | "npm: watch", 56 | "npm: watch-tests" 57 | ], 58 | "problemMatcher": [] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/RenameProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, Position, ProviderResult, RenameProvider, 3 | TextDocument, Uri, WorkspaceEdit, 4 | } from 'vscode'; 5 | 6 | import { mxsBackend } from './backend/Backend.js'; 7 | import { Utilities } from './utils.js'; 8 | 9 | export class mxsRenameProvider implements RenameProvider 10 | { 11 | public constructor(private backend: mxsBackend) { } 12 | 13 | public provideRenameEdits( 14 | document: TextDocument, 15 | position: Position, 16 | newName: string, 17 | _token: CancellationToken): ProviderResult 18 | { 19 | return new Promise((resolve) => 20 | { 21 | const occurrences = 22 | this.backend.getContext(document.uri.toString()).symbolInfoAtPositionCtxOccurrences( 23 | position.line + 1, 24 | position.character); 25 | 26 | if (occurrences) { 27 | const workspaceEdit = new WorkspaceEdit(); 28 | for (const symbol of occurrences) { 29 | // if (symbol.definition) { 30 | workspaceEdit.replace( 31 | Uri.parse(symbol.source), 32 | Utilities.symbolNameRange(symbol), 33 | newName 34 | ); 35 | // } 36 | } 37 | resolve(workspaceEdit); 38 | } else { 39 | resolve(undefined); 40 | } 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /esbuild.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * esbuild configuration for production builds 3 | * 4 | * This is a CommonJS file (.cjs) because it runs in Node.js directly. 5 | * Development builds use tsc (outputs to out/), production uses esbuild (outputs to dist/). 6 | * 7 | * @type {import('esbuild')} 8 | */ 9 | 10 | const esbuild = require("esbuild"); 11 | 12 | const production = process.argv.includes('--production'); 13 | const watch = process.argv.includes('--watch'); 14 | 15 | /** 16 | * @type {import('esbuild').Plugin} 17 | */ 18 | const esbuildProblemMatcherPlugin = { 19 | name: 'esbuild-problem-matcher', 20 | 21 | setup(build) { 22 | build.onStart(() => { 23 | console.log('[watch] build started'); 24 | }); 25 | build.onEnd((result) => { 26 | result.errors.forEach(({ text, location }) => { 27 | console.error(`✘ [ERROR] ${text}`); 28 | if (location == null) return; 29 | console.error(` ${location.file}:${location.line}:${location.column}:`); 30 | }); 31 | console.log('[watch] build finished'); 32 | }); 33 | }, 34 | }; 35 | 36 | async function main() { 37 | const ctx = await esbuild.context({ 38 | entryPoints: ['src/extension.ts'], 39 | bundle: true, 40 | format: 'cjs', 41 | minify: production, 42 | sourcemap: !production, 43 | sourcesContent: false, 44 | platform: 'node', 45 | outfile: 'out/extension.cjs', 46 | external: ['vscode'], 47 | logLevel: 'silent', 48 | plugins: [ 49 | /* add to the end of plugins array */ 50 | esbuildProblemMatcherPlugin, 51 | ], 52 | }); 53 | if (watch) { 54 | await ctx.watch(); 55 | } else { 56 | await ctx.rebuild(); 57 | await ctx.dispose(); 58 | } 59 | } 60 | 61 | main().catch(e => { 62 | console.error(e); 63 | process.exit(1); 64 | }); 65 | -------------------------------------------------------------------------------- /src/ReferenceProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: 3 | - Fix references for symbols with the same name or referenced from an enclosed construct (like calling a method of a structure that initiated into a variable) 4 | */ 5 | import { 6 | CancellationToken, Location, Position, ProviderResult, 7 | ReferenceContext, ReferenceProvider, TextDocument, Uri, 8 | } from 'vscode'; 9 | 10 | import { mxsBackend } from './backend/Backend.js'; 11 | import { Utilities } from './utils.js'; 12 | 13 | export class mxsReferenceProvider implements ReferenceProvider 14 | { 15 | public constructor(private backend: mxsBackend) { } 16 | 17 | provideReferences( 18 | document: TextDocument, 19 | position: Position, 20 | _context: ReferenceContext, 21 | _token: CancellationToken): ProviderResult 22 | { 23 | return new Promise((resolve) => 24 | { 25 | const occurrences = 26 | this.backend.getContext(document.uri.toString()).symbolInfoAtPositionCtxOccurrences( 27 | position.line + 1, 28 | position.character); 29 | 30 | if (occurrences) { 31 | const result: Location[] = []; 32 | for (const symbol of occurrences) { 33 | if (symbol.definition) { 34 | const location = 35 | new Location( 36 | Uri.parse(symbol.source), 37 | Utilities.symbolNameRange(symbol) 38 | ); 39 | result.push(location); 40 | } 41 | } 42 | resolve(result); 43 | } else { 44 | resolve(undefined); 45 | } 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import { Position, Range, TextDocument } from 'vscode'; 3 | 4 | import { ExtensionHost } from './ExtensionHost.js'; 5 | import { ILexicalRange, ISymbolInfo } from './types.js'; 6 | 7 | export class Utilities 8 | { 9 | public static isLanguageFile(document?: TextDocument | undefined): boolean 10 | { 11 | return document ? document.languageId === ExtensionHost.langSelector.language && 12 | document.uri.scheme === ExtensionHost.langSelector.scheme : false; 13 | } 14 | 15 | public static lexicalRangeToRange(range: ILexicalRange): Range 16 | { 17 | const start = new Position( 18 | range.start.row === 0 ? 0 : range.start.row - 1, 19 | range.start.column 20 | ); 21 | const end = new Position( 22 | range.end.row === 0 ? 0 : range.end.row - 1, 23 | range.end.column 24 | ); 25 | return new Range(start, end); 26 | } 27 | 28 | public static rangeToLexicalRange(range: Range): ILexicalRange 29 | { 30 | return { 31 | start: { 32 | row: range.start.line + 1, 33 | column: range.start.character 34 | }, 35 | end: { 36 | row: range.end.line + 1, 37 | column: range.end.character 38 | } 39 | }; 40 | } 41 | 42 | public static symbolNameRange(symbol: ISymbolInfo): Range 43 | { 44 | return new Range( 45 | symbol.definition!.range.start.row - 1, 46 | symbol.definition!.range.start.column, 47 | symbol.definition!.range.end.row - 1, 48 | symbol.definition!.range.start.column + symbol.name.length, 49 | ); 50 | } 51 | 52 | public static prefixFile = (path: string, prefix: string): string => Path.join(path, '..', prefix + Path.basename(path)); 53 | } -------------------------------------------------------------------------------- /src/HoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, Hover, HoverProvider, MarkdownString, 3 | Position, ProviderResult, TextDocument, 4 | } from 'vscode'; 5 | 6 | import { mxsBackend } from './backend/Backend.js'; 7 | import { 8 | mxsLanguageCompletions, 9 | } from './backend/schemas/mxsCompletions-base.js'; 10 | import { symbolDescriptionFromEnum } from './Symbol.js'; 11 | 12 | export class mxsHoverProvider implements HoverProvider 13 | { 14 | public constructor(private backend: mxsBackend) { } 15 | 16 | provideHover(document: TextDocument, position: Position, _token: CancellationToken): ProviderResult 17 | { 18 | return new Promise((resolve) => 19 | { 20 | const info = this.backend.symbolInfoAtPosition( 21 | document.uri.toString(), 22 | position.line + 1, 23 | position.character 24 | ); 25 | // console.log(info); 26 | if (info) { 27 | // provide hover for API definitions 28 | const mxsReference = mxsLanguageCompletions.has(info.name); 29 | 30 | if (mxsReference) { 31 | resolve(new Hover([ 32 | `**${mxsReference.label.toString()}**`, 33 | `3ds MaxAPI | ${mxsReference.detail}`, 34 | ])); 35 | } else { 36 | // provide symbol definition 37 | const info = this.backend.symbolInfoDefinition( 38 | document.uri.toString(), 39 | position.line + 1, 40 | position.character); 41 | if (info && info.definition) { 42 | const markedStr: MarkdownString = new MarkdownString(`**${symbolDescriptionFromEnum(info.kind)}**\n`) 43 | markedStr.appendCodeblock(info.definition.text, 'maxscript') 44 | resolve(new Hover([ 45 | markedStr 46 | ])); 47 | } else resolve(undefined); 48 | } 49 | } else { 50 | resolve(undefined); 51 | } 52 | }); 53 | } 54 | } -------------------------------------------------------------------------------- /src/DefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: 3 | - Fix definition for symbols with the same name or referenced from an enclosed construct (linke calling a method of a structure that initiated into a variable) 4 | - I should implement a method to derive a reference tree, instead of looking at the symbol table, of find a better implementation of the symbol table, keeping track of references in the listener 5 | - keep track of named symbols, definition, references and aliases (assignations and re-assignation). respect scope 6 | */ 7 | import { 8 | CancellationToken, Definition, DefinitionLink, DefinitionProvider, 9 | Location, Position, ProviderResult, TextDocument, 10 | Uri, 11 | } from 'vscode'; 12 | 13 | import { mxsBackend } from './backend/Backend.js'; 14 | import { Utilities } from './utils.js'; 15 | 16 | export class mxsDefinitionProvider implements DefinitionProvider 17 | { 18 | public constructor(private backend: mxsBackend) { } 19 | 20 | provideDefinition( 21 | document: TextDocument, 22 | position: Position, 23 | token: CancellationToken): ProviderResult 24 | { 25 | return new Promise((resolve) => 26 | { 27 | const info = this.backend.getContext(document.uri.toString())?.symbolDefinition( 28 | position.line + 1, 29 | position.character); 30 | 31 | if (info) { 32 | // VS code shows the text for the range given here on holding ctrl/cmd, which is rather 33 | // useless given that we show this info already in the hover provider. So, in order 34 | // to limit the amount of text we only pass on the smallest range which is possible. 35 | // Yet we need the correct start position to not break the goto-definition feature. 36 | if (info.definition) { 37 | resolve(new Location(Uri.parse(info.source), Utilities.lexicalRangeToRange(info.definition.range))); 38 | } else { 39 | // Empty for built-in entities. 40 | resolve(new Location(Uri.parse(""), new Position(0, 0))); 41 | } 42 | } else resolve(null); 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /TextMate-scopes.md: -------------------------------------------------------------------------------- 1 | * comment.block.documentation.mxs 2 | * comment.block.mxs 3 | * comment.line.mxs 4 | * constant.character.escape.mxs 5 | * constant.language.axis.mxs 6 | * constant.language.boolean.mxs 7 | * constant.language.colour.mxs 8 | * constant.language.math.mxs 9 | * constant.language.null.mxs 10 | * constant.numeric.hex.mxs 11 | 12 | --- 13 | 14 | * constant.numeric.mxs 15 | * documentation.bold.mxs 16 | * documentation.fixme.mxs 17 | * documentation.plain.mxs 18 | * documentation.todo.mxs 19 | * documentation.underline.mxs 20 | * entity.name.entity.mxs 21 | * entity.name.event.mxs 22 | * entity.name.function.mxs 23 | * entity.name.type.mxs 24 | 25 | --- 26 | 27 | * entity.other.event.mxs 28 | * keyword.control.mxs 29 | * keyword.operator.arithmetic.mxs 30 | * keyword.operator.assignment.mxs 31 | * keyword.operator.byref.mxs 32 | * keyword.operator.comparison.mxs 33 | * keyword.operator.logical.mxs 34 | * keyword.operator.mxs 35 | * keyword.operator.relational.mxs 36 | * keyword.other.mxs 37 | 38 | --- 39 | 40 | * meta.array.mxs 41 | * meta.bitarray.mxs 42 | * meta.block.mxs 43 | * meta.entity 44 | * meta.event.mxs 45 | * meta.function.mxs 46 | * meta.point.mxs 47 | * meta.struct.mxs 48 | * punctuation.accessor.mxs 49 | * punctuation.bitrange.mxs 50 | 51 | --- 52 | 53 | * punctuation.definition.comment.mxs 54 | * punctuation.definition.sharp.mxs 55 | * punctuation.section.braces.mxs 56 | * punctuation.section.brackets.mxs 57 | * punctuation.section.parens.mxs 58 | * punctuation.separator.comma.mxs 59 | * punctuation.terminator.linebreak.mxs 60 | * punctuation.terminator.statement.mxs 61 | * storage.type.class 62 | * storage.type.declaration.mxs 63 | 64 | --- 65 | 66 | * storage.type.entity.mxs 67 | * storage.type.event.mxs 68 | * storage.type.function.mxs 69 | * storage.type.struct.mxs 70 | * string.quoted.double.mxs 71 | * string.quoted.double.verbatim.mxs 72 | * string.quoted.other.mxs 73 | * support.function.array.mxs 74 | * support.function.generic.mxs 75 | * support.function.geo.mxs 76 | 77 | --- 78 | 79 | * support.function.math.mxs 80 | * support.function.string.mxs 81 | * support.function.vector.mxs 82 | * support.type.bitwise.mxs 83 | * support.type.primitive.mxs 84 | * support.type.rolloutcontrol.mxs 85 | * support.variable.objectset.mxs 86 | * variable.identifier.name.mxs 87 | * variable.identifier.name.quoted.mxs 88 | * variable.identifier.quoted.mxs 89 | * variable.language.mxs 90 | 91 | --- 92 | 93 | * variable.other.mxs 94 | * variable.other.pathname.mxs 95 | * variable.other 96 | * variable.parameter.mxs 97 | * variable.property.mxs -------------------------------------------------------------------------------- /src/backend/diagnostics/CustomErrorStrategy.ts: -------------------------------------------------------------------------------- 1 | import { DefaultErrorStrategy, Parser, RecognitionException, Token } from 'antlr4ng'; 2 | 3 | /** 4 | * Custom error recovery strategy that extends DefaultErrorStrategy. 5 | * Key improvements: 6 | * 1. Deduplicates error reports at the same position 7 | * 2. Suppresses error reports during sync() operations (cascading errors) 8 | */ 9 | export class CustomErrorStrategy extends DefaultErrorStrategy 10 | { 11 | private isSyncing = false; 12 | private reportedErrors = new Set(); 13 | 14 | /** 15 | * Override to deduplicate error reports. 16 | * The same syntax error can be reported multiple times as the parser 17 | * tries different recovery strategies. 18 | */ 19 | public override reportError(recognizer: Parser, e: RecognitionException): void 20 | { 21 | // Create unique key for this error location 22 | const errorKey = `${e.offendingToken?.line}:${e.offendingToken?.column}:${e.offendingToken?.text}`; 23 | 24 | // Skip duplicates 25 | if (this.reportedErrors.has(errorKey)) { 26 | return; 27 | } 28 | 29 | this.reportedErrors.add(errorKey); 30 | super.reportError(recognizer, e); 31 | } 32 | 33 | /** 34 | * Override sync to track when we're in sync mode. 35 | * During sync, the parser skips tokens to find a recovery point. 36 | * We don't want to report errors for tokens being skipped. 37 | */ 38 | public override sync(recognizer: Parser): void 39 | { 40 | this.isSyncing = true; 41 | try { 42 | super.sync(recognizer); 43 | } finally { 44 | this.isSyncing = false; 45 | } 46 | } 47 | 48 | /** 49 | * Override to suppress error reporting during sync operations. 50 | * This is where "extraneous input" cascading errors come from. 51 | */ 52 | public override reportUnwantedToken(recognizer: Parser): void 53 | { 54 | if (this.isSyncing) { 55 | // Suppress - this is a cascading error during sync 56 | return; 57 | } 58 | super.reportUnwantedToken(recognizer); 59 | } 60 | 61 | /** 62 | * Override to suppress missing token reports during sync. 63 | */ 64 | public override reportMissingToken(recognizer: Parser): void 65 | { 66 | if (this.isSyncing) { 67 | // Suppress - this is a cascading error during sync 68 | return; 69 | } 70 | super.reportMissingToken(recognizer); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/SymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, DocumentSymbol, DocumentSymbolProvider, ProviderResult, 3 | SymbolInformation, TextDocument, 4 | } from 'vscode'; 5 | 6 | import { mxsBackend } from './backend/Backend.js'; 7 | import { symbolDescriptionFromEnum, translateSymbolKind } from './Symbol.js'; 8 | import { ISymbolInfo } from './types.js'; 9 | import { Utilities } from './utils.js'; 10 | 11 | export class mxsSymbolProvider implements DocumentSymbolProvider 12 | { 13 | public constructor(private backend: mxsBackend) { } 14 | 15 | private collectAllChildren(symbol: ISymbolInfo): DocumentSymbol 16 | { 17 | function dfs(currentSymbol: ISymbolInfo): DocumentSymbol 18 | { 19 | // if (!currentSymbol.definition) { return; } 20 | const range = Utilities.lexicalRangeToRange(currentSymbol.definition!.range); 21 | 22 | const info = new DocumentSymbol( 23 | currentSymbol.name, 24 | symbolDescriptionFromEnum(currentSymbol.kind), 25 | translateSymbolKind(currentSymbol.kind), 26 | range, 27 | range // TODO: SelectionRange 28 | ); 29 | 30 | if (currentSymbol.children?.length) { 31 | info.children = currentSymbol.children 32 | .filter(child => 'name' in child && 'definition' in child) 33 | .map(child => dfs(child)) 34 | } 35 | return info; 36 | } 37 | return dfs(symbol); 38 | } 39 | 40 | provideDocumentSymbols(document: TextDocument, _token: CancellationToken): 41 | ProviderResult 42 | { 43 | return new Promise((resolve) => 44 | { 45 | const symbols = 46 | this.backend.getContext(document.uri.toString())?.listTopLevelSymbols(false); 47 | const symbolsList: DocumentSymbol[] = []; 48 | 49 | for (const symbol of symbols) { 50 | if (!symbol.definition || !symbol.name) { 51 | continue; 52 | } 53 | if (symbol.children?.length) { 54 | // childrens 55 | symbolsList.push(this.collectAllChildren(symbol)); 56 | } else { 57 | // symbol does not have children 58 | const range = Utilities.lexicalRangeToRange(symbol.definition.range); 59 | // /* 60 | symbolsList.push(new DocumentSymbol( 61 | symbol.name, 62 | symbolDescriptionFromEnum(symbol.kind), 63 | translateSymbolKind(symbol.kind), 64 | range, 65 | range // TODO: selectionRange 66 | )); 67 | // */ 68 | } 69 | } 70 | resolve(symbolsList); 71 | // resolve([]); 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /src/parser/mxsLexerBase.ts: -------------------------------------------------------------------------------- 1 | import { CharStream, Lexer, Token } from 'antlr4ng'; 2 | 3 | import { mxsLexer } from './mxsLexer.js'; 4 | 5 | export abstract class mxsLexerBase extends Lexer 6 | { 7 | public constructor(input: CharStream) 8 | { 9 | super(input); 10 | } 11 | 12 | /* 13 | private lastToken: Token; 14 | 15 | public override nextToken(): Token 16 | { 17 | // Get the next token. 18 | const next: Token = super.nextToken(); 19 | // console.log(`${next.line} : ${next.channel} ${JSON.stringify(next.text)}`); 20 | if (next.channel == Token.DEFAULT_CHANNEL) { 21 | // Keep track of the last token on the default channel. 22 | this.lastToken = next; 23 | } 24 | return next; 25 | } 26 | */ 27 | 28 | public override emit(): Token 29 | { 30 | // Sanitize NL tokens 31 | if (this.type === mxsLexer.NL) { 32 | // Single pass: check for semicolon once 33 | const txt = this.text; 34 | this.text = txt.indexOf(';') !== -1 ? ';' : '\r\n'; 35 | } 36 | return super.emit(); 37 | } 38 | 39 | protected noWsOrEqualNext(): boolean 40 | { 41 | const la = this.inputStream.LA(1); 42 | // Single comparison instead of two separate ones 43 | return la > 32 && la !== this.text.charCodeAt(0); 44 | } 45 | 46 | protected noWsOrEqualBefore(): boolean 47 | { 48 | const la = this.inputStream.LA(-1); 49 | return la > 32 && la !== this.text.charCodeAt(0); 50 | } 51 | 52 | /* 53 | private static readonly IS_NON_ALPHANUM = (() => { 54 | const arr = new Array(256).fill(false); 55 | // Mark non-alphanumeric positions 56 | for (let i = 0; i <= 33; i++) arr[i] = true; 57 | // ... mark other ranges 58 | return arr; 59 | })(); 60 | 61 | protected noAlphanumBefore(): boolean 62 | { 63 | const la = this.inputStream.LA(-1); 64 | return la >= 0 && la < 256 ? mxsLexerBase.IS_NON_ALPHANUM[la] : false; 65 | } 66 | */ 67 | 68 | private static readonly NON_ALPHANUM_CHARS = new Set([ 69 | // Control & whitespace (0-33) 70 | ...Array.from({ length: 34 }, (_, i) => i), 71 | // Special chars: # $ % & ' ( ) (35-40) 72 | 35, 36, 37, 38, 39, 40, 73 | // Operators: * + , - . / (42-47) 74 | 42, 43, 44, 45, 46, 47, 75 | // Punctuation: : ; < = > ? @ (58-63, 64) 76 | 58, 59, 60, 61, 62, 63, 64, 77 | // Brackets: [ \ ] (91-93) 78 | 91, 92, 93, 79 | // More special: ^ _ ` (94-96) 80 | 94, 95, 96, 81 | // Braces: { | } (123-125) 82 | 123, 124, 125, 83 | // Tilde and DEL (126, 127, 255) 84 | 126, 127, 255 85 | ]); 86 | 87 | protected noAlphanumBefore(): boolean 88 | { 89 | const la = this.inputStream.LA(-1); 90 | // Single Set lookup instead of 8 range comparisons 91 | return mxsLexerBase.NON_ALPHANUM_CHARS.has(la); 92 | } 93 | } -------------------------------------------------------------------------------- /src/CodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, CodeLens, CodeLensProvider, Event, 3 | ProviderResult, TextDocument, 4 | } from 'vscode'; 5 | 6 | import { mxsBackend } from './backend/Backend.js'; 7 | 8 | //TODO: add codeLens for references in structs, functions, declarations, rollouts, etc 9 | 10 | export class mxsCodeLensProvider implements CodeLensProvider 11 | { 12 | public constructor(private backend: mxsBackend) { } 13 | 14 | // private changeEvent = new EventEmitter(); 15 | // private documentName: string; 16 | 17 | onDidChangeCodeLenses?: Event | undefined; 18 | 19 | /* 20 | public get onDidChangeCodeLenses(): Event { 21 | return this.changeEvent.event; 22 | } 23 | 24 | public refresh(): void { 25 | this.changeEvent.fire(); 26 | } 27 | */ 28 | 29 | provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult 30 | { 31 | throw new Error("Method not implemented."); 32 | /* 33 | return new Promise((resolve) => { 34 | if (workspace.getConfiguration("antlr4.referencesCodeLens").enabled !== true) { 35 | resolve(null); 36 | } else { 37 | this.documentName = document.fileName; 38 | const symbols = this.backend.listTopLevelSymbols(document.fileName, false); 39 | const lenses = []; 40 | for (const symbol of symbols) { 41 | if (!symbol.definition) { 42 | continue; 43 | } 44 | 45 | switch (symbol.kind) { 46 | case SymbolKind.FragmentLexerToken: 47 | case SymbolKind.LexerRule: 48 | case SymbolKind.LexerMode: 49 | case SymbolKind.ParserRule: { 50 | const range = new Range( 51 | symbol.definition.range.start.row - 1, 52 | symbol.definition.range.start.column, 53 | symbol.definition.range.end.row - 1, 54 | symbol.definition.range.end.column, 55 | ); 56 | const lens = new SymbolCodeLens(symbol, range); 57 | lenses.push(lens); 58 | 59 | break; 60 | } 61 | 62 | default: 63 | } 64 | } 65 | 66 | resolve(lenses); 67 | } 68 | }); 69 | */ 70 | } 71 | 72 | resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult 73 | { 74 | throw new Error("Method not implemented."); 75 | /* 76 | const refs = this.backend.countReferences(this.documentName, (codeLens as SymbolCodeLens).symbol.name); 77 | codeLens.command = { 78 | title: (refs === 1) ? "1 reference" : `${refs} references`, 79 | command: "", 80 | arguments: undefined, 81 | }; 82 | 83 | return codeLens; 84 | */ 85 | } 86 | } -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ------------------------------------------------------------------------------------------ 3 | * The global settings, used when the `workspace/configuration` request is not supported by the client. 4 | * Please note that this is not the case when using this server with the client provided in this example 5 | * but could happen with other clients. 6 | *------------------------------------------------------------------------------------------ 7 | */ 8 | 9 | import { 10 | ICodeFormatSettings, IMaxScriptSettings, IMinifySettings, IPrettifySettings, 11 | } from './types.js'; 12 | 13 | export const minifySettings: ICodeFormatSettings & IMinifySettings & IPrettifySettings = { 14 | whitespaceChar: ' ', 15 | newLineChar: ';', 16 | indentChar: '', 17 | exprEndChar: ';', 18 | lineContinuationChar: '', 19 | statements: { 20 | useLineBreaks: true, 21 | optionalWhitespace: false 22 | }, 23 | codeblock: { 24 | parensInNewLine: false, 25 | newlineAllways: false, 26 | spaced: false, 27 | }, 28 | list: { 29 | useLineBreaks: false 30 | }, 31 | condenseWhitespace: true, 32 | removeUnnecessaryScopes: true, //TODO: 33 | expressionsToBlock: false, 34 | } 35 | 36 | export const prettifySettings: ICodeFormatSettings & IMinifySettings & IPrettifySettings = { 37 | whitespaceChar: ' ', 38 | newLineChar: '\r\n', 39 | indentChar: '\t', 40 | exprEndChar: '\r\n', 41 | lineContinuationChar: '\\', 42 | codeblock: { 43 | newlineAllways: true, //ok 44 | parensInNewLine: true, //ok 45 | spaced: true, //ok 46 | }, 47 | list: { 48 | useLineBreaks: false //ok 49 | }, 50 | statements: { 51 | useLineBreaks: false, //TODO: 52 | optionalWhitespace: false //TODO: 53 | }, 54 | removeUnnecessaryScopes: false, //TODO: 55 | condenseWhitespace: false, //ok 56 | expressionsToBlock: true, //TODO: 57 | } 58 | 59 | // default settings 60 | export const defaultSettings: IMaxScriptSettings = { 61 | // language: { 62 | // SemanticTokens: true, 63 | // GoToSymbol: true, 64 | // GoToDefinition: true, 65 | // Diagnostics: true, 66 | //}, 67 | // parser: { 68 | // multiThreading: true, 69 | // }, 70 | completions: { 71 | dataBaseCompletion: true, 72 | codeCompletion: true 73 | }, 74 | formatter: { 75 | indentChar: '\t', 76 | newLineChar: '\r\n', 77 | exprEndChar: ';', 78 | lineContinuationChar: '\\', 79 | whitespaceChar: ' ', 80 | codeblock: { 81 | parensInNewLine: true, 82 | newlineAllways: false, 83 | spaced: true, 84 | }, 85 | statements: { 86 | useLineBreaks: true, 87 | optionalWhitespace: false 88 | }, 89 | list: { 90 | useLineBreaks: false 91 | } 92 | }, 93 | prettifier: { 94 | filePrefix: 'pretty_', 95 | expressionsToBlock: true, 96 | }, 97 | minifier: { 98 | filePrefix: 'min_', 99 | removeUnnecessaryScopes: false, 100 | condenseWhitespace: true, 101 | } 102 | }; -------------------------------------------------------------------------------- /src/backend/schemas/mxsTokenDefs.ts: -------------------------------------------------------------------------------- 1 | /**List of token type defintions, used in Diagnostics suggestions */ 2 | export const tokenDefinitions: Record = { 3 | 'arraydef': 'Array definition', 4 | 'assign': 'Assign operator <=|+=|-=|*=|/=>,', 5 | 'bitarraydef': 'BitArray definition', 6 | 'bitrange': 'BitArray bitrange value', 7 | 'comment_BLK': 'Block comment', 8 | 'comment_SL': 'Single line comment', 9 | 'comparison': 'Comparision operator < == | != | > | < | >= | <= >', 10 | 'delimiter': 'Delimiter <.>', 11 | 'global_typed': 'Typed ::Global ', 12 | 'hex': 'Hex literal', 13 | 'identity': 'Identifier or definition', 14 | 'kw_about': ' keyword', 15 | 'kw_at': ' keyword', 16 | 'kw_attributes': 'Custom attributes definition', 17 | 'kw_bool': 'Boolean value ', 18 | 'kw_case': ' keyword', 19 | 'kw_context': 'Context statement', 20 | 'kw_animate': 'Context statement', 21 | 'kw_coordsys': ' keyword', 22 | 'kw_do': ' keyword', 23 | 'kw_exit': ' keyword', 24 | 'kw_for': ' keyword', 25 | 'kw_function': 'Function declaration', 26 | 'kw_global': ' keyword', 27 | 'kw_group': ' keyword', 28 | 'kw_if': ' keyword', 29 | 'kw_in': ' keyword', 30 | 'kw_local': ' keyword', 31 | 'kw_macroscript': ' declaration', 32 | 'kw_mapped': ' keyword', 33 | 'kw_not': ' keyword', 34 | 'kw_null': 'Undefined value', 35 | 'kw_objectset': ' keyword', 36 | 'kw_on': 'Boolean value ', 37 | 'kw_persistent': ' keyword', 38 | 'kw_plugin': ' declaration', 39 | 'kw_rcmenu': ' declaration', 40 | 'kw_return': ' keyword', 41 | 'kw_rollout': ' declaration', 42 | 'kw_set': ' keyword', 43 | 'kw_struct': ' keyword', 44 | 'kw_time': '