diff --git a/.gitignore b/.gitignore index 49fa97c..16cffdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.vsix *.DS_store +node_modules/ +out/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4d522d8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Runescript Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: watch", + "sourceMaps": true + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..801d887 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/client/cache/activeCursorCache.js b/client/cache/activeCursorCache.js deleted file mode 100644 index aa0b697..0000000 --- a/client/cache/activeCursorCache.js +++ /dev/null @@ -1,24 +0,0 @@ -let activeCursorMatchTypeId; -let line; -let index; -let path; - -function get(document, position) { - if (document.uri.fsPath === path && position.line === line && getIndex(document, position) === index) { - return activeCursorMatchTypeId; - } - return null; -} - -function set(value, document, position) { - path = document.uri.fsPath; - index = getIndex(document, position); - line = position.line; - activeCursorMatchTypeId = value; -} - -function getIndex(document, position) { - return document.lineAt(position.line).text.substring(0, position.character).split(',').length; -} - -module.exports = { get, set }; diff --git a/client/cache/activeFileCache.js b/client/cache/activeFileCache.js deleted file mode 100644 index 7610735..0000000 --- a/client/cache/activeFileCache.js +++ /dev/null @@ -1,120 +0,0 @@ -const vscode = require('vscode'); -const { TRIGGER_LINE, TRIGGER_DEFINITION, LOCAL_VAR_WORD_PATTERN } = require('../enum/regex'); -const { getWords } = require('../utils/matchUtils'); -const dataTypeToMatchId = require('../resource/dataTypeToMatchId'); -const { getLines } = require('../utils/stringUtils'); - -/** - * A cache which keeps track of script blocks in the active / viewing file - * Only applies to rs2 files - * Allows a quick look up of script data by passing in a line number - * Script data object: -{ - name: string - start: number (line number that the script starts on) - trigger: string - returns: string[] (matchTypeId) - variables: { $varName1: {type: string, matchTypeId: string, parameter: boolean, declaration: range, references: range[]}, ... } -} - */ -var scriptData; -var lineNumToScript; -var curData; - -function getScriptData(lineNum) { - let data; - for (const script of scriptData) { - if (lineNum >= script.start) data = script; - } - return data; -} - -function rebuild() { - scriptData = []; - lineNumToScript = {}; - curData = null; - const activeEditor = vscode.window.activeTextEditor; - if (activeEditor && activeEditor.document.uri.path.endsWith('.rs2')) { - parseFile(getLines(activeEditor.document.getText()), activeEditor.document.uri); - } -} - -function parseFile(lines, uri) { - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - let indexOffset = 0; - if (TRIGGER_LINE.test(line)) { - const definitionLength = TRIGGER_DEFINITION.exec(line); - if (definitionLength) { - // Split the line into definition part and code part, for scripts with same line code - indexOffset = definitionLength[0].length; - parseTriggerLine(line.substring(0, indexOffset), i, uri); - line = line.substring(indexOffset); // update line to only the code portion of the line (if any) - } - } - parseLine(line, i, uri, indexOffset); - } - if (curData) scriptData.push(curData); -} - -function parseTriggerLine(line, lineNum, uri) { - // Save previously parsed script data and init a new one for this block - if (curData) scriptData.push(curData); - curData = {start: lineNum, variables: {}, returns: []}; - - // Parse for script name and trigger - const nameAndTrigger = line.substring(1, line.indexOf(']')).split(','); - curData.trigger = nameAndTrigger[0]; - curData.name = nameAndTrigger[1]; - - // Parse script params and save as variables - let openingIndex = line.indexOf('('); - let closingIndex = line.indexOf(')'); - if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { - line.substring(openingIndex, closingIndex).split(',').forEach(param => { - const split = param.trim().split(' '); - const position = new vscode.Position(lineNum, line.indexOf(split[1])); - const location = new vscode.Location(uri, new vscode.Range(position, position.translate(0, split[1].length))); - addVariable(split[0], split[1], location, true); - }); - } - - // Parse return type into an array of matchTypeId (string) - line = line.substring(closingIndex + 1); - openingIndex = line.indexOf('('); - closingIndex = line.indexOf(')'); - if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { - curData.returns = line.substring(openingIndex, closingIndex).split(',').map(item => dataTypeToMatchId(item.trim())); - } -} - -function parseLine(line, lineNum, uri, indexOffset=0) { - const words = getWords(line.split('//')[0], LOCAL_VAR_WORD_PATTERN); - for (let i = 0; i < words.length; i++) { - if (words[i].value.charAt(0) === '$') { - const name = words[i].value; - const position = new vscode.Position(lineNum, words[i].start + indexOffset); - const location = new vscode.Location(uri, new vscode.Range(position, position.translate(0, name.length))); - (i > 0 && words[i-1].value.startsWith('def_')) ? addVariable(words[i-1].value.substring(4), name, location) : addVariableReference(name, location); - } - } -} - -function addVariable(type, name, location, isParam=false) { - curData.variables[name] = { - type: type, - matchTypeId: dataTypeToMatchId(type), - parameter: isParam, - declaration: location, - references: [] - }; - addVariableReference(name, location); -} - -function addVariableReference(name, location) { - if (curData.variables[name]) { - curData.variables[name].references.push(location); - } -} - -module.exports = { rebuild, getScriptData }; diff --git a/client/cache/cacheManager.js b/client/cache/cacheManager.js deleted file mode 100644 index f1647e7..0000000 --- a/client/cache/cacheManager.js +++ /dev/null @@ -1,197 +0,0 @@ -const fs = require('fs').promises; -const vscode = require('vscode'); -const matchType = require("../matching/matchType"); -const identifierCache = require('./identifierCache'); -const activeFileCache = require('./activeFileCache'); -const stringUtils = require('../utils/stringUtils'); -const { matchWords } = require('../matching/matchWord'); -const identifierFactory = require('../resource/identifierFactory'); -const { INFO_MATCHER, TRIGGER_LINE } = require('../enum/regex'); -const cacheUtils = require('../utils/cacheUtils'); -const returnBlockLinesCache = require('./returnBlockLinesCache'); -const switchStmtLinesCache = require('./switchStmtLinesCache'); -const dataTypeToMatchId = require('../resource/dataTypeToMatchId'); - -/** - * Builds the set of monitored file types, any file events with other file types will be ignored - * Monitored file types are determined by checking all file types defined in the matchType object - */ -const monitoredFileTypes = new Set(); -function determineFileTypes() { - monitoredFileTypes.add('pack'); - Object.keys(matchType).filter(mt => !mt.referenceOnly).forEach(matchTypeId => { - const fileTypes = matchType[matchTypeId].fileTypes || []; - for (const fileType of fileTypes) { - monitoredFileTypes.add(fileType); - } - }); -} - -/** - * Rebuilds the entire identifier cache for all relevant workspace files - * Need to do 2 passes on the files to for ensuring things like engine command - * parameters get matched correctly. On the first pass, the commands don't yet exist in the cache - * so the matching service cannot accurately build everything until 2 passes are made - */ -async function rebuildAll() { - if (monitoredFileTypes.size === 0) determineFileTypes(); - clearAll(); - const fileUris = await getFiles(); - await Promise.all(fileUris.map(uri => parseFileAndCacheIdentifiers(uri))); - await Promise.all(fileUris.map(uri => parseFileAndCacheIdentifiers(uri))); - return rebuildActiveFile(); -} - -/** - * Rebuilds the activeFileCache, parses the active text editor file and stores relevant script data - * such as script variables, script return types, switch statement types, etc... - */ -var debounceTimer; -function rebuildActiveFile() { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => { - activeFileCache.rebuild(); - }, 400); -} - -/** - * Rebuilds the identifier cache for identifiers in the provided file uri - */ -async function rebuildFile(uri) { - if (isValidFile(uri)) { - clearFile(uri); - parseFileAndCacheIdentifiers(uri); - rebuildActiveFile(); - } -} - -/** - * Clears the identifier cache for identifiers in the provided list of file uris - */ -async function clearFiles(uris) { - for (const uri of uris) { - if (isValidFile(uri)) { - clearFile(uri); - } - } -} - -/** - * Clears the identifier cache for identifiers in the provided list of old file uris - * and then recaches the files using the new file names - */ -async function renameFiles(uriPairs) { - for (const uriPair of uriPairs) { - if (isValidFile(uriPair.oldUri) && isValidFile(uriPair.newUri)) { - clearFile(uriPair.oldUri); - parseFileAndCacheIdentifiers(uriPair.newUri); - } - } -} - -/** - * Adds to cache for new files - */ -async function createFiles(uris) { - for (const uri of uris) { - if (isValidFile(uri)) { - parseFileAndCacheIdentifiers(uri); - } - } -} - -/** - * Get a list of all relevant files in the workspace which might contain identifiers - */ -async function getFiles() { - const fileTypesToScan = []; - monitoredFileTypes.forEach(fileType => fileTypesToScan.push(`**/*.${fileType}`)); - return vscode.workspace.findFiles(`{${[...fileTypesToScan].join(',')}}`); -} - -/** - * Parses the input file for identifiers, and caches them when found - */ -async function parseFileAndCacheIdentifiers(uri) { - const isRs2 = uri.fsPath.endsWith('.rs2'); - const fileText = await fs.readFile(uri.fsPath, "utf8"); - const lines = stringUtils.getLines(fileText); - for (let line = 0; line < lines.length; line++) { - cacheSwitchStatementBlock(line, uri); - const matches = (matchWords(lines[line], line, uri) || []).filter(match => match && match.match.cache); - if (matches.length > 0) { - const text = {lines: null, start: 0}; - matches.forEach(match => { - if (match.match.declaration) { - text.lines = (text.lines) ? text.lines : lines.slice(line); - const location = new vscode.Location(uri, new vscode.Position(line, match.context.word.start)); - const identifier = identifierFactory.build(match.word, match.match, location, getInfo(lines, line), text); - identifierCache.put(match.word, match.match, identifier); - identifierCache.putReference(match.word, match.match, uri, line, match.context.word.start); - cacheReturnBlock(identifier, line, match); - } else { - const id = match.context.cert ? undefined : match.context.packId; - let index = match.context.word.start; - if (!match.context.modifiedWord && match.word.indexOf(':') > 0) { - index += match.word.indexOf(':') + 1; - } - identifierCache.putReference(match.word, match.match, uri, line, index, id); - } - }); - } - } - - function cacheReturnBlock(identifier, line, match) { - if (isRs2 && identifier.signature.returns.length > 0 && TRIGGER_LINE.test(lines[line])) { - returnBlockLinesCache.put(line + 1, cacheUtils.resolveKey(match.word, match.match), uri); - } - } - - function cacheSwitchStatementBlock(line, uri) { - if (isRs2) { - const switchSplit = lines[line].split("switch_"); - if (switchSplit.length > 1) { - const switchMatchType = dataTypeToMatchId(switchSplit[1].split(/[ (]/)[0]); - if (switchMatchType !== matchType.UNKNOWN.id) { - switchStmtLinesCache.put(line + 1, switchMatchType, uri); - } - } - } - } -} - -/** - * Checks the previous line before an identifier for an "info" tag, if so it is added to the identifier - */ -function getInfo(lines, line) { - if (line < 1) return null; - const infoMatch = INFO_MATCHER.exec(lines[line - 1]); - return (infoMatch && infoMatch[2]) ? infoMatch[2].trim() : null; -} - -/** - * Checks if the file extension of the uri is in the list of monitored file types - */ -function isValidFile(uri) { - return monitoredFileTypes.has(uri.fsPath.split(/[#?]/)[0].split('.').pop().trim()); -} - -/** - * Empty the caches entirely - */ -function clearAll() { - identifierCache.clear(); - returnBlockLinesCache.clear(); - switchStmtLinesCache.clear(); -} - -/** - * Empty the caches for a single file - */ -function clearFile(uri) { - identifierCache.clearFile(uri); - returnBlockLinesCache.clearFile(uri); - switchStmtLinesCache.clearFile(uri); -} - -module.exports = { rebuildAll, rebuildFile, rebuildActiveFile, clearFiles, renameFiles, createFiles, clearAll } diff --git a/client/cache/class/LineReferenceCache.js b/client/cache/class/LineReferenceCache.js deleted file mode 100644 index e4c35bd..0000000 --- a/client/cache/class/LineReferenceCache.js +++ /dev/null @@ -1,57 +0,0 @@ -const { resolveFileKey } = require("../../utils/cacheUtils"); - -function encodeLineValue(startLine, identifierKey) { - return `${startLine}|${identifierKey}`; -} - -function decodeLineValue(encodedValue) { - const split = encodedValue.split('|'); - return (split.length !== 2) ? null : { line: Number(split[0]), value: split[1] }; -} - -class LineReferenceCache { - constructor() { - this.cache = {}; - } - - put(startLine, value, uri) { - const fileKey = resolveFileKey(uri); - if (value && fileKey) { - const fileLineReferences = this.cache[fileKey] || new Set(); - fileLineReferences.add(encodeLineValue(startLine, value)); - this.cache[fileKey] = fileLineReferences; - } - } - - get(lineNum, uri) { - const fileKey = resolveFileKey(uri); - const fileLineReferences = this.cache[fileKey] || new Set(); - let curKey; - let curLine = 0; - fileLineReferences.forEach(ref => { - const { line, value } = decodeLineValue(ref); - if (lineNum >= line && curLine < line) { - curKey = value; - curLine = line; - } - }); - return curKey; - } - - getAll() { - return this.cache; - } - - clearFile(uri) { - const fileKey = resolveFileKey(uri); - if (fileKey) { - delete this.cache[fileKey]; - } - } - - clear() { - this.cache = {}; - } -} - -module.exports = LineReferenceCache; diff --git a/client/cache/class/Trie.js b/client/cache/class/Trie.js deleted file mode 100644 index 8466921..0000000 --- a/client/cache/class/Trie.js +++ /dev/null @@ -1,89 +0,0 @@ -class Trie { - constructor() { - this.root = new TrieNode(); - } - - insert(word) { - if (!word) return false; - let currNode = this.root; - for (const letter of word) { - if (!currNode.children.has(letter)) { - currNode.children.set(letter, new TrieNode(letter)); - } - currNode = currNode.children.get(letter); - } - currNode.endOfWord = true; - return currNode; - } - - getLastNode(letters, start = this.root) { - let currNode = start; - for (const letter of letters) { - if (!currNode.children.has(letter)) return false; - currNode = currNode.children.get(letter); - } - return currNode; - } - - hasWord(word, start = this.root) { - let node = this.getLastNode(word, start); - return node && node !== this.root ? node.endOfWord : false; - } - - findAllWithPrefix(prefix, start = this.root) { - let words = []; - let currNode = this.getLastNode(prefix, start); - if (currNode) { - if (currNode.endOfWord) words.push(prefix); - currNode.children.forEach((child) => - this.getWordsFrom(child, prefix, words) - ); - } - return words; - } - - getWordsFrom(node = this.root, string = '', array = []) { - if (!node) return; - string += node.value; - if (node.endOfWord) array.push(string); - node.children.forEach((child) => { - this.getWordsFrom(child, string, array); - }); - return array; - } - - removeWord(word) { - if (!word) return false; - let currNode = this.root; - let stack = []; - for (const letter of word) { - if (!currNode.children.has(letter)) return false; - currNode = currNode.children.get(letter); - if (word[word.length - 1] !== currNode.value) stack.push(currNode); - } - currNode.endOfWord = false; - while (stack.length > 0 && !currNode.endOfWord) { - let prevNode = currNode; - currNode = stack.pop(); - if (prevNode.children.size > 0) { - break; - } - currNode.children.delete(prevNode.value); - } - return true; - } - - clear() { - this.root.children.clear(); - } -} - -class TrieNode { - constructor(value = "") { - this.children = new Map(); - this.value = value; - this.endOfWord = false; - } -} - -module.exports = Trie; diff --git a/client/cache/completionCache.js b/client/cache/completionCache.js deleted file mode 100644 index b9a96cc..0000000 --- a/client/cache/completionCache.js +++ /dev/null @@ -1,55 +0,0 @@ -const Trie = require('./class/Trie'); - -/** - * One trie per matchType, stores the names of all identifiers of a matchtype in a trie datastructure - * This is used for quicker code completion lookups - */ -var completionCache = {}; - -function put(name, matchTypeId) { - if (!completionCache[matchTypeId]) { - completionCache[matchTypeId] = new Trie(); - } - completionCache[matchTypeId].insert(name); - const colonIndex = name.indexOf(':'); - if (colonIndex >= 0) { - completionCache[matchTypeId].insert(name.substring(colonIndex + 1)); - } -} - -function getAllWithPrefix(prefix, matchTypeId) { - const matchTrie = completionCache[matchTypeId]; - if (matchTrie) { - return matchTrie.findAllWithPrefix(prefix); - } - return null; -} - -function contains(name, matchTypeId) { - const matchTrie = completionCache[matchTypeId]; - if (matchTrie) { - return matchTrie.hasWord(name); - } - return false; -} - -function remove(name, matchTypeId) { - const matchTrie = completionCache[matchTypeId]; - if (matchTrie) { - matchTrie.removeWord(name); - } -} - -function clear(matchTypeId) { - if (matchTypeId) { - delete completionCache[matchTypeId]; - } else { - completionCache = {}; - } -} - -function getTypes() { - return Object.keys(completionCache); -} - -module.exports = { put, getAllWithPrefix, getTypes, contains, remove, clear }; \ No newline at end of file diff --git a/client/cache/identifierCache.js b/client/cache/identifierCache.js deleted file mode 100644 index 2f9fdb6..0000000 --- a/client/cache/identifierCache.js +++ /dev/null @@ -1,136 +0,0 @@ -const { buildRef } = require('../resource/identifierFactory'); -const cacheUtils = require('../utils/cacheUtils'); -const completionCache = require('./completionCache'); - -/** - * The identifierCache stores all matched identifiers in the workspace - * identifierCache = {key [name+matchTypeId]: identifier} - * See identifierFactory.js for the object structure - */ -var identifierCache = {}; - -/** - * The fileToIdentiferMap keeps track of all identifiers and references in a file - * This is used for updating the cache as necessary when a file is modified - * fileToIdentiferMap = {filePath: {declarations: Set(): identifierKey, references: Set(): identifierKey}} - */ -var fileToIdentifierMap = {}; - -function contains(name, match) { - return identifierCache[cacheUtils.resolveKey(name, match)] !== undefined; -} - -function get(name, match) { - return identifierCache[cacheUtils.resolveKey(name, match)]; -} - -function getByKey(key) { - return identifierCache[key]; -} - -/** - * Given a file URI, a line number, this will return the closest declaration identifier - * to the given line number which is above the line number provided. - */ -function getParentDeclaration(uri, lineNum, requiredMatchTypeId=undefined) { - const fileIdentifiers = fileToIdentifierMap[cacheUtils.resolveFileKey(uri)]; - if (!fileIdentifiers) { - return null; - } - let lineRef = -1; - let declaration; - fileIdentifiers.declarations.forEach(dec => { - const iden = identifierCache[dec]; - if (iden.declaration && iden.declaration.range.start.line < lineNum && iden.declaration.range.start.line > lineRef) { - if (!requiredMatchTypeId || requiredMatchTypeId === iden.matchId) { - lineRef = iden.declaration.range.start.line; - declaration = iden; - } - } - }); - return declaration; -} - -function put(name, match, identifier) { - const key = cacheUtils.resolveKey(name, match); - const fileKey = cacheUtils.resolveFileKey(identifier.declaration.uri); - if (!key || !fileKey) { - return null; - } - let curIdentifier = identifierCache[key]; - if (curIdentifier && curIdentifier.declaration) { - return null; // declaration already exists, don't overwrite, if it needs to be updated it should be deleted first - } - if (curIdentifier) { - if (curIdentifier.id) identifier.id = curIdentifier.id; - if (!curIdentifier.declaration) identifier.references = curIdentifier.references; - } - addToFileMap(fileKey, key); - identifierCache[key] = identifier; - completionCache.put(name, match.id); -} - -function putReference(name, match, uri, lineNum, index, packId) { - const key = cacheUtils.resolveKey(name, match) - const fileKey = cacheUtils.resolveFileKey(uri); - if (!key || !fileKey) { - return null; - } - if (!identifierCache[key]) { - identifierCache[key] = buildRef(name, match); - } - const fileReferences = identifierCache[key].references[fileKey] || new Set(); - fileReferences.add(cacheUtils.encodeReference(lineNum, index)); - addToFileMap(fileKey, key, false); - identifierCache[key].references[fileKey] = fileReferences; - if (packId) identifierCache[key].id = packId; - if (match.referenceOnly) completionCache.put(name, match.id); -} - -function clear() { - identifierCache = {}; - fileToIdentifierMap = {}; - completionCache.clear(); -} - -function clearFile(uri) { - const fileKey = cacheUtils.resolveFileKey(uri); - const identifiersInFile = fileToIdentifierMap[fileKey] || { declarations: new Set(), references: new Set() }; - identifiersInFile.references.forEach(key => { - if (identifierCache[key]) { - // Delete references to the cleared file from every identifier which referenced the file - if (identifierCache[key].references[fileKey]) { - delete identifierCache[key].references[fileKey]; - } - // Cleanup/Delete identifiers without a declaration who no longer have any references - if (Object.keys(identifierCache[key].references).length === 0 && !identifierCache[key].declaration) { - const iden = identifierCache[key]; - completionCache.remove(iden.name, iden.matchId); - delete identifierCache[key]; - } - } - }) - identifiersInFile.declarations.forEach(key => { - if (identifierCache[key]) { - // If the identifier has orphaned references, then we only delete the declaration and keep the identifier w/references - // Otherwise, we delete the entire identifier (no declaration and no references => no longer exists in any capacity) - const iden = identifierCache[key]; - completionCache.remove(iden.name, iden.matchId); - const hasOrphanedRefs = Object.keys(identifierCache[key].references).length > 0; - if (hasOrphanedRefs) { - delete identifierCache[key].declaration; - } else { - delete identifierCache[key]; - } - } - }); - delete fileToIdentifierMap[fileKey]; -} - -function addToFileMap(fileKey, identifierKey, declaration=true) { - const identifiersInFile = fileToIdentifierMap[fileKey] || { declarations: new Set(), references: new Set() }; - (declaration) ? identifiersInFile.declarations.add(identifierKey) : identifiersInFile.references.add(identifierKey); - fileToIdentifierMap[fileKey] = identifiersInFile; -} - -module.exports = { contains, get, getParentDeclaration, getByKey, put, putReference, clear, clearFile }; diff --git a/client/cache/returnBlockLinesCache.js b/client/cache/returnBlockLinesCache.js deleted file mode 100644 index 345b9c6..0000000 --- a/client/cache/returnBlockLinesCache.js +++ /dev/null @@ -1,11 +0,0 @@ -const LineReferenceCache = require("./class/LineReferenceCache"); - -/** - * A cache which enables a quick lookup of the identifier for the block the line is in - * Given a line number, it will return the name of the block that line number is a part of (if any) - * A block referring to the code block of a proc, label, queue, etc... - * This cache is used to quickly determine the return type for a given line - */ -const returnBlockLinesCache = new LineReferenceCache(); - -module.exports = returnBlockLinesCache; diff --git a/client/cache/switchStmtLinesCache.js b/client/cache/switchStmtLinesCache.js deleted file mode 100644 index b2ffbc4..0000000 --- a/client/cache/switchStmtLinesCache.js +++ /dev/null @@ -1,10 +0,0 @@ -const LineReferenceCache = require("./class/LineReferenceCache"); - -/** - * A cache which enables a quick lookup of the matchType of a switch statement - * Given a line number, this cache will return the type (if any) for the switch statement - * that line number is a part of - */ -const switchStmtLinesCache = new LineReferenceCache(); - -module.exports = switchStmtLinesCache; diff --git a/client/enum/hoverConfigOptions.js b/client/enum/hoverConfigOptions.js deleted file mode 100644 index 2ffe6e7..0000000 --- a/client/enum/hoverConfigOptions.js +++ /dev/null @@ -1,9 +0,0 @@ -const option = { - DECLARATION_HOVER_ITEMS: 'DECLARATION_HOVER_ITEMS', // display items that show on hover for identifier declarations - REFERENCE_HOVER_ITEMS: 'REFERENCE_HOVER_ITEMS', // display items that show on hover for identifier references - LANGUAGE: 'LANGUAGE', // the code language that this matchType should use in hover codeblock text - BLOCK_SKIP_LINES: 'BLOCK_SKIP_LINES', // the number of lines to skip in code block displays (default value is 1 -> skip first line for most blocks which is the '[identifierName]' line) - CONFIG_INCLUSIONS: 'CONFIG_INCLUSIONS' // the config tags you want to be shown (ex: obj displays name, desc, and category only), if null (default) then all fields are displayed -} - -module.exports = option; diff --git a/client/enum/hoverDisplayItems.js b/client/enum/hoverDisplayItems.js deleted file mode 100644 index ae4be8d..0000000 --- a/client/enum/hoverDisplayItems.js +++ /dev/null @@ -1,14 +0,0 @@ -// In order for a display item to be shown in hover texts, the matchType to which the identifier belongs to -// must define a declaration or reference config which includes the desired hoverDisplay item in its displayItems array -// Note: in order to get identifier.value to display you must define a custom postProcessor for the matchType which -// populates identifier.value, there is no default value parsing like there is with the others - -const hoverDisplay = { - TITLE: 'title', // hover text title display : fileType.png matchType.id identifier.name - INFO: 'info', // hover text info display : identifier.info (in italics) - VALUE: 'value', // hover text value display : identifier.value (plain text) - SIGNATURE: 'signature', // signature display : identifier.params
identifier.returns (in code syntax) - CODEBLOCK: 'codeblock' // block display : identifier.block (in code syntax) -}; - -module.exports = hoverDisplay; diff --git a/client/enum/regex.js b/client/enum/regex.js deleted file mode 100644 index 707ca58..0000000 --- a/client/enum/regex.js +++ /dev/null @@ -1,22 +0,0 @@ -const regex = { - COORD: /(\d+_){4}\d+/, - COLOR: /\d{6}/, - RECOLOR: /(recol[1-6][sd])=(\d+)/g, - NUMBER: /^\d+.?\d+$/, - END_OF_BLOCK: /(\r\n|\r|\n)(\[.+|val=.+|\^.+|\d+=.+)(?:$|(\r\n|\r|\n))/, - END_OF_BLOCK_LINE: /^(\[|\^|\d+=)/, - START_OF_LINE: /(?<=[\n])(?!.*[\n]).*/, - END_OF_LINE: /\r\n|\r|\n/, - WORD_PATTERN: /(\.\w+)|(\w+:\w+)|([^\`\~\!\@\#\%\^\&\*\(\)\-\$\=\+\[\{\]\}\\\|\;\:\'\\"\,\.\<\>\/\?\s]+)/g, - LOCAL_VAR_WORD_PATTERN: /(\$\w+)|(\.\w+)|(\w+:\w+)|([^\`\~\!\@\#\%\^\&\*\(\)\-\$\=\+\[\{\]\}\\\|\;\:\'\\"\,\.\<\>\/\?\s]+)/g, - CONFIG_LINE: /^\w+=.+$/, - CONFIG_DECLARATION: /\[\w+\]/, - TRIGGER_LINE: /\[\w+,(\.)?\w+(:\w+)?\]/, - TRIGGER_DEFINITION: /\[.+,.+\](\([\w, :\.$]*\))?(\([\w, :\.$]*\))?/, - INFO_MATCHER: /\/\/[ ]{0,1}(desc|info):(.+)/, - SWITCH_CASE: /\s*case.+/, - COLOR24: /(colour|mapcolour|activecolour|overcolour|activeovercolour)=(\w+)/g, - LOC_MODEL: /^(?!model_[a-z0-9]$)\w+_[a-z0-9]\b/ -} - -module.exports = regex; diff --git a/client/info/triggerInfo.js b/client/info/triggerInfo.js deleted file mode 100644 index 3215321..0000000 --- a/client/info/triggerInfo.js +++ /dev/null @@ -1,18 +0,0 @@ -const { expandCsvKeyObject } = require("../utils/matchUtils"); - -/** - * Defines trigger information which will be displayed on hover if a user hovers over a trigger keyword - * Tip: You can use CSV keys such as 'oploc1, oploc2, oploc3' to apply the same info message for all of those triggers - * Tip: The string 'NAME' will be replaced with the actual triggers defined name [trigger,triggerName] - */ -const triggerInfo = expandCsvKeyObject({ - logout: 'The script that executes when the user logs out', - debugproc: 'Proc that only runs for users with cheats enabled, run with ::NAME' -}); - - -function matchTriggerInfo(key, triggerName) { - return (triggerInfo[key] || '').replace('NAME', triggerName); -} - -module.exports = matchTriggerInfo; diff --git a/client/matching/matchType.js b/client/matching/matchType.js deleted file mode 100644 index 4ec30f2..0000000 --- a/client/matching/matchType.js +++ /dev/null @@ -1,196 +0,0 @@ -const { dataTypePostProcessor, enumPostProcessor, columnPostProcessor, rowPostProcessor, componentPostProcessor, - fileNamePostProcessor, coordPostProcessor, configKeyPostProcessor, triggerPostProcessor, categoryPostProcessor } = require('../resource/postProcessors'); -const { VALUE, SIGNATURE, CODEBLOCK, TITLE, INFO } = require("../enum/hoverDisplayItems"); -const { DECLARATION_HOVER_ITEMS, REFERENCE_HOVER_ITEMS, LANGUAGE, BLOCK_SKIP_LINES, CONFIG_INCLUSIONS } = require('../enum/hoverConfigOptions'); - -/* -Match types define the possible types of identifiers that can be found. The config for a match type tells the extension -all the necessary data it needs for finding declarations, building hover texts, and finding references. -{ - id: String - the unique id for the matchType, - types: String[] - the type keywords which map to this matchType, for example: [namedobj, obj] for OBJ - fileTypes: String[] - the possible file types this matchType can be declared in - cache: boolean - whether or not identifiers with this matchType should be cached - hoverConfig: Object - Config options to modify the hover display for this matchType, options in hoverConfig.js - postProcessor: Function(identifier) - An optional post processing function to apply for this matchType, see postjs - allowRename: Whether or not to allow rename symbol (F2) on this type - referenceOnly: If true, then declaration is not saved/doesn't exist and only references exist. Default ctrl+click will be goto references rather than goto definition. - hoverOnly: boolean - if true, this match type is only used for hover displays - noop: boolean - if true, nothing is done with this match type (but still useful for terminating word searching early) -} -*/ -const matchType = { - LOCAL_VAR: { - id: 'LOCAL_VAR', types: [], fileTypes: ['rs2'], cache: false, allowRename: true, - }, - GLOBAL_VAR: { - id: 'GLOBAL_VAR', types: ['var'], fileTypes: ['varp', 'varbit', 'vars', 'varn'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'varpconfig'}, - postProcessor: dataTypePostProcessor - }, - CONSTANT: { - id: 'CONSTANT', types: [], fileTypes: ['constant'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'constants', [BLOCK_SKIP_LINES]: 0}, - }, - LABEL: { - id: 'LABEL', types: ['label'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - PROC: { - id: 'PROC', types: ['proc'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - TIMER: { - id: 'TIMER', types: ['timer'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - SOFTTIMER: { - id: 'SOFTTIMER', types: ['softtimer'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - QUEUE: { - id: 'QUEUE', types: ['queue'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - SEQ: { - id: 'SEQ', types: ['seq'], fileTypes: ['seq'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'seqconfig'}, - }, - SPOTANIM: { - id: 'SPOTANIM', types: ['spotanim'], fileTypes: ['spotanim'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'spotanimconfig'}, - }, - HUNT: { - id: 'HUNT', types: ['hunt'], fileTypes: ['hunt'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'huntconfig', [CONFIG_INCLUSIONS]: ['type']}, - }, - LOC: { - id: 'LOC', types: ['loc'], fileTypes: ['loc'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'locconfig', [CONFIG_INCLUSIONS]: ['name', 'desc', 'category']}, - }, - NPC: { - id: 'NPC', types: ['npc'], fileTypes: ['npc'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'npcconfig', [CONFIG_INCLUSIONS]: ['name', 'desc', 'category']}, - }, - OBJ: { - id: 'OBJ', types: ['namedobj', 'obj'], fileTypes: ['obj'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'objconfig', [CONFIG_INCLUSIONS]: ['name', 'desc', 'category']}, - }, - INV: { - id: 'INV', types: ['inv'], fileTypes: ['inv'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'invconfig', [CONFIG_INCLUSIONS]: ['scope', 'size']}, - }, - ENUM: { - id: 'ENUM', types: ['enum'], fileTypes: ['enum'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'enumconfig', [CONFIG_INCLUSIONS]: ['inputtype', 'outputtype']}, - postProcessor: enumPostProcessor - }, - DBCOLUMN: { - id: 'DBCOLUMN', types: ['dbcolumn'], fileTypes: ['dbtable'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'runescript', [BLOCK_SKIP_LINES]: 0}, - postProcessor: columnPostProcessor - }, - DBROW: { - id: 'DBROW', types: ['dbrow'], fileTypes: ['dbrow'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'dbrowconfig', [CONFIG_INCLUSIONS]: ['table']}, - postProcessor: rowPostProcessor - }, - DBTABLE: { - id: 'DBTABLE', types: ['dbtable'], fileTypes: ['dbtable'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'dbtableconfig'}, - }, - INTERFACE: { - id: 'INTERFACE', types: ['interface'], fileTypes: ['if'], cache: true, allowRename: false, referenceOnly: true, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'interface'}, - postProcessor: fileNamePostProcessor - }, - COMPONENT: { - id: 'COMPONENT', types: ['component'], fileTypes: ['if'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'interface'}, - postProcessor: componentPostProcessor - }, - PARAM: { - id: 'PARAM', types: ['param'], fileTypes: ['param'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'paramconfig'}, - postProcessor: dataTypePostProcessor - }, - COMMAND: { - id: 'COMMAND', types: [], fileTypes: ['rs2'], cache: true, allowRename: false, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - SYNTH: { - id: 'SYNTH', types: ['synth'], fileTypes: ['synth'], cache: true, allowRename: true, referenceOnly: true, renameFile: true, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, INFO]}, - postProcessor: fileNamePostProcessor - }, - MODEL: { - id: 'MODEL', types: ['ob2', 'model'], fileTypes: ['ob2'], cache: true, allowRename: true, referenceOnly: true, renameFile: true, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, INFO]}, - }, - WALKTRIGGER: { - id: 'WALKTRIGGER', types: ['walktrigger'], fileTypes: ['rs2'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, SIGNATURE]}, - }, - IDK: { - id: 'IDK', types: ['idk', 'idkit'], fileTypes: ['idk'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO, CODEBLOCK], [LANGUAGE]: 'idkconfig'}, - }, - MESANIM: { - id: 'MESANIM', types: ['mesanim'], fileTypes: ['mesanim'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'mesanimconfig'}, - }, - STRUCT: { - id: 'STRUCT', types: ['struct'], fileTypes: ['struct'], cache: true, allowRename: true, - hoverConfig: {[DECLARATION_HOVER_ITEMS]: [TITLE, INFO], [REFERENCE_HOVER_ITEMS]: [TITLE, INFO], [LANGUAGE]: 'structconfig'}, - }, - // Hover only match types that are only used for displaying hover displays (no finding references/declarations) - // Useful for terminating word searches early when detected. Postprocessing can be done on these. - // Specify referenceConfig to select which displayItems should be shown on hover. - COORDINATES: { - id: 'COORDINATES', types: [], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, VALUE]}, - postProcessor: coordPostProcessor - }, - CONFIG_KEY: { - id: 'CONFIG_KEY', types: [], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, INFO]}, - postProcessor: configKeyPostProcessor - }, - TRIGGER: { - id: 'TRIGGER', types: [], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, INFO]}, - postProcessor: triggerPostProcessor - }, - STAT: { - id: 'STAT', types: ['stat'], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE]}, - }, - NPC_STAT: { - id: 'NPC_STAT', types: ['npc_stat'], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE]}, - }, - NPC_MODE: { - id: 'NPC_MODE', types: ['npc_mode'], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE]}, - }, - LOCSHAPE: { - id: 'LOCSHAPE', types: ['locshape'], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE]}, - }, - FONTMETRICS: { - id: 'FONTMETRICS', types: ['fontmetrics'], hoverOnly: true, cache: false, allowRename: false, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE]}, - }, - CATEGORY: { - id: 'CATEGORY', types: ['category'], hoverOnly: true, cache: true, allowRename: true, referenceOnly: true, - hoverConfig: {[REFERENCE_HOVER_ITEMS]: [TITLE, VALUE]}, - postProcessor: categoryPostProcessor - }, - // NOOP Match types that might get detected, but nothing is done with them (no hover display, no finding references/declarations) - // Useful for terminating word searching early when detected, and possibly doing something with them at a later date - UNKNOWN: { id: 'UNKNOWN', noop: true, cache: false }, // default to map to when a value is matched but no specific matchType for it - COLOR: { id: 'COLOR', noop: true, cache: false }, - NUMBER: { id: 'NUMBER', noop: true, cache: false } -}; - -module.exports = matchType; diff --git a/client/matching/matchWord.js b/client/matching/matchWord.js deleted file mode 100644 index ff70384..0000000 --- a/client/matching/matchWord.js +++ /dev/null @@ -1,131 +0,0 @@ -const matchType = require('./matchType'); -const { getWordAtIndex, getBaseContext } = require('../utils/matchUtils'); -const { getParentDeclaration } = require('../cache/identifierCache'); -const { LOC_MODEL } = require('../enum/regex'); - -// Do not reorder the matchers unless there is a reason to -// quicker potential matches are processed earlier in order to short circuit faster -const matchers = [ - require('./matchers/packMatcher'), - require('./matchers/regexWordMatcher'), - require('./matchers/commandMatcher'), - require('./matchers/localVarMatcher'), - require('./matchers/prevCharMatcher'), - require('./matchers/triggerMatcher'), - require('./matchers/configMatcher').configMatcher, - require('./matchers/switchCaseMatcher'), - require('./matchers/parametersMatcher').parametersMatcher -]; - -/** - * Match with one word given a vscode document and a vscode position - */ -function matchWordFromDocument(document, position) { - return matchWord(document.lineAt(position.line).text, position.line, document.uri, position.character); -} - -/** - * Match with one word given a line of text and an index position - */ -function matchWord(lineText, lineNum, uri, index) { - if (!lineText || !uri || !index) { - return undefined; - } - const context = getBaseContext(lineText, lineNum, uri); - const word = getWordAtIndex(context.words, index); - const wordContext = { - ...context, - word: word, - lineIndex: index, - prevWord: (word.index === 0) ? undefined : context.words[word.index - 1], - prevChar: lineText.charAt(word.start - 1), - nextChar: lineText.charAt(word.end + 1), - } - return match(wordContext); -} - -/** - * Match with all words given a line of text - */ -function matchWords(lineText, lineNum, uri) { - if (!lineText || !uri) { - return undefined; - } - const context = getBaseContext(lineText, lineNum, uri); - const matches = []; - for (let i = 0; i < context.words.length; i++) { - const wordContext = { - ...context, - word: context.words[i], - lineIndex: context.words[i].start, - prevWord: (i === 0) ? undefined : context.words[i-1], - prevChar: lineText.charAt(context.words[i].start - 1), - nextChar: lineText.charAt(context.words[i].end + 1), - } - matches.push(match(wordContext)); - } - return matches; -} - -/** - * Iterates thru all matchers to try to find a match, short circuits early if a match is made - */ -function match(context) { - if (!context.word || context.word.value === 'null') { // Also ignore null - return response(); - } - - for (const matcher of matchers) { - let match = matcher(context); - if (match) { - return response(match, context); - } - } - return response(); -} - -/** - * Build the response object for a match response - */ -function response(match, context) { - if (!match || !context) { - return undefined; - } - if (match.id === matchType.COMPONENT.id && !context.word.value.includes(':')) { - context.word.value = `${context.file.name}:${context.word.value}`; - context.modifiedWord = true; - } - if (match.id === matchType.DBCOLUMN.id && !context.word.value.includes(':')) { - const requiredType = context.file.type === 'dbtable' ? matchType.DBTABLE.id : matchType.DBROW.id; - const iden = getParentDeclaration(context.uri, context.line.number, requiredType); - if (!iden) { - return undefined; - } - const tableName = (context.file.type === 'dbrow') ? iden.extraData.table : iden.name; - context.word.value = `${tableName}:${context.word.value}`; - context.modifiedWord = true; - } - if (match.id === matchType.OBJ.id && context.word.value.startsWith('cert_')) { - context.word.value = context.word.value.substring(5); - context.word.start = context.word.start + 5; - context.originalPrefix = 'cert_'; - context.cert = true; - context.modifiedWord = true; - } - if (match.id === matchType.CATEGORY.id && context.word.value.startsWith('_')) { - context.word.value = context.word.value.substring(1); - context.word.start = context.word.start + 1; - context.originalPrefix = '_'; - context.modifiedWord = true; - } - // If model match type, determine if it is a loc model and if so remove the suffix part (_0 or _q, etc...) - if (match.id === matchType.MODEL.id && LOC_MODEL.test(context.word.value)) { - const lastUnderscore = context.word.value.lastIndexOf("_"); - context.originalSuffix = context.word.value.slice(lastUnderscore); - context.word.value = context.word.value.slice(0, lastUnderscore); - context.modifiedWord = true; - } - return { match: match, word: context.word.value, context: context }; -} - -module.exports = { matchWord, matchWords, matchWordFromDocument }; diff --git a/client/matching/matchers/commandMatcher.js b/client/matching/matchers/commandMatcher.js deleted file mode 100644 index d8f20d8..0000000 --- a/client/matching/matchers/commandMatcher.js +++ /dev/null @@ -1,22 +0,0 @@ -const identifierCache = require("../../cache/identifierCache"); -const matchType = require("../matchType"); -const { reference, declaration } = require("../../utils/matchUtils"); -const { TRIGGER_LINE } = require("../../enum/regex"); - -/** - * Looks for matches of known engine commands - */ -function commandMatcher(context) { - const command = identifierCache.get(context.word.value, matchType.COMMAND); - if (command) { - if (context.uri.fsPath.includes("engine.rs2") && TRIGGER_LINE.test(context.line.text) && context.word.index === 1) { - return declaration(matchType.COMMAND); - } - if (command.signature.params.length > 0 && context.nextChar !== '('){ - return null; - } - return reference(matchType.COMMAND); - } -} - -module.exports = commandMatcher; diff --git a/client/matching/matchers/configMatcher.js b/client/matching/matchers/configMatcher.js deleted file mode 100644 index e1b0d96..0000000 --- a/client/matching/matchers/configMatcher.js +++ /dev/null @@ -1,147 +0,0 @@ -const { CONFIG_DECLARATION, CONFIG_LINE } = require("../../enum/regex"); -const matchType = require("../matchType"); -const { declaration, reference } = require("../../utils/matchUtils"); -const dataTypeToMatchId = require("../../resource/dataTypeToMatchId"); -const { regexConfigKeys, configKeys, specialCaseKeys } = require("../../resource/configKeys"); -const identifierCache = require('../../cache/identifierCache'); - -/** - * Looks for matches on config files, both config declarations and config line items - */ -function configMatcher(context) { - // Check for config file declarations (i.e. declarations with [NAME]) - if (CONFIG_DECLARATION.test(context.line.text)) { - return declarationMatcher(context); - } - - // Check if the line we are matching is a config line - const configMatch = getConfigLineMatch(context); - return configMatch ? configMatch.match : undefined; -} - -function declarationMatcher(context) { - switch (context.file.type) { - case "varp": case "varbit": case "varn": case "vars": return declaration(matchType.GLOBAL_VAR); - case "obj": return declaration(matchType.OBJ); - case "loc": return declaration(matchType.LOC); - case "npc": return declaration(matchType.NPC); - case "param": return declaration(matchType.PARAM); - case "seq": return declaration(matchType.SEQ); - case "struct": return declaration(matchType.STRUCT); - case "dbrow": return declaration(matchType.DBROW); - case "dbtable": return declaration(matchType.DBTABLE); - case "enum": return declaration(matchType.ENUM); - case "hunt": return declaration(matchType.HUNT); - case "inv": return declaration(matchType.INV); - case "spotanim": return declaration(matchType.SPOTANIM); - case "idk": return declaration(matchType.IDK); - case "mesanim": return declaration(matchType.MESANIM); - case "if": return declaration(matchType.COMPONENT) - } -} - -function getConfigLineMatch(context) { - if (!CONFIG_LINE.test(context.line.text)) return null; - const configKey = context.words[0].value; - let response = {key: configKey}; - // The config key itsself is selected, so check if it is a known config key or not (config key with info) - if (context.word.index === 0) { - return {...response, match: reference(matchType.CONFIG_KEY)}; - } - // Check for special cases that need to be manually handled - if (specialCaseKeys.includes(configKey)) { - return handleSpecialCases(response, configKey, context); - } - // Otherwise, if the second word is the selected word (word after '=') then handle remaining known keys/regex keys - if (context.word.index >= 1) { - const configMatch = configKeys[configKey] || getRegexKey(configKey, context); - if (configMatch) { - const paramIndex = getParamIndex(context); - const param = configMatch.params[paramIndex]; - if (param) { - const match = (param.declaration) ? declaration(matchType[dataTypeToMatchId(param.typeId)]) : reference(matchType[dataTypeToMatchId(param.typeId)]); - return {...response, match: match, params: configMatch.params.map(p => p.typeId), index: paramIndex}; - } - } - } - return null; -} - -function getRegexKey(configKey, context) { - const fileTypeRegexMatchers = regexConfigKeys.get(context.file.type) || []; - for (let regexKey of fileTypeRegexMatchers) { - if (regexKey.regex.test(configKey)) { - return regexKey; - } - } - return null; -} - -function getParamIndex(context) { - let line = context.line.text; - let index = 0; - const split = line.substring(index).split(','); - for (i = 0; i < split.length; i++) { - index += split[i].length + 1; - if (context.lineIndex < index) { - return i; - } - } - return undefined; -} - -function handleSpecialCases(response, key, context) { - switch (key) { - case 'param': return paramSpecialCase(response, context); - case 'val': return valSpecialCase(response, context); - case 'data': return dataSpecialCase(response, context); - } -} - -function paramSpecialCase(response, context) { - if (context.word.index === 1) { - return {...response, match: reference(matchType.PARAM), params: ['param','value'], index: 0}; - } - if (context.word.index === 2) { - const paramIdentifier = identifierCache.get(context.words[1].value, matchType.PARAM); - if (paramIdentifier && paramIdentifier.extraData) { - const match = reference(matchType[dataTypeToMatchId(paramIdentifier.extraData.dataType)]); - return {...response, match: match, params: [paramIdentifier.name, paramIdentifier.extraData.dataType], index: 1}; - } - } - return {...response, match: matchType.UNKNOWN}; -} - -function valSpecialCase(response, context) { - const enumIdentifier = identifierCache.getParentDeclaration(context.uri, context.line.number); - if (enumIdentifier) { - response.params = [enumIdentifier.extraData.inputType, enumIdentifier.extraData.outputType]; - response.index = getParamIndex(context); - response.match = reference(matchType[dataTypeToMatchId(response.params[response.index])]); - return response; - } - return {...response, match: matchType.UNKNOWN}; -} - -function dataSpecialCase(response, context) { - if (context.word.index === 1) { - return {...response, match: reference(matchType.DBCOLUMN), params: ['dbcolumn', 'fields...'], index: 0}; - } - if (context.word.index > 1) { - let colName = context.words[1].value; - if (context.words[1].value.indexOf(':') < 0) { - const row = identifierCache.getParentDeclaration(context.uri, context.line.number); - colName = `${row.extraData.table}:${context.words[1].value}` - } - const col = identifierCache.get(colName, matchType.DBCOLUMN); - if (col && col.extraData) { - response.params = [col.name, ...col.extraData.dataTypes]; - response.index = getParamIndex(context); - response.match = reference(matchType[dataTypeToMatchId(response.params[response.index])]); - return response; - } - } - return {...response, match: matchType.UNKNOWN}; -} - -module.exports = { configMatcher, getConfigLineMatch }; diff --git a/client/matching/matchers/localVarMatcher.js b/client/matching/matchers/localVarMatcher.js deleted file mode 100644 index 6235f83..0000000 --- a/client/matching/matchers/localVarMatcher.js +++ /dev/null @@ -1,23 +0,0 @@ -const matchType = require("../matchType"); -const { reference, declaration } = require("../../utils/matchUtils"); - -/** - * Looks for matches of local variables - */ -function matchLocalVar(context) { - if (context.prevChar === '$') { - let prevWord = context.prevWord; - if (!prevWord) { - return reference(matchType.LOCAL_VAR); - } - prevWord = prevWord.value; - if (prevWord.startsWith("def_")) { - prevWord = prevWord.substr(4); - } - const defKeyword = "\\b(int|string|boolean|seq|locshape|component|idk|midi|npc_mode|namedobj|synth|stat|npc_stat|fontmetrics|enum|loc|model|npc|obj|player_uid|spotanim|npc_uid|inv|category|struct|dbrow|interface|dbtable|coord|mesanim|param|queue|weakqueue|timer|softtimer|char|dbcolumn|proc|label)\\b"; - const match = prevWord.match(new RegExp(defKeyword)); - return !match ? reference(matchType.LOCAL_VAR) : declaration(matchType.LOCAL_VAR); - } -} - -module.exports = matchLocalVar; diff --git a/client/matching/matchers/packMatcher.js b/client/matching/matchers/packMatcher.js deleted file mode 100644 index 5642a78..0000000 --- a/client/matching/matchers/packMatcher.js +++ /dev/null @@ -1,25 +0,0 @@ -const { reference } = require("../../utils/matchUtils"); -const dataTypeToMatchId = require("../../resource/dataTypeToMatchId"); -const matchType = require("../matchType"); - -/** - * Looks for matches in pack files - */ -function packMatcher(context) { - if (context.file.type === 'pack' && context.word.index === 1) { - let match; - if (matchType.GLOBAL_VAR.fileTypes.includes(context.file.name)) { - match = matchType.GLOBAL_VAR; - } else if(context.file.name === 'interface' && context.word.value.includes(':')) { - match = matchType.COMPONENT; - } else { - match = matchType[dataTypeToMatchId(context.file.name)]; - } - if (match.id !== matchType.UNKNOWN.id) { - context.packId = context.words[0].value; - } - return reference(match); - } -} - -module.exports = packMatcher; diff --git a/client/matching/matchers/parametersMatcher.js b/client/matching/matchers/parametersMatcher.js deleted file mode 100644 index 348be36..0000000 --- a/client/matching/matchers/parametersMatcher.js +++ /dev/null @@ -1,155 +0,0 @@ -const matchType = require('../matchType'); -const identifierCache = require('../../cache/identifierCache'); -const returnBlockLinesCache = require('../../cache/returnBlockLinesCache'); -const { getWordAtIndex, reference } = require('../../utils/matchUtils'); - -/** - * Looks for matches of values inside of parenthesis - * This includes return statement params, engine command parameters, proc parameters, label parameters, and queue parameters - */ -function parametersMatcher(context) { - if (context.file.type !== 'rs2') { - return null; - } - const paramsIdentifier = getParamsMatch(context); - return (paramsIdentifier) ? paramsIdentifier.match : null; -} - -// Checks if the index location of a line of code is within the parenthesis of an identifier -// If it is, it returns which param index the cursor is at, the match type of that param, and the parent identifier itself -function getParamsMatch(context) { - const { identifierName, paramIndex } = parseForIdentifierNameAndParamIndex(context.line.text, context.lineIndex, context.words); - if (!identifierName) { - return null; - } - const name = identifierName.value; - const prev = context.line.text.charAt(identifierName.start - 1); - - if (name === 'return') { - const blockIdentifierKey = returnBlockLinesCache.get(context.line.number, context.uri); - if (blockIdentifierKey) { - const iden = identifierCache.getByKey(blockIdentifierKey); - if (iden && iden.signature && iden.signature.returns.length > paramIndex) { - return {identifier: iden, index: paramIndex, match: reference(matchType[iden.signature.returns[paramIndex]]), isReturns: true}; - } - } - return null; - } - - let iden; - let indexOffset = 0; - let dynamicCommand; - if (name === 'queue') { - indexOffset = 2; - if (paramIndex < indexOffset) { - iden = identifierCache.get(name, matchType.COMMAND); - indexOffset = 0; - } else { - const queueName = getWordAtIndex(context.words, identifierName.end + 2); - iden = (queueName) ? identifierCache.get(queueName.value, matchType.QUEUE) : null; - dynamicCommand = name; - } - } else if (name === 'longqueue') { - indexOffset = 3; - if (paramIndex < indexOffset) { - iden = identifierCache.get(name, matchType.COMMAND); - indexOffset = 0; - } else { - const queueName = getWordAtIndex(context.words, identifierName.end + 2); - iden = (queueName) ? identifierCache.get(queueName.value, matchType.QUEUE) : null; - dynamicCommand = name; - } - } else if (prev === '@') { - iden = identifierCache.get(name, matchType.LABEL); - } else if (prev === '~') { - iden = identifierCache.get(name, matchType.PROC); - } else { - iden = identifierCache.get(name, matchType.COMMAND); - } - if (!iden) { - return null; - } - const response = {identifier: iden, index: paramIndex, isReturns: false, dynamicCommand: dynamicCommand}; - if (iden.signature && iden.signature.params.length > (paramIndex - indexOffset)) { - response.match = reference(matchType[iden.signature.params[(paramIndex - indexOffset)].matchTypeId]); - } - return response; -} - -// Determines if we are inside of an identifiers parenthesis, and returns which param index it is if we are -// Scans the characters from the cursor index to the begining of the code -function parseForIdentifierNameAndParamIndex(lineText, index, words) { - const init = initializeString(lineText, index); - lineText = init.interpolatedText || lineText; - let isInString = init.isInString; - let isInInterpolated = 0; - let isInParams = 0; - let paramIndex = 0; - for (let i = index; i >= 0; i--) { - const char = lineText.charAt(i); - - // Handle interpolated code inside of strings, and nested interpolated code - if (char === '>') isInInterpolated++; - if (isInInterpolated > 0) { - if (char === '<') isInInterpolated--; - continue; - } - - // Handle strings and escaped quotes within strings - if (isInString) { - if (char === '"' && i > 0 && lineText.charAt(i - 1) !== '\\') isInString = false; - continue; - } - else if (char === '"' && i > 0 && lineText.charAt(i - 1) !== '\\') { - isInString = true; - continue; - } - - // Handle nested parenthesis - if (char === ')') isInParams++; - if (isInParams > 0) { - if (char === '(') isInParams--; - continue; - } - - // === Code below is only reached when not inside a string, interpolated code, or nested params === - - // Increase param index when a comma is found - if (char === ',') { - paramIndex++; - } - // Reached the end of interpolated code without finding a match, exit early with null response - if (char === '<') { - return {identifierName: null, paramIndex: null}; - } - // Found an opening parenthesis which marks the end of our search, return the previous word (the identifier name) - if (char === '(') { - return {identifierName: getWordAtIndex(words, i - 2), paramIndex: paramIndex}; - } - } - return {identifierName: null, paramIndex: null}; -} - -// Determines if we are currently inside of a string, and if we are inside of interpolated code -// Scans the characters from the cursor index to the end of the line of code -function initializeString(lineText, index) { - let quoteCount = 0; - let interpolatedCount = 0; - for (let i = index; i < lineText.length; i++) { - if (lineText.charAt(i) === '"' && i > 0 && lineText.charAt(i - 1) !== '\\') { - quoteCount++; - } - if (lineText.charAt(i) === '>') { - if (interpolatedCount === 0) { - return { interpolatedText: lineText.substring(0, i - 1), isInString: quoteCount % 2 === 1 }; - } - interpolatedCount--; - } - if (lineText.charAt(i) === '<') { - interpolatedCount++; - } - } - return { isInString: quoteCount % 2 === 1 }; -} - -module.exports = { parametersMatcher, getParamsMatch }; diff --git a/client/matching/matchers/prevCharMatcher.js b/client/matching/matchers/prevCharMatcher.js deleted file mode 100644 index 5f36424..0000000 --- a/client/matching/matchers/prevCharMatcher.js +++ /dev/null @@ -1,17 +0,0 @@ -const matchType = require("../matchType"); -const { reference, declaration } = require("../../utils/matchUtils"); - -/** - * Looks for matches based on the previous character, such as ~WORD indicates a proc reference - */ -function prevCharMatcher(context) { - switch (context.prevChar) { - case '^': return (context.file.type === "constant") ? declaration(matchType.CONSTANT) : reference(matchType.CONSTANT); - case '%': return reference(matchType.GLOBAL_VAR); - case '@': return (context.nextChar === '@') ? null : reference(matchType.LABEL); - case '~': return reference(matchType.PROC); - case ',': return (context.prevWord.value === "p") ? reference(matchType.MESANIM) : null; - } -} - -module.exports = prevCharMatcher; diff --git a/client/matching/matchers/regexWordMatcher.js b/client/matching/matchers/regexWordMatcher.js deleted file mode 100644 index 6a65534..0000000 --- a/client/matching/matchers/regexWordMatcher.js +++ /dev/null @@ -1,21 +0,0 @@ -const { COLOR, COORD, NUMBER } = require("../../enum/regex"); -const matchType = require("../matchType"); -const { reference } = require("../../utils/matchUtils"); - -/** - * Looks for matches with direct word regex checks, such as for coordinates - */ -function regexWordMatcher(context) { - const word = context.word.value; - if (COORD.test(word)) { - return reference(matchType.COORDINATES); - } - if (COLOR.test(word)) { - return reference(matchType.COLOR); - } - if (NUMBER.test(word)) { - return reference(matchType.NUMBER); - } -} - -module.exports = regexWordMatcher; diff --git a/client/matching/matchers/switchCaseMatcher.js b/client/matching/matchers/switchCaseMatcher.js deleted file mode 100644 index 2b456c3..0000000 --- a/client/matching/matchers/switchCaseMatcher.js +++ /dev/null @@ -1,17 +0,0 @@ -const { SWITCH_CASE } = require("../../enum/regex"); -const matchType = require("../matchType"); -const { reference } = require("../../utils/matchUtils"); -const switchStmtLinesCache = require("../../cache/switchStmtLinesCache"); - -/** - * Looks for matches in case statements - */ -function switchCaseMatcher(context) { - if (context.file.type === 'rs2' && context.word.index > 0 && context.word.value !== 'default' && - SWITCH_CASE.test(context.line.text) && context.lineIndex < context.line.text.indexOf(' :')) { - const matchTypeId = switchStmtLinesCache.get(context.line.number, context.uri); - return matchTypeId ? reference(matchType[matchTypeId]) : matchType.UNKNOWN; - } -} - -module.exports = switchCaseMatcher; diff --git a/client/matching/matchers/triggerMatcher.js b/client/matching/matchers/triggerMatcher.js deleted file mode 100644 index 769338d..0000000 --- a/client/matching/matchers/triggerMatcher.js +++ /dev/null @@ -1,27 +0,0 @@ -const { TRIGGER_LINE } = require("../../enum/regex"); -const matchType = require("../matchType"); -const triggers = require("../../resource/triggers"); -const { reference, declaration } = require("../../utils/matchUtils"); - -/** - * Looks for matches with known runescript triggers, see triggers.js - */ -function triggerMatcher(context) { - if (context.file.type !== 'rs2') { - return null; - } - if (TRIGGER_LINE.test(context.line.text) && context.word.index <= 1) { - const trigger = triggers[context.words[0].value.toLowerCase()]; - if (trigger) { - if (context.word.index === 0) { - return reference(matchType.TRIGGER, {triggerName: context.words[1].value}); - } - if (context.word.value.charAt(0) === '_') { - return reference(matchType.CATEGORY, {matchId: trigger.match.id, categoryName: context.word.value.substring(1)}); - } - return trigger.declaration ? declaration(trigger.match) : reference(trigger.match); - } - } -} - -module.exports = triggerMatcher; diff --git a/client/provider/color24Provider.js b/client/provider/color24Provider.js deleted file mode 100644 index 8b80bbc..0000000 --- a/client/provider/color24Provider.js +++ /dev/null @@ -1,41 +0,0 @@ -const vscode = require('vscode'); -const { COLOR24 } = require('../enum/regex'); - -const color24Provider = { - provideColorPresentations(color, context, token) { - const r = Math.round(color.red * 255); - const g = Math.round(color.green * 255); - const b = Math.round(color.blue * 255); - const rgb = (r << 16) | (g << 8) | b; - - return [ - { - label: 'Color Picker', - textEdit: new vscode.TextEdit(context.range, '0x' + rgb.toString(16).toUpperCase().padStart(6, '0')) - } - ]; - }, - - provideDocumentColors(document) { - const text = document.getText(); - let match; - - const matches = []; - while (match = COLOR24.exec(text)) { - const rgb = parseInt(match[2], 16); - - const r = (rgb >> 16) & 0xFF; - const g = (rgb >> 8) & 0xFF; - const b = rgb & 0xFF; - - matches.push({ - color: new vscode.Color(r / 255, g / 255, b / 255, 1), - range: new vscode.Range(document.positionAt(match.index + match[1].length + 1), document.positionAt(match.index + match[1].length + match[2].length + 1)) - }); - } - - return matches; - } -}; - -module.exports = color24Provider; diff --git a/client/provider/completionProvider.js b/client/provider/completionProvider.js deleted file mode 100644 index 439b0d2..0000000 --- a/client/provider/completionProvider.js +++ /dev/null @@ -1,132 +0,0 @@ -const vscode = require('vscode'); -const activeFileCache = require('../cache/activeFileCache'); -const completionCache = require('../cache/completionCache'); -const matchType = require('../matching/matchType'); -const { matchWord } = require('../matching/matchWord'); -const activeCursorCache = require('../cache/activeCursorCache'); -const runescriptTrigger = require('../resource/triggers'); - -const triggers = ['$', '^', '%', '~', '@', '`', '>']; -const autoTriggeredTypeIds = [ - matchType.CONSTANT.id, - matchType.GLOBAL_VAR.id, - matchType.LOCAL_VAR.id, - matchType.PROC.id, - matchType.LABEL.id -]; - -const provider = { - provideCompletionItems(document, position, cancellationToken, context) { - if (context.triggerKind === 1) { - if (context.triggerCharacter === '`' && position.character > 1 && - document.lineAt(position.line).text.charAt(position.character - 2) === '`') { - return searchForMatchType(document, position, true); - } - return invoke(document, position, position.character - 1, ''); - } - const wordRange = document.getWordRangeAtPosition(position); - const word = (!wordRange) ? '' : document.getText(wordRange); - const triggerIndex = (!wordRange) ? position.character - 1 : wordRange.start.character - 1; - return invoke(document, position, triggerIndex, word); - } -} - -function invoke(document, position, triggerIndex, word) { - switch (document.lineAt(position.line).text.charAt(triggerIndex)) { - case '$': return completeLocalVar(position); - case '`': return completionTypeSelector(position); - case '>': return completionByType(document, position, triggerIndex, word); - case '^': return completionByTrigger(word, matchType.CONSTANT.id); - case '%': return completionByTrigger(word, matchType.GLOBAL_VAR.id); - case '~': return completionByTrigger(word, matchType.PROC.id); - case '@': return completionByTrigger(word, matchType.LABEL.id); - default: return searchForMatchType(document, position); - } -} - -function completeLocalVar(position) { - const completionItems = []; - const completionKind = getCompletionItemKind(matchType.LOCAL_VAR.id); - const scriptData = activeFileCache.getScriptData(position.line); - if (scriptData) { - Object.keys(scriptData.variables).forEach(varName => { - const localVar = scriptData.variables[varName]; - const range = localVar.declaration.range; - if (position.line > range.start.line || (position.line === range.start.line && position.character > range.end.character)) { - const item = new vscode.CompletionItem(varName, completionKind); - item.range = new vscode.Range(position.translate(0, -1), position); - item.detail = localVar.parameter ? `${localVar.type} (param)` : localVar.type; - completionItems.push(item); - } - }); - } - return completionItems; -} - -function completionByTrigger(prefix, matchTypeId, additionalTextEdits) { - let identifierNames; - if (matchTypeId === matchType.TRIGGER.id) { - identifierNames = Object.keys(runescriptTrigger); - } else { - identifierNames = completionCache.getAllWithPrefix(prefix, matchTypeId); - } - if (!identifierNames) { - return null; - } - const completionKind = getCompletionItemKind(matchTypeId); - const completionItems = []; - identifierNames.forEach(identifierName => { - const item = new vscode.CompletionItem(identifierName, completionKind); - item.detail = matchTypeId.toLowerCase(); - if (additionalTextEdits) item.additionalTextEdits = additionalTextEdits; - completionItems.push(item); - }); - return completionItems; -} - -function completionTypeSelector(position) { - const completionItems = completionCache.getTypes().filter(type => !autoTriggeredTypeIds.includes(type)).map(type => { - const item = new vscode.CompletionItem(`${type}>`, vscode.CompletionItemKind.Enum); - item.additionalTextEdits = [vscode.TextEdit.delete(new vscode.Range(position.translate(0, -1), position))]; - item.command = { command: 'editor.action.triggerSuggest' }; - return item; - }); - return completionItems; -} - -function completionByType(document, position, triggerIndex, word) { - prevWordRange = document.getWordRangeAtPosition(new vscode.Position(position.line, triggerIndex)); - if (!prevWordRange) { - return null; - } - const matchTypeId = document.getText(prevWordRange); - const additionalTextEdits = [vscode.TextEdit.delete(new vscode.Range(prevWordRange.start, prevWordRange.end.translate(0, 1)))]; - return completionByTrigger(word, matchTypeId, additionalTextEdits); -} - -function searchForMatchType(document, position, fromTrigger = false) { - const triggerOffset = fromTrigger ? 2 : 0; - let matchTypeId = fromTrigger ? false : activeCursorCache.get(document, position); - if (!matchTypeId) { - let str = document.lineAt(position.line).text; - str = str.substring(0, position.character - triggerOffset) + 'temp' + str.substring(position.character); - const match = matchWord(str, position.line, document.uri, position.character); - matchTypeId = (match) ? match.match.id : matchType.COMMAND.id; - } - const additionalTextEdits = [vscode.TextEdit.delete(new vscode.Range(position.translate(0, -triggerOffset), position))]; - return completionByTrigger('', matchTypeId, additionalTextEdits); -} - -function getCompletionItemKind(matchTypeId) { - switch (matchTypeId) { - case matchType.CONSTANT.id: return vscode.CompletionItemKind.Constant; - case matchType.LOCAL_VAR.id: - case matchType.GLOBAL_VAR.id: return vscode.CompletionItemKind.Variable; - case matchType.COMMAND.id: - case matchType.PROC.id: - case matchType.LABEL.id: return vscode.CompletionItemKind.Function; - default: return vscode.CompletionItemKind.Text; - } -} - -module.exports = {triggers, provider}; diff --git a/client/provider/configHelpProvider.js b/client/provider/configHelpProvider.js deleted file mode 100644 index 0c1a5c9..0000000 --- a/client/provider/configHelpProvider.js +++ /dev/null @@ -1,56 +0,0 @@ -const vscode = require('vscode'); -const { getBaseContext, getWordAtIndex } = require('../utils/matchUtils'); -const { getConfigLineMatch } = require('../matching/matchers/configMatcher'); -const dataTypeToMatchId = require('../resource/dataTypeToMatchId'); -const { contains } = require('../cache/completionCache'); -const matchType = require('../matching/matchType'); -const activeCursorCache = require('../cache/activeCursorCache'); - -const metadata = { - triggerCharacters: ['=', ','], - retriggerCharacters: [','] -} - -const provider = { - provideSignatureHelp(document, position) { - let str = document.lineAt(position.line).text; - str = str.substring(0, position.character) + 'temp' + str.substring(position.character); - const matchContext = getBaseContext(str, position.line, document.uri); - matchContext.lineIndex = position.character + 1; - matchContext.word = getWordAtIndex(matchContext.words, matchContext.lineIndex); - const config = getConfigLineMatch(matchContext); - if (!config) { - return null; - } - - // Build the signature info - const signatureInfo = new vscode.SignatureInformation(`${config.key}=${config.params.join(',')}`); - let index = config.key.length + 1; // Starting index of params - config.params.forEach(param => { - // use range instead of param name due to possible duplicates - signatureInfo.parameters.push(new vscode.ParameterInformation([index, index + param.length])); - index += param.length + 1; - }); - signatureInfo.activeParameter = config.index; - - // Build the signature help - const signatureHelp = new vscode.SignatureHelp(); - signatureHelp.signatures.push(signatureInfo); - signatureHelp.activeSignature = 0; - invokeCompletionItems(dataTypeToMatchId(config.params[config.index]), document, position); - return signatureHelp; - } -} - -function invokeCompletionItems(matchTypeId, document, position) { - activeCursorCache.set(matchTypeId, document, position); - if (matchTypeId !== matchType.UNKNOWN.id) { - const word = document.getText(document.getWordRangeAtPosition(position)); - if (contains(word, matchTypeId)) { - return; - } - vscode.commands.executeCommand('editor.action.triggerSuggest'); - } -} - -module.exports = { provider, metadata }; diff --git a/client/provider/gotoDefinition.js b/client/provider/gotoDefinition.js deleted file mode 100644 index 4b243ae..0000000 --- a/client/provider/gotoDefinition.js +++ /dev/null @@ -1,39 +0,0 @@ -const vscode = require('vscode'); -const identifierCache = require("../cache/identifierCache"); -const matchType = require('../matching/matchType'); -const { matchWordFromDocument } = require('../matching/matchWord'); -const activeFileCache = require('../cache/activeFileCache'); - -const gotoDefinitionProvider = { - async provideDefinition(document, position) { - // Get a match for the current word, and ignore noop or hover only tagged matches - const { match, word } = matchWordFromDocument(document, position) - if (!match || match.noop || match.isHoverOnly) { - return null; - } - - // If we are already on a declaration, there is nowhere to goto. Returning current location - // indicates to vscode that we instead want to try doing "find references" - if (match.declaration || match.referenceOnly) { - return new vscode.Location(document.uri, position); - } - - // Search for the identifier and its declaration location, and goto it if found - if (match.id === matchType.LOCAL_VAR.id) { - return gotoLocalVar(position, word); - } - return gotoDefinition(word, match); - } -} - -const gotoLocalVar = (position, word) => { - const scriptData = activeFileCache.getScriptData(position.line); - return (scriptData) ? (scriptData.variables[`$${word}`] || {declaration: null}).declaration : null; -} - -const gotoDefinition = async (word, match) => { - const definition = identifierCache.get(word, match); - return (definition) ? definition.declaration : null; -} - -module.exports = gotoDefinitionProvider; diff --git a/client/provider/hoverProvider.js b/client/provider/hoverProvider.js deleted file mode 100644 index eeaf2a7..0000000 --- a/client/provider/hoverProvider.js +++ /dev/null @@ -1,80 +0,0 @@ -const vscode = require('vscode'); -const matchType = require('../matching/matchType'); -const identifierCache = require('../cache/identifierCache'); -const activeFileCache = require('../cache/activeFileCache'); -const identifierFactory = require('../resource/identifierFactory'); -const { matchWordFromDocument } = require('../matching/matchWord'); -const { resolve } = require('../resource/hoverConfigResolver'); -const { DECLARATION_HOVER_ITEMS, REFERENCE_HOVER_ITEMS } = require('../enum/hoverConfigOptions'); -const { markdownBase, appendTitle, appendInfo, appendValue, appendSignature, - appendCodeBlock, expectedIdentifierMessage } = require('../utils/markdownUtils'); - -const hoverProvider = function(context) { - return { - async provideHover(document, position) { - // Find a match for the word user is hovering over, and ignore noop tagged matches - const { word, match, context: matchContext } = matchWordFromDocument(document, position); - if (!match || match.noop) { - return null; - } - - // Setup the hover text markdown content object - const markdown = markdownBase(context); - - // Local vars are handled differently than the rest - if (match.id === matchType.LOCAL_VAR.id) { - appendLocalVarHoverText(position, word, match, markdown); - return new vscode.Hover(markdown); - } - - // If no config found, or no items to display then exit early - const hoverDisplayItems = (match.declaration) ? resolve(DECLARATION_HOVER_ITEMS, match) : resolve(REFERENCE_HOVER_ITEMS, match); - if (hoverDisplayItems.length === 0) { - return null; - } - - // Get/Build identifier object for the match found - const identifier = getIdentifier(word, match, document, position); - - // No identifier or hideDisplay property is set, then there is nothing to display - if (!identifier || identifier.hideDisplay) { - return null; - } - - // Match type is a reference, but it has no declaration => display a warning message "expected identifier" - if (!match.declaration && !match.referenceOnly && !identifier.declaration) { - expectedIdentifierMessage(word, match, markdown); - return new vscode.Hover(markdown); - } - - // Append the registered hoverDisplayItems defined in the matchType for the identifier - appendTitle(identifier.name, identifier.fileType, identifier.matchId, markdown, identifier.id, matchContext.cert); - appendInfo(identifier, hoverDisplayItems, markdown); - appendValue(identifier, hoverDisplayItems, markdown); - appendSignature(identifier, hoverDisplayItems, markdown); - appendCodeBlock(identifier, hoverDisplayItems, markdown); - return new vscode.Hover(markdown); - } - }; -} - -function appendLocalVarHoverText(position, word, match, markdown) { - const scriptData = activeFileCache.getScriptData(position.line); - if (scriptData) { - const variable = scriptData.variables[`$${word}`]; - if (variable) { - appendTitle(word, 'rs2', match.id, markdown); - markdown.appendCodeblock(variable.parameter ? `${variable.type} $${word} (script parameter)` : `${variable.type} $${word}`, 'runescript'); - } else { - expectedIdentifierMessage(word, match, markdown); - } - } -} - -function getIdentifier(word, match, document, position) { - return (match.hoverOnly) ? - identifierFactory.build(word, match, new vscode.Location(document.uri, position)) : - identifierCache.get(word, match, match.declaration ? document.uri : null); -} - -module.exports = hoverProvider; diff --git a/client/provider/recolorProvider.js b/client/provider/recolorProvider.js deleted file mode 100644 index 916e543..0000000 --- a/client/provider/recolorProvider.js +++ /dev/null @@ -1,41 +0,0 @@ -const vscode = require('vscode'); -const { RECOLOR } = require('../enum/regex'); - -const recolProvider = { - provideColorPresentations(color, context, token) { - const r = Math.round(color.red * 31); - const g = Math.round(color.green * 31); - const b = Math.round(color.blue * 31); - const rgb = (r << 10) | (g << 5) | b; - - return [ - { - label: 'Model Recolor', - textEdit: new vscode.TextEdit(context.range, rgb.toString()) - } - ]; - }, - - provideDocumentColors(document) { - const text = document.getText(); - let match; - - const matches = []; - while (match = RECOLOR.exec(text)) { - const rgb = parseInt(match[2]); - - const r = (rgb >> 10) & 0x1f; - const g = (rgb >> 5) & 0x1f; - const b = rgb & 0x1f; - - matches.push({ - color: new vscode.Color(r / 31, g / 31, b / 31, 1), - range: new vscode.Range(document.positionAt(match.index + match[1].length + 1), document.positionAt(match.index + match[1].length + match[2].length + 1)) - }); - } - - return matches; - } -}; - -module.exports = recolProvider; diff --git a/client/provider/referenceProvider.js b/client/provider/referenceProvider.js deleted file mode 100644 index 0a4eea0..0000000 --- a/client/provider/referenceProvider.js +++ /dev/null @@ -1,46 +0,0 @@ -const vscode = require('vscode'); -const { matchWordFromDocument } = require('../matching/matchWord'); -const identifierCache = require('../cache/identifierCache'); -const cacheUtils = require('../utils/cacheUtils'); -const matchType = require('../matching/matchType'); -const activeFileCache = require('../cache/activeFileCache'); - -const referenceProvider = { - async provideReferences(document, position) { - // Find a match for the current word, and ignore noop or hoverOnly tagged matches - const { match, word } = matchWordFromDocument(document, position) - if (!match || match.noop || match.isHoverOnly) { - return null; - } - - // Use activeFileCache to get references of variables for active script block - if (match.id === matchType.LOCAL_VAR.id) { - const scriptData = activeFileCache.getScriptData(position.line); - if (scriptData) { - return (scriptData.variables[`$${word}`] || {references: []}).references; - } - return null; - } - - // Get the identifier from the cache - const identifier = identifierCache.get(word, match); - if (!identifier || !identifier.references) { - return null; - } - - // Decode all the references for the identifier into an array of vscode Location objects - const referenceLocations = []; - Object.keys(identifier.references).forEach(fileKey => { - const uri = vscode.Uri.file(fileKey); - identifier.references[fileKey].forEach(encodedReference => - referenceLocations.push(cacheUtils.decodeReferenceToLocation(uri, encodedReference))); - }); - // If there is only one reference and its the declaration, return null as theres no other references to show - if (match.declaration && referenceLocations.length === 1) { - return null; - } - return referenceLocations; - } -} - -module.exports = referenceProvider; diff --git a/client/provider/renameProvider.js b/client/provider/renameProvider.js deleted file mode 100644 index bb82877..0000000 --- a/client/provider/renameProvider.js +++ /dev/null @@ -1,111 +0,0 @@ -const vscode = require('vscode'); -const identifierCache = require('../cache/identifierCache'); -const { matchWordFromDocument } = require('../matching/matchWord'); -const cacheUtils = require('../utils/cacheUtils'); -const matchType = require('../matching/matchType'); -const activeFileCache = require('../cache/activeFileCache'); -const { LOC_MODEL } = require('../enum/regex'); - -const renameProvider = { - prepareRename(document, position) { - const matchedWord = matchWordFromDocument(document, position); - if (!matchedWord) { - throw new Error("Cannot rename"); - } - const { match, word } = matchedWord; - if (!match.allowRename || match.noop) { - throw new Error(`${match.id} renaming not supported`); - } - if (match.id !== matchType.LOCAL_VAR.id) { - const identifier = identifierCache.get(word, match); - if (!identifier) { - return new Error('Cannot find any references to rename'); - } - } - }, - - provideRenameEdits(document, position, newName) { - const { word, match, context } = matchWordFromDocument(document, position); - - if (match.id === matchType.LOCAL_VAR.id) { - return renameLocalVariableReferences(position, word, newName); - } - - const validationError = validate(match, newName); - if (validationError) { - vscode.window.showErrorMessage('Rename failed: ' + validationError); - return new vscode.WorkspaceEdit(); - } - - const adjustedNewName = adjustNewName(context, newName); - const identifier = identifierCache.get(word, match); - renameFiles(match, word, adjustedNewName); - return renameReferences(identifier, word, adjustedNewName); - } -} - -// Use activeFileCache to get references of variables for active script block -function renameLocalVariableReferences(position, word, newName) { - const renameWorkspaceEdits = new vscode.WorkspaceEdit(); - const scriptData = activeFileCache.getScriptData(position.line); - if (scriptData) { - (scriptData.variables[`$${word}`] || { references: [] }).references.forEach(location => { - renameWorkspaceEdits.replace(location.uri, location.range, `$${newName}`); - }); - } - return renameWorkspaceEdits; -} - -function validate(match, newName) { - if (match.id === matchType.MODEL.id && LOC_MODEL.test(newName)) { - return `Do not include final _X suffix in model renames`; - } -} - -// Decode all the references for the identifier into an array of vscode ranges, -// then use that to rename all of the references to the newName -function renameReferences(identifier, oldName, newName) { - const renameWorkspaceEdits = new vscode.WorkspaceEdit(); - if (identifier.references) { - const wordLength = oldName.length - oldName.indexOf(':') - 1; - Object.keys(identifier.references).forEach(fileKey => { - const uri = vscode.Uri.file(fileKey); - identifier.references[fileKey].forEach(encodedReference => { - const range = cacheUtils.decodeReferenceToRange(wordLength, encodedReference); - renameWorkspaceEdits.replace(uri, range, newName); - }); - }); - } - return renameWorkspaceEdits; -} - -function adjustNewName(context, newName) { - // Strip the cert_ and the _ prefix on objs or categories - if (context.originalPrefix && newName.startsWith(context.originalPrefix)) { - newName = newName.substring(context.originalPrefix.length); - } - // Strip the _0 (or others) on loc models - if (context.originalSuffix && newName.endsWith(context.originalSuffix)) { - newName = newName.slice(0, -2); - } - // Strip the left side of identifier names with colons in them - if (newName.indexOf(':') > -1) { - newName = newName.substring(newName.indexOf(':') + 1); - } - return newName; -} - -async function renameFiles(match, oldName, newName) { - if (match.renameFile && Array.isArray(match.fileTypes) && match.fileTypes.length > 0) { - const fileSearch = match.id === matchType.MODEL.id ? `**/${oldName}_*.${match.fileTypes[0]}` : `**/${oldName}.${match.fileTypes[0]}`; - const files = await vscode.workspace.findFiles(fileSearch) || []; - for (const oldUri of files) { - const suffix = match.id === matchType.MODEL.id ? oldUri.path.slice(-6, -4) : ''; - const newFileName = suffix ? `${newName}${suffix}.${match.fileTypes[0]}` : `${newName}.${match.fileTypes[0]}`; - const newUri = vscode.Uri.joinPath(oldUri.with({ path: oldUri.path.replace(/\/[^/]+$/, '') }), newFileName); - vscode.workspace.fs.rename(oldUri, newUri); - } - } -} - -module.exports = renameProvider; diff --git a/client/provider/signatureHelpProvider.js b/client/provider/signatureHelpProvider.js deleted file mode 100644 index 4667323..0000000 --- a/client/provider/signatureHelpProvider.js +++ /dev/null @@ -1,134 +0,0 @@ -const vscode = require('vscode'); -const { getBaseContext } = require('../utils/matchUtils'); -const matchType = require('../matching/matchType'); -const { get } = require('../cache/identifierCache'); -const activeCursorCache = require('../cache/activeCursorCache'); -const dataTypeToMatchId = require('../resource/dataTypeToMatchId'); -const runescriptTrigger = require('../resource/triggers'); -const { contains } = require('../cache/completionCache'); -const { getParamsMatch } = require('../matching/matchers/parametersMatcher'); - -const metadata = { - triggerCharacters: ['(', ',', '['], - retriggerCharacters: [','] -} - -const provider = { - provideSignatureHelp(document, position) { - const signatureHelp = getScriptTriggerHelp(document, position); - if (signatureHelp) { - return signatureHelp; - } - return getParametersHelp(document, position); - } -} - -function getScriptTriggerHelp(document, position) { - let matchTypeId = matchType.UNKNOWN.id; - let signatureInfo; - const str = document.lineAt(position.line).text; - if (str.charAt(0) === '[') { - if (position.character > str.indexOf(']')) { - return null; - } - const split = str.split(','); - if (split.length > 1) { - const triggerName = split[0].substring(1); - const trigger = runescriptTrigger[triggerName]; - if (trigger) { - matchTypeId = trigger.declaration ? matchType.UNKNOWN.id : trigger.match.id; - const matchLabel = matchTypeId === matchType.UNKNOWN.id ? `script_name` : matchTypeId.toLowerCase(); - signatureInfo = new vscode.SignatureInformation(`script [${triggerName},${matchLabel}]`); - signatureInfo.parameters.push(new vscode.ParameterInformation(triggerName)); - signatureInfo.parameters.push(new vscode.ParameterInformation(matchLabel)); - signatureInfo.activeParameter = 1; - } - } else { - matchTypeId = matchType.TRIGGER.id; - signatureInfo = new vscode.SignatureInformation('script [trigger,value]'); - signatureInfo.parameters.push(new vscode.ParameterInformation('trigger')); - signatureInfo.parameters.push(new vscode.ParameterInformation('value')); - signatureInfo.activeParameter = 0; - } - } - if (signatureInfo) { - const signatureHelp = new vscode.SignatureHelp(); - signatureHelp.signatures.push(signatureInfo); - signatureHelp.activeSignature = 0; - invokeCompletionItems(matchTypeId, document, position); - return signatureHelp; - } - return null; -} - -function getParametersHelp(document, position) { - let str = document.lineAt(position.line).text; - str = str.substring(0, position.character) + 'temp' + str.substring(position.character); - const matchContext = getBaseContext(str, position.line, document.uri); - matchContext.lineIndex = position.character + 1; - var paramIden = getParamsMatch(matchContext); - if (!paramIden) { - return null; - } - if (!paramIden.isReturns && paramIden.identifier.signature.paramsText.length === 0) { - return displayMessage(`${paramIden.identifier.matchId} ${paramIden.identifier.name} has no parameters, remove the parenthesis`); - } - - // For things like queues, manually handled - todo try to find better way - paramIden = handleDynamicParams(paramIden); - - // Build the signature info - const signature = paramIden.identifier.signature; - const params = (paramIden.isReturns) ? signature.returnsText : signature.paramsText; - const label = (paramIden.isReturns) ? `return (${params})` : `${paramIden.identifier.name}(${params})${signature.returnsText.length > 0 ? `: ${signature.returnsText}` : ''}`; - const signatureInfo = new vscode.SignatureInformation(label); - params.split(',').forEach(param => signatureInfo.parameters.push(new vscode.ParameterInformation(param.trim()))); - signatureInfo.activeParameter = paramIden.index; - - // Build the signature help - const signatureHelp = new vscode.SignatureHelp(); - signatureHelp.signatures.push(signatureInfo); - signatureHelp.activeSignature = 0; - - // Trigger autocomplete suggestions - invokeCompletionItems(dataTypeToMatchId(signatureInfo.parameters[paramIden.index].label.split(' ')[0]), document, position); - - return signatureHelp; -} - -function handleDynamicParams(paramIdentifier) { - if (paramIdentifier.dynamicCommand && paramIdentifier.identifier.signature.paramsText.length > 0) { - const name = paramIdentifier.identifier.name; - const command = get(paramIdentifier.dynamicCommand, matchType.COMMAND); - if (command && command.signature.paramsText.length > 0) { - let paramsText = `${command.signature.paramsText}, ${paramIdentifier.identifier.signature.paramsText}`; - paramsText = `${name}${paramsText.substring(paramsText.indexOf(','))}`; - return {index: paramIdentifier.index, identifier: {name: paramIdentifier.dynamicCommand, signature: {paramsText: paramsText, returnsText: ''}}}; - } - } - return paramIdentifier; -} - -function displayMessage(message) { - const signatureInfo = new vscode.SignatureInformation(message); - const signatureHelp = new vscode.SignatureHelp(); - signatureHelp.signatures.push(signatureInfo); - signatureHelp.activeSignature = 0; - return signatureHelp; -} - -function invokeCompletionItems(matchTypeId, document, position) { - activeCursorCache.set(matchTypeId, document, position); - if (matchTypeId !== matchType.UNKNOWN.id) { - const word = document.getText(document.getWordRangeAtPosition(position)); - if (matchType.TRIGGER.id === matchTypeId && runescriptTrigger[word]) { - return; - } - if (contains(word, matchTypeId)) { - return; - } - vscode.commands.executeCommand('editor.action.triggerSuggest'); - } -} - -module.exports = { provider, metadata }; diff --git a/client/provider/vscodeCommands.js b/client/provider/vscodeCommands.js deleted file mode 100644 index ec39cb1..0000000 --- a/client/provider/vscodeCommands.js +++ /dev/null @@ -1,17 +0,0 @@ -const vscode = require('vscode'); -const cacheManager = require('../cache/cacheManager'); - -const commands = { - rebuildCache: { - id: 'RuneScriptLanguage.rebuildCache', - command: () => { - vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: "Runescript Extension: Building cache / Indexing files...", - cancellable: false - }, cacheManager.rebuildAll); - } - } -}; - -module.exports = commands; diff --git a/client/resource/configKeys.js b/client/resource/configKeys.js deleted file mode 100644 index 1b7200d..0000000 --- a/client/resource/configKeys.js +++ /dev/null @@ -1,44 +0,0 @@ -// === STATIC CONFIG KEY MATCHES === -const configKeys = { - walkanim: { params: [param('seq'), param('seq'), param('seq'), param('seq')] }, - multivar: { params: [param('var')] }, - multiloc: { params: [param('int'), param('loc')] }, - multinpc: { params: [param('int'), param('npc')] }, - basevar: { params: [param('var')] }, - - category: { params: [param('category')] }, - huntmode: { params: [param('hunt')] }, - table: { params: [param('dbtable')] }, - column: { params: [param('dbcolumn', true)] }, -} - -// === REGEX CONFIG KEY MATCHES === -const regexConfigKeys = groupByFileType([ - { regex: /stock\d+/, params: [param('obj'), param('int'), param('int')], fileTypes: ["inv"] }, - { regex: /count\d+/, params: [param('obj'), param('int')], fileTypes: ["obj"] }, - { regex: /(model|head|womanwear|manwear|womanhead|manhead|activemodel)\d*/, params: [param('ob2')], fileTypes:['npc', 'loc', 'obj', 'spotanim', 'if', 'idk'] }, - { regex: /\w*anim\w*/, params: [param('seq')], fileTypes: ["loc", "npc", "if", "spotanim"] }, - { regex: /replaceheldleft|replaceheldright/, params: [param('obj')], fileTypes: ["seq"] }, -]); - -// === CONFIG KEYS THAT ARE HANDLED MANUALLY IN CONFIG_MATCHER === -const specialCaseKeys = ['val', 'param', 'data']; - -function param(type, declaration = false) { - return {typeId: type, declaration: declaration}; -} - -function groupByFileType(config) { - const result = new Map(); - for (const { regex, params, fileTypes } of config) { - for (const fileType of fileTypes) { - if (!result.has(fileType)) { - result.set(fileType, []); - } - result.get(fileType).push({ regex, params }); - } - } - return result; -} - -module.exports = { configKeys, regexConfigKeys, specialCaseKeys }; diff --git a/client/resource/dataTypeToMatchId.js b/client/resource/dataTypeToMatchId.js deleted file mode 100644 index ecc8427..0000000 --- a/client/resource/dataTypeToMatchId.js +++ /dev/null @@ -1,15 +0,0 @@ -const matchType = require("../matching/matchType"); - -const keywordToId = {}; - -Object.keys(matchType).forEach(matchTypeId => { - for (let keyword of (matchType[matchTypeId].types || [])) { - keywordToId[keyword] = matchTypeId; - } -}); - -function dataTypeToMatchId(keyword) { - return keywordToId[keyword] || matchType.UNKNOWN.id; -} - -module.exports = dataTypeToMatchId; diff --git a/client/resource/hoverConfigResolver.js b/client/resource/hoverConfigResolver.js deleted file mode 100644 index 02f9291..0000000 --- a/client/resource/hoverConfigResolver.js +++ /dev/null @@ -1,22 +0,0 @@ -const { DECLARATION_HOVER_ITEMS, REFERENCE_HOVER_ITEMS, LANGUAGE, BLOCK_SKIP_LINES, CONFIG_INCLUSIONS } = require("../enum/hoverConfigOptions"); - -const resolve = function(opt, match) { - const config = (!match.hoverConfig) ? {} : match.hoverConfig; - switch(opt) { - case DECLARATION_HOVER_ITEMS: return config[opt] || []; - case REFERENCE_HOVER_ITEMS: return config[opt] || []; - case LANGUAGE: return config[opt] || 'runescript'; - case BLOCK_SKIP_LINES: return (config[opt] !== undefined) ? match.hoverConfig[opt] : 1; - case CONFIG_INCLUSIONS: return config[opt] || null; - } -} - -const resolveAllHoverItems = function(match) { - const config = (!match.hoverConfig) ? {} : match.hoverConfig; - const displayItems = new Set(); - (config[DECLARATION_HOVER_ITEMS] || []).forEach(item => displayItems.add(item)); - (config[REFERENCE_HOVER_ITEMS] || []).forEach(item => displayItems.add(item)); - return displayItems; -} - -module.exports = { resolve, resolveAllHoverItems }; diff --git a/client/resource/identifierFactory.js b/client/resource/identifierFactory.js deleted file mode 100644 index 929a3ad..0000000 --- a/client/resource/identifierFactory.js +++ /dev/null @@ -1,135 +0,0 @@ -const dataTypeToMatchId = require('./dataTypeToMatchId'); -const hoverConfigResolver = require('./hoverConfigResolver'); -const { SIGNATURE, CODEBLOCK } = require('../enum/hoverDisplayItems'); -const { END_OF_BLOCK_LINE } = require('../enum/regex'); -const { LANGUAGE, BLOCK_SKIP_LINES, CONFIG_INCLUSIONS } = require('../enum/hoverConfigOptions'); -const matchType = require('../matching/matchType'); - -/** - * Builds an identifier object - * identifier = { - * name: String, - * matchId: matchTypeId, - * declaration: vscode.Location - * references: {filePath1: String[], filePath2: String[], ...} (String is encoded location value) - * fileType: String, - * language: String, - * info?: String, - * signature?: {params: {type: String, name: String, matchTypeId: String}[], returns: String, paramsText: String}, - * block?: String - * } - */ -function build(name, match, location, info = null, text = {lines: [], start: 0}) { - const identifier = { - name: name, - match: match, - declaration: location, - references: {}, - fileType: location ? location.uri.fsPath.split(/[#?]/)[0].split('.').pop().trim() : 'rs2', - language: hoverConfigResolver.resolve(LANGUAGE, match), - text: text - } - if (info) identifier.info = info; - addExtraData(identifier, match.extraData); - process(identifier); - cleanup(identifier); - return identifier; -} - -function buildRef(name, match) { - const identifier = { - name: name, - match: match, - references: {}, - fileType: (match.fileTypes || [])[0] || 'rs2', - language: hoverConfigResolver.resolve(LANGUAGE, match), - } - if (match.referenceOnly) { - addExtraData(identifier, match.extraData); - process(identifier); - } - cleanup(identifier); - return identifier; -} - -function process(identifier) { - // Process specififed display items - if (identifier.text) { - const hoverDisplayItems = hoverConfigResolver.resolveAllHoverItems(identifier.match); - for (const hoverDisplayItem of hoverDisplayItems) { - switch(hoverDisplayItem) { - case SIGNATURE: processSignature(identifier); break; - case CODEBLOCK: processCodeBlock(identifier); break; - } - } - } - - // Execute custom post processing for the identifier's matchType (if defined) - if (identifier.match.postProcessor) { - identifier.match.postProcessor(identifier); - } -} - -function cleanup(identifier) { - identifier.matchId = identifier.match.id; - delete identifier.match; - delete identifier.text; -} - -function processSignature(identifier) { - // Get first line of text, which should contain the data for parsing the signature - let line = identifier.text.lines[identifier.text.start]; - - // Parse input params - const params = []; - let openingIndex = line.indexOf('('); - let closingIndex = line.indexOf(')'); - if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { - line.substring(openingIndex, closingIndex).split(',').forEach(param => { - if (param.startsWith(' ')) param = param.substring(1); - const split = param.split(' '); - if (split.length === 2) { - params.push({type: split[0], name: split[1], matchTypeId: dataTypeToMatchId(split[0])}); - } - }); - } - - // Parse response type - let returns = []; - let returnsText = ''; - line = line.substring(closingIndex + 1); - openingIndex = line.indexOf('('); - closingIndex = line.indexOf(')'); - if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { - returnsText = line.substring(openingIndex, closingIndex); - returns = line.substring(openingIndex, closingIndex).split(',').map(item => dataTypeToMatchId(item.trim())); - } - - // Add signature to identifier - const paramsText = (params.length > 0) ? params.map(param => `${param.type} ${param.name}`).join(', ') : ''; - identifier.signature = {params: params, returns: returns, paramsText: paramsText, returnsText: returnsText}; -} - -function processCodeBlock(identifier) { - const lines = identifier.text.lines; - const startIndex = identifier.text.start + hoverConfigResolver.resolve(BLOCK_SKIP_LINES, identifier.match); - const configInclusionTags = hoverConfigResolver.resolve(CONFIG_INCLUSIONS, identifier.match); - let blockInclusionLines = []; - if (identifier.match.id === matchType.CONSTANT.id) blockInclusionLines.push(lines[startIndex]); - for (let i = startIndex; i < lines.length; i++) { - let currentLine = lines[i]; - if (END_OF_BLOCK_LINE.test(currentLine)) break; - if (currentLine.startsWith('//')) continue; - if (configInclusionTags && !configInclusionTags.some(inclusionTag => currentLine.startsWith(inclusionTag))) continue; - blockInclusionLines.push(currentLine); - } - identifier.block = blockInclusionLines.join('\n'); -} - -function addExtraData(identifier, extraData) { - if (!extraData) return; - if (!identifier.extraData) identifier.extraData = {}; - Object.keys(extraData).forEach(key => identifier.extraData[key] = extraData[key]); -} - -module.exports = { build, buildRef }; diff --git a/client/resource/postProcessors.js b/client/resource/postProcessors.js deleted file mode 100644 index 195434e..0000000 --- a/client/resource/postProcessors.js +++ /dev/null @@ -1,80 +0,0 @@ -const { END_OF_LINE } = require('../enum/regex'); -const matchConfigKeyInfo = require('../info/configKeyInfo'); -const matchTriggerInfo = require('../info/triggerInfo'); -const { getLineText } = require('../utils/stringUtils'); - -// Post processors are used for any additional post modification needed for a matchType, after an identifier has been built -// postProcessors must be a function which takes indentifier as an input, and directly modifies that identifier as necessary - -const coordPostProcessor = function(identifier) { - const coordinates = identifier.name.split('_'); - const xCoord = Number(coordinates[1] << 6) + Number(coordinates[3]); - const zCoord = Number(coordinates[2] << 6) + Number(coordinates[4]); - identifier.value = `Absolute coordinates: (${xCoord}, ${zCoord})`; -} - -const enumPostProcessor = function(identifier) { - const inputtypeLine = getLineText(identifier.block.substring(identifier.block.indexOf("inputtype="))); - const outputtypeLine = getLineText(identifier.block.substring(identifier.block.indexOf("outputtype="))); - identifier.extraData = {inputType: inputtypeLine.substring(10), outputType: outputtypeLine.substring(11)}; -} - -const dataTypePostProcessor = function(identifier) { - const index = identifier.block.indexOf("type="); - const dataType = (index < 0) ? 'int' : getLineText(identifier.block.substring(index)).substring(5); - identifier.extraData = {dataType: dataType}; -} - -const configKeyPostProcessor = function(identifier) { - const info = matchConfigKeyInfo(identifier.name, identifier.fileType); - info ? identifier.info = info.replaceAll('$TYPE', identifier.fileType) : identifier.hideDisplay = true; -} - -const triggerPostProcessor = function(identifier) { - const info = matchTriggerInfo(identifier.name, identifier.extraData.triggerName); - if (info) identifier.info = info; -} - -const categoryPostProcessor = function(identifier) { - const extraData = identifier.extraData; - if (extraData && extraData.matchId && extraData.categoryName) { - identifier.value = `This script applies to all ${identifier.extraData.matchId} with \`category=${identifier.extraData.categoryName}\``; - } -} - -const componentPostProcessor = function(identifier) { - const split = identifier.name.split(':'); - identifier.info = `A component of the ${split[0]} interface`; - identifier.name = split[1]; -} - -const rowPostProcessor = function(identifier) { - if (identifier.block) { - const tableName = (identifier.block.split('=') || ['', ''])[1]; - identifier.info = `A row in the ${tableName} table`; - delete identifier.block; - identifier.extraData = {table: tableName}; - } -} - -const columnPostProcessor = function(identifier) { - const split = identifier.name.split(':'); - identifier.info = `A column of the ${split[0]} table`; - identifier.name = split[1]; - - const exec = END_OF_LINE.exec(identifier.block); - if (!exec) return; - const types = identifier.block.substring(8 + identifier.name.length, exec.index).split(','); - identifier.extraData = {dataTypes: types}; - identifier.block = `Field types: ${types.join(', ')}`; -} - -const fileNamePostProcessor = function(identifier) { - identifier.info = `Refers to the file ${identifier.name}.${identifier.fileType}`; -} - -module.exports = { - coordPostProcessor, enumPostProcessor, dataTypePostProcessor, configKeyPostProcessor, - triggerPostProcessor, categoryPostProcessor, componentPostProcessor, columnPostProcessor, - fileNamePostProcessor, rowPostProcessor -}; diff --git a/client/resource/triggers.js b/client/resource/triggers.js deleted file mode 100644 index 1d72ad2..0000000 --- a/client/resource/triggers.js +++ /dev/null @@ -1,61 +0,0 @@ -const matchType = require("../matching/matchType"); - -const runescriptTrigger = { - proc: build(matchType.PROC, true), - label: build(matchType.LABEL, true), - queue: build(matchType.QUEUE, true), - softtimer: build(matchType.SOFTTIMER, true), - timer: build(matchType.TIMER, true), - ai_timer: build(matchType.NPC, false), - if_button: build(matchType.COMPONENT, false), - if_close: build(matchType.COMPONENT, false), - walktrigger: build(matchType.WALKTRIGGER, true), - ai_walktrigger: build(matchType.NPC, false), - debugproc: build(matchType.UNKNOWN, true), - login: build(matchType.UNKNOWN, true), - logout: build(matchType.UNKNOWN, true), - tutorial: build(matchType.UNKNOWN, true), - advancestat: build(matchType.STAT, false), - mapzone: build(matchType.UNKNOWN, true), - mapzoneexit: build(matchType.UNKNOWN, true), - zone: build(matchType.UNKNOWN, true), - zoneexit: build(matchType.UNKNOWN, true), - command: build(matchType.COMMAND, true) -} - -const configDuplicates = [ - {startsWith: 'opnpc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'apnpc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'ai_apnpc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'ai_opnpc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'opobj', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.OBJ}, - {startsWith: 'apobj', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.OBJ}, - {startsWith: 'ai_apobj', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.OBJ}, - {startsWith: 'ai_opobj', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.OBJ}, - {startsWith: 'oploc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.LOC}, - {startsWith: 'aploc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.LOC}, - {startsWith: 'ai_aploc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.LOC}, - {startsWith: 'ai_oploc', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.LOC}, - {startsWith: 'opplayer', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.UNKNOWN}, - {startsWith: 'applayer', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.UNKNOWN}, - {startsWith: 'ai_applayer', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'ai_opplayer', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'ai_queue', upToNum: 20, includeU: false, includeT: false, includeD: false, defaultMatch: matchType.NPC}, - {startsWith: 'opheld', upToNum: 5, includeU: true, includeT: true, includeD: false, defaultMatch: matchType.OBJ}, - {startsWith: 'inv_button', upToNum: 5, includeU: false, includeT: false, includeD: true, defaultMatch: matchType.COMPONENT}, -]; - -configDuplicates.forEach(dupeDef => { - for (let i = 1; i <= dupeDef.upToNum; i++) { - runescriptTrigger[`${dupeDef.startsWith}${i}`] = build(dupeDef.defaultMatch, false); - } - if (dupeDef.includeU) runescriptTrigger[`${dupeDef.startsWith}u`] = build(dupeDef.defaultMatch, false); - if (dupeDef.includeT) runescriptTrigger[`${dupeDef.startsWith}t`] = build(matchType.COMPONENT, false); - if (dupeDef.includeD) runescriptTrigger[`${dupeDef.startsWith}d`] = build(dupeDef.defaultMatch, false); -}); - -function build(match, declaration) { - return {match: match, declaration: declaration}; -} - -module.exports = runescriptTrigger; diff --git a/client/runescript-language.js b/client/runescript-language.js deleted file mode 100644 index 85d9034..0000000 --- a/client/runescript-language.js +++ /dev/null @@ -1,61 +0,0 @@ -const vscode = require('vscode'); -const hoverProvider = require('./provider/hoverProvider'); -const recolorProvider = require('./provider/recolorProvider'); -const definitionProvider = require('./provider/gotoDefinition'); -const referenceProvider = require('./provider/referenceProvider'); -const renameProvider = require('./provider/renameProvider'); -const cacheManager = require('./cache/cacheManager'); -const commands = require('./provider/vscodeCommands'); -const signatureHelp = require('./provider/signatureHelpProvider'); -const configHelp = require('./provider/configHelpProvider'); -const completionProvider = require('./provider/completionProvider'); -const color24Provider = require('./provider/color24Provider.js'); - -const languages = ['runescript','locconfig','objconfig','npcconfig','dbtableconfig','dbrowconfig','paramconfig','structconfig','enumconfig','varpconfig','varbitconfig','varnconfig','varsconfig','invconfig','seqconfig','spotanimconfig','mesanimconfig','idkconfig','huntconfig','constants','interface','pack','floconfig']; - -function activate(context) { - // Register commands created by this extension - Object.keys(commands).forEach(key => - context.subscriptions.push(vscode.commands.registerCommand(commands[key].id, commands[key].command))); - - // Populate cache on extension activation - vscode.commands.executeCommand(commands.rebuildCache.id); - - // Cache processing event handlers for git branch changes, updating files, create/rename/delete files - vscode.workspace.createFileSystemWatcher('**/.git/HEAD').onDidCreate(() => vscode.commands.executeCommand(commands.rebuildCache.id)); - vscode.workspace.onDidSaveTextDocument(saveDocumentEvent => cacheManager.rebuildFile(saveDocumentEvent.uri)); - vscode.workspace.onDidChangeTextDocument(() => cacheManager.rebuildActiveFile()); - vscode.window.onDidChangeActiveTextEditor(() => cacheManager.rebuildActiveFile()); - vscode.workspace.onDidDeleteFiles(filesDeletedEvent => cacheManager.clearFiles(filesDeletedEvent.files)); - vscode.workspace.onDidRenameFiles(filesRenamedEvent => cacheManager.renameFiles(filesRenamedEvent.files)); - vscode.workspace.onDidCreateFiles(filesCreatedEvent => cacheManager.createFiles(filesCreatedEvent.files)); - - // Register providers (hover, rename, recolor, definition, reference) - for (const language of languages) { - vscode.languages.registerHoverProvider(language, hoverProvider(context)); - vscode.languages.registerRenameProvider(language, renameProvider); - vscode.languages.registerCompletionItemProvider(language, completionProvider.provider, ...completionProvider.triggers); - context.subscriptions.push(vscode.languages.registerDefinitionProvider(language, definitionProvider)); - context.subscriptions.push(vscode.languages.registerReferenceProvider(language, referenceProvider)); - - if (language === 'floconfig' || language === 'interface') { - vscode.languages.registerColorProvider(language, color24Provider); - } else if (language.endsWith('config')) { - vscode.languages.registerColorProvider(language, recolorProvider); - } - - if (language.endsWith('config') || language === 'interface') { - vscode.languages.registerSignatureHelpProvider(language, configHelp.provider, configHelp.metadata); - } - } - vscode.languages.registerSignatureHelpProvider('runescript', signatureHelp.provider, signatureHelp.metadata); -} - -function deactivate() { - cacheManager.clearAll(); - } - -module.exports = { - activate, - deactivate -}; diff --git a/client/utils/cacheUtils.js b/client/utils/cacheUtils.js deleted file mode 100644 index 026c1f9..0000000 --- a/client/utils/cacheUtils.js +++ /dev/null @@ -1,29 +0,0 @@ -const vscode = require('vscode'); - -function resolveKey(name, match) { - return (!name || !match) ? null : name + match.id; -} - -function resolveFileKey(uri) { - return (uri) ? uri.fsPath : null; -} - -function encodeReference(line, index) { - return `${line}|${index}`; -} - -function decodeReferenceToLocation(uri, encodedValue) { - const split = encodedValue.split('|'); - return (split.length !== 2) ? null : new vscode.Location(uri, new vscode.Position(Number(split[0]), Number(split[1]))); -} - -function decodeReferenceToRange(wordLength, encodedValue) { - const split = encodedValue.split('|'); - if (split.length !== 2) { - return null; - } - const startPosition = new vscode.Position(Number(split[0]), Number(split[1])); - return new vscode.Range(startPosition, startPosition.translate(0, wordLength)); -} - -module.exports = { resolveKey, resolveFileKey, encodeReference, decodeReferenceToLocation, decodeReferenceToRange }; diff --git a/client/utils/markdownUtils.js b/client/utils/markdownUtils.js deleted file mode 100644 index 97478e5..0000000 --- a/client/utils/markdownUtils.js +++ /dev/null @@ -1,66 +0,0 @@ -const vscode = require('vscode'); -const path = require('path'); -const { INFO, VALUE, SIGNATURE, CODEBLOCK } = require('../enum/hoverDisplayItems'); -const { GLOBAL_VAR } = require('../matching/matchType'); - -function markdownBase(extensionContext) { - const markdown = new vscode.MarkdownString(); - markdown.supportHtml = true; - markdown.isTrusted = true; - markdown.supportThemeIcons = true; - markdown.baseUri = vscode.Uri.file(path.join(extensionContext.extensionPath, 'icons', path.sep)); - return markdown; -} - -function expectedIdentifierMessage(word, match, markdown) { - markdown.appendMarkdown(`${match.id}${word} not found`); -} - -function appendTitle(name, type, matchId, markdown, id, isCert) { - if (isCert && id) { - name = `${name} (cert) [${Number(id) + 1}]`; - } else if (id) { - name = `${name} [${id}]`; - } - //   - if (matchId === GLOBAL_VAR.id) { - markdown.appendMarkdown(`${type.toUpperCase()} ${name}`); - } else { - markdown.appendMarkdown(`${matchId} ${name}`); - } -} - -function appendInfo(identifier, displayItems, markdown) { - if (displayItems.includes(INFO) && identifier.info) { - appendBody(`${identifier.info}`, markdown); - } -} - -function appendValue(identifier, displayItems, markdown) { - if (displayItems.includes(VALUE) && identifier.value) { - appendBody(`${identifier.value}`, markdown); - } -} - -function appendSignature(identifier, displayItems, markdown) { - if (displayItems.includes(SIGNATURE) && identifier.signature) { - if (identifier.signature.paramsText.length > 0) markdown.appendCodeblock(`params: ${identifier.signature.paramsText}`, identifier.language); - if (identifier.signature.returnsText.length > 0) markdown.appendCodeblock(`returns: ${identifier.signature.returnsText}`, identifier.language); - } -} - -function appendCodeBlock(identifier, displayItems, markdown) { - if (displayItems.includes(CODEBLOCK) && identifier.block) { - markdown.appendCodeblock(identifier.block, identifier.language); - } -} - -function appendBody(text, markdown) { - if (!markdown.value.includes('---')) { - markdown.appendMarkdown('\n\n---'); - } - markdown.appendMarkdown(`\n\n${text}`); -} - -module.exports = { markdownBase, expectedIdentifierMessage, appendTitle, appendInfo, appendValue, - appendSignature, appendCodeBlock, appendBody }; diff --git a/client/utils/matchUtils.js b/client/utils/matchUtils.js deleted file mode 100644 index 7834dc7..0000000 --- a/client/utils/matchUtils.js +++ /dev/null @@ -1,54 +0,0 @@ -const { WORD_PATTERN } = require("../enum/regex"); - -function getWords(lineText, wordPattern=WORD_PATTERN) { - return [ ...lineText.matchAll(wordPattern) ].map((wordMatch, index) => { - return { value: wordMatch[0], start: wordMatch.index, end: wordMatch.index + wordMatch[0].length - 1, index: index} - }); -} - -function getWordAtIndex(words, index) { - if (words.length < 1) return null; - let prev; - for (let i = words.length - 1; i >= 0; i--) { - if (index <= words[i].end) prev = words[i]; - else break; - } - return (prev && prev.start <= index && prev.end >= index) ? prev : null -} - -function expandCsvKeyObject(obj) { - let keys = Object.keys(obj); - for (let i = 0; i < keys.length; ++i) { - let key = keys[i]; - let subkeys = key.split(/,\s?/); - let target = obj[key]; - delete obj[key]; - subkeys.forEach(k => obj[k] = target); - } - return obj; -} - -/** - * Context items shared by both matchWord and matchWords - */ -function getBaseContext(lineText, lineNum, uri) { - lineText = lineText.split('//')[0]; // Ignore anything after a comment - const words = getWords(lineText); - const fileSplit = uri.fsPath.split('\\').pop().split('/').pop().split('.'); - return { - words: words, - uri: uri, - line: {text: lineText, number: lineNum}, - file: {name: fileSplit[0], type: fileSplit[1]}, - } -} - -function reference(type, extraData) { - return (extraData) ? { ...type, extraData: extraData, declaration: false } : { ...type, declaration: false }; -} - -function declaration(type, extraData) { - return (extraData) ? { ...type, extraData: extraData, declaration: true } : { ...type, declaration: true }; -} - -module.exports = { getWords, getWordAtIndex, getBaseContext, expandCsvKeyObject, reference, declaration }; diff --git a/client/utils/stringUtils.js b/client/utils/stringUtils.js deleted file mode 100644 index ea5d577..0000000 --- a/client/utils/stringUtils.js +++ /dev/null @@ -1,49 +0,0 @@ -const vscode = require('vscode'); -const { END_OF_LINE, END_OF_BLOCK } = require('../enum/regex'); - -const getLineText = function(input) { - const endOfLine = END_OF_LINE.exec(input); - return !endOfLine ? input : input.substring(0, endOfLine.index); -} - -const getLines = function(input) { - return input.split(END_OF_LINE); -} - -const skipFirstLine = function(input) { - const endOfLine = END_OF_LINE.exec(input); - return !endOfLine ? input : input.substring(endOfLine.index + 1); -} - -const getBlockText = function(input) { - const endOfBlock = END_OF_BLOCK.exec(input); - return !endOfBlock ? input : input.substring(0, endOfBlock.index); -} - -const nthIndexOf = function(input, pattern, n) { - let i = -1; - while (n-- > 0 && i++ < input.length) { - i = input.indexOf(pattern, i); - if (i < 0) break; - } - return i; -} - -const truncateMatchingParenthesis = function(str) { - let truncateIndex = 0; - let count = 0; - for (let i = 0; i < str.length; i++) { - if (str.charAt(i) === '(') count++; - if (str.charAt(i) === ')' && --count === 0) truncateIndex = i; - } - return (truncateIndex > 0) ? str.substring(truncateIndex + 1) : str; -} - -function createSearchableString(linkableText, query, filesToInclude, isRegex=false) { - const searchOptions = JSON.stringify({ query: query, filesToInclude: filesToInclude, isRegex: isRegex}); - return `[${linkableText}](${vscode.Uri.parse(`command:workbench.action.findInFiles?${encodeURIComponent(searchOptions)}`)})`; -} - -module.exports = { - getLineText, getLines, skipFirstLine, getBlockText, nthIndexOf, truncateMatchingParenthesis, createSearchableString -}; diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..f70849c --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,54 @@ +const tsParser = require("@typescript-eslint/parser"); +const tsPlugin = require("@typescript-eslint/eslint-plugin"); +const importPlugin = require("eslint-plugin-import"); + +module.exports = [ + { + files: ["src/**/*.ts"], + languageOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: "module", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + import: importPlugin, + }, + rules: { + "import/no-default-export": "error", + "import/no-namespace": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports", + "fixStyle": "separate-type-imports" + } + ], + "@typescript-eslint/no-floating-promises": "error", + "eqeqeq": "error", + "no-restricted-syntax": [ + "error", + { + "selector": "ImportDefaultSpecifier", + "message": "Default imports are not allowed." + }, + { + "selector": "ImportNamespaceSpecifier", + "message": "Namespace imports are not allowed." + } + ], + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 6242117..a27733e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,3663 @@ { "name": "runescriptlanguage", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runescriptlanguage", + "version": "0.3.0", + "dependencies": { + "object-sizeof": "^2.6.5" + }, + "devDependencies": { + "@types/node": "^25.0.8", + "@types/vscode": "^1.108.1", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^9.39.2", + "eslint-plugin-import": "^2.32.0", + "rimraf": "^6.0.1", + "typescript": "^5.9.3" + }, + "engines": { + "vscode": "^1.108.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.108.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz", + "integrity": "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", + "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/type-utils": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", + "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-sizeof": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/object-sizeof/-/object-sizeof-2.6.5.tgz", + "integrity": "sha512-Mu3udRqIsKpneKjIEJ2U/s1KmEgpl+N6cEX1o+dDl2aZ+VW5piHqNgomqAk5YMsDoSkpcA8HnIKx1eqGTKzdfw==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", "engines": { - "vscode": "^1.75.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index 0637b21..04327f4 100644 --- a/package.json +++ b/package.json @@ -4,18 +4,27 @@ "publisher": "2004scape", "repository": "https://github.com/LostCityRS/RuneScriptLanguage", "description": "Syntax highlighting for RuneScript.", - "version": "0.2.4", + "version": "0.3.0", "icon": "icons/icon-min.png", "engines": { - "vscode": "^1.75.0" + "vscode": "^1.108.0" }, "categories": [ "Programming Languages", "Snippets" ], - "main": "./client/runescript-language.js", - "activationEvents": [ - ], + "main": "./out/runescriptExtension.js", + "activationEvents": [], + "scripts": { + "clean": "rimraf out", + "compile": "npm run clean && tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile", + "lint": "eslint \"src/**/*.ts\"", + "vscode:prepublish": "npm run lint && npm run compile", + "publish": "npx vsce publish", + "package": "npx vsce package" + }, "contributes": { "languages": [ { @@ -539,11 +548,44 @@ "path": "./snippets/snippets.json" } ], + "configuration": { + "title": "Runescript", + "properties": { + "runescript.hover.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable displaying hover information on mouseover." + }, + "runescript.diagnostics.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable diagnostics from the RuneScript extension." + }, + "runescript.devMode.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable developer mode: highlight all matches, show detailed hover info, write data to the runescript output channel." + } + } + }, "commands": [ { "command": "RuneScriptLanguage.rebuildCache", "title": "Runescript: Rebuild Workspace Cache" } ] + }, + "devDependencies": { + "@types/node": "^25.0.8", + "@types/vscode": "^1.108.1", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^9.39.2", + "eslint-plugin-import": "^2.32.0", + "rimraf": "^6.0.1", + "typescript": "^5.9.3" + }, + "dependencies": { + "object-sizeof": "^2.6.5" } } diff --git a/src/cache/activeFileCache.ts b/src/cache/activeFileCache.ts new file mode 100644 index 0000000..cbcb3e2 --- /dev/null +++ b/src/cache/activeFileCache.ts @@ -0,0 +1,475 @@ +import type { Position, TextDocument, Uri } from "vscode"; +import type { DataRange, Identifier, IdentifierText, Item, MatchResult, MatchType, OperatorToken, ParsedFile, ParsedWord, TextRange } from "../types"; +import { get as getIdentifier } from "./identifierCache"; +import { LOCAL_VAR, QUEUE, SKIP, KEYWORD, TRIGGER, UNKNOWN } from "../matching/matchType"; +import { decodeReferenceToLocation, resolveFileKey, resolveKeyFromIdentifier } from "../utils/cacheUtils"; +import { addReference, buildFromDeclaration } from "../resource/identifierFactory"; +import { findMatchInRange } from "../utils/matchUtils"; +import { LineReferenceCache } from "./class/LineReferenceCache"; +import { CONFIG_DECLARATION_REGEX, QUEUE_REGEX } from "../enum/regex"; +import { dataTypeToMatchType } from "../resource/dataTypeToMatchId"; +import { isDevMode, logWarning } from "../core/devMode"; + +/* A cache which holds info about the last processed file, typically the actively open file */ + +/** + * Tracks the file that the cache has been built for, used to make sure you're getting the data you expect + */ +let file: string = ''; + +// ===== CACHE DATA ===== // + +/** + * File matches, keyed by line number + * The value is an array of a data range containing a match result + * The data range is the index range on that line of the word the match is for + */ +const fileMatches = new Map[]>(); + +/** + * File parsed words, keyed by line number + * The value is an array of parsed words on that line + */ +let parsedWords: Map = new Map(); + +/** + * File parsed operator tokens, keyed by line number + * The value is an array of parsed operator tokens on that line + */ +let operatorTokens: Map = new Map(); + +/** + * File parsed string ranges, keyed by line number + * The value is an array of string ranges on that line + */ +let stringRanges: Map = new Map(); + +/** + * File parsed interpolation ranges, keyed by line number + * The value is an array of interpolation ranges on that line + */ +let interpolationRanges: Map = new Map(); + +// ===== GET DATA ===== // + +/** + * Returns an item at the given position in the given document, if it exists + * @param document The document to get items for + * @param position The position (line num + index) to get the item for + * @returns The item at that positon, if exists + */ +export function getByDocPosition(document: TextDocument, position: Position): Item | undefined { + return get(document.uri, position.line, position.character); +} + +/** + * Returns an item at the given index of a given line in the file uri, if it exists + * @param uri The uri of the file to check + * @param lineNum The line number to search + * @param lineIndex The index within the line to find the match for + * @returns The item on that line at that index, if exists + */ +export function getByLineIndex(uri: Uri, lineNum: number, lineIndex: number): Item | undefined { + return get(uri, lineNum, lineIndex); +} + +/** + * Returns a parsed word at the given position in the given document, if it exists + * @param document The document to get word for + * @param position The position (line num + index) to get the word for + * @returns The word at that positon, if exists + */ +export function getParsedWordByDocPosition(position: Position): ParsedWord | undefined { + const lineWords = parsedWords.get(position.line); + if (lineWords) { + return findMatchInRange(position.character, lineWords.map(word => ({start: word.start, end: word.end, data: word})))?.data; + } +} + +/** + * Returns a parsed word at the given position in the given document, if it exists + * @param document The document to get word for + * @param position The position (line num + index) to get the word for + * @returns The word at that positon, if exists + */ +export function getOperatorByDocPosition(position: Position): OperatorToken | undefined { + const lineOperators = operatorTokens.get(position.line); + if (lineOperators) { + return findMatchInRange(position.character, lineOperators.map(operator => ({start: operator.index, end: operator.index + operator.token.length, data: operator})))?.data; + } +} + +/** + * Returns a string range at the given position in the document, if it exists + * @param position The position (line num + index) to get the string range for + * @returns The string range at that position, if exists + */ +export function getStringRangeByDocPosition(position: Position): TextRange | undefined { + const lineStrings = stringRanges.get(position.line); + if (lineStrings) { + return findMatchInRange(position.character, lineStrings.map(range => ({ start: range.start, end: range.end, data: range })))?.data; + } +} + +/** + * Returns an interpolation range at the given position in the document, if it exists + * @param position The position (line num + index) to get the interpolation range for + * @returns The interpolation range at that position, if exists + */ +export function getInterpolationRangeByDocPosition(position: Position): TextRange | undefined { + const lineRanges = interpolationRanges.get(position.line); + if (lineRanges) { + return findMatchInRange(position.character, lineRanges.map(range => ({ start: range.start, end: range.end, data: range })))?.data; + } +} + +/** + * Returns a call function's match result + * @param lineNum Line number to start on (will check previous lines if not on this line) + * @param callName The call function name we are looking for + * @param callerIndex The index of the word of the call function name + */ +export function getCallIdentifier(uri: Uri, lineNum: number, callName: string, callNameIndex: number): Identifier | undefined { + for (let curLine = lineNum; curLine >= Math.max(0, lineNum - 10); curLine--) { + const lineParsedWords = parsedWords.get(curLine); + const potentialCallWord = lineParsedWords?.[callNameIndex]; + if (potentialCallWord?.value === callName) { + const item = get(uri, curLine, potentialCallWord.start); + if (!item?.identifier || item.context.matchType.id === LOCAL_VAR.id) { + if (QUEUE_REGEX.test(potentialCallWord.callName ?? '')) { + const queueName = lineParsedWords?.[(potentialCallWord.callNameIndex ?? -2) + 1]; + if (!queueName) return; + return getIdentifier(queueName.value, QUEUE); + } + } + return item?.identifier; + } + } +} + +export function getLeftHandSide(): Item | undefined { + return undefined; +} + +export function getRightHandSide(): Item | undefined { + return undefined; +} + +/** + * The core get item which does the searching of the caches to get an item on a line at that index + * @param uri Used to validate the uri is the same as the one the cache is using + * @param lineNum Line number the item is on + * @param index Position/Index the item is at within that line + * @returns The item at that position, if it exists + */ +function get(uri: Uri, lineNum: number, index: number): Item | undefined { + if (file !== uri.fsPath) { + return undefined; + } + const result = findMatchInRange(index, fileMatches.get(lineNum))?.data; + if (result) { + return buildItem(result); + } +} + +/** + * Builds the Item from the match result (Item = match result + identifier) + * @param result The match result to build the item for + */ +function buildItem(result: MatchResult): Item { + return { + word: result.word, + context: result.context, + identifier: result.context.matchType.id === LOCAL_VAR.id ? getLocalVar(result, result.context.line.number) : getIdentifier(result.word, result.context.matchType) + } +} + +/** + * Returns all match results in the active window + */ +export function getAllMatches(): MatchResult[] { + return Array.from(fileMatches.values()).flat().map(range => range.data); +} + +/** + * Returns the fsPath of the file the active cache currently represents. + */ +export function getActiveCacheFile(): string { + return file; +} + +/** + * Returns all of the matches and parsed words for the file + */ +export function getAllParsedWords(): Map { + return parsedWords; +} + +/** + * Returns all of the matches and parsed words for the file + */ +export function getAllOperatorTokens(): Map { + return operatorTokens; +} + +/** + * Returns all of the string ranges for the file + */ +export function getAllStringRanges(): Map { + return stringRanges; +} + +/** + * Returns all of the interpolation ranges for the file + */ +export function getAllInterpolationRanges(): Map { + return interpolationRanges; +} + +// ==== CACHE POPULATING FUNCTIONS ==== // + +/** + * Clears the cache and then initializes it for the new file + * @param uri The uri of the file being built + */ +export function init(uri: Uri, parsedFile: ParsedFile) { + fileMatches.clear(); + parsedWords = parsedFile.parsedWords; + operatorTokens = parsedFile.operatorTokens; + stringRanges = parsedFile.stringRanges; + interpolationRanges = parsedFile.interpolationRanges; + localVarCache.clear(); + codeBlockCache.clear(); + switchStmtCache.clear(); + file = uri.fsPath; + newCodeblockFlag = -1; +} + +/** + * Clear all of the data + */ +export function clear() { + fileMatches.clear(); + parsedWords = new Map(); + operatorTokens = new Map(); + stringRanges = new Map(); + interpolationRanges = new Map(); + localVarCache.clear(); + codeBlockCache.clear(); + switchStmtCache.clear(); + file = ''; + newCodeblockFlag = -1; +} + +/** + * Process a matched word into the active file cache. + * Builds additional context essential for some matches to be found. + * This method must be called on each match in a file sequentially left to right, and line to line. + * @param match the match found for the word + */ +export function processMatch(result: MatchResult): void { + const lineNum = result.context.line.number; + const lineItems = fileMatches.get(lineNum) ?? []; + lineItems.push({ start: result.context.word.start, end: result.context.word.end, data: result }); + cacheCodeBlock(result); + cacheSwitchStmt(result); + cacheLocalVariable(result); + fileMatches.set(lineNum, lineItems); +} + +/** + * Insert new matches into the active file cache, takes all matches for one line and inserts them in order + * @param results results to insert + */ +export function insertLineMatches(results: MatchResult[]): void { + if (results.length === 0) return; + const lineNum = results[0]!.context.line.number; + const lineResults = results.filter(r => r.context.line.number === lineNum); + if (isDevMode() && lineResults.length !== results.length) { + logWarning(`[activeFileCache] insertLineMatches expected all results to be on the same line, but got results spanning multiple lines`); + } + if (lineResults.length === 0) return; + const lineItems = fileMatches.get(lineNum) ?? []; + const additions = lineResults + .map(r => ({ start: r.context.word.start, end: r.context.word.end, data: r })) + .sort((a, b) => a.start - b.start); + + if (lineItems.length === 0) { + fileMatches.set(lineNum, additions); + return; + } + + // Merge sorted additions into the existing sorted lineItems. + const merged: typeof lineItems = []; + let i = 0; + let j = 0; + while (i < lineItems.length && j < additions.length) { + if (lineItems[i]!.start <= additions[j]!.start) { + merged.push(lineItems[i++]!); + } else { + merged.push(additions[j++]!); + } + } + while (i < lineItems.length) merged.push(lineItems[i++]!); + while (j < additions.length) merged.push(additions[j++]!); + + fileMatches.set(lineNum, merged); +} + + +// ==== Local Variable Stuff ==== // + +// key : codeBlock cache key | value : map of local variable identifiers keyed by variable name +const localVarCache = new Map>(); + +/** + * Given a local variable match, this builds the identifier for it and puts it into the local variables cache + * @param matchResult the match result of the local variable + * @param lineNum the line number the local variable is on + */ +function cacheLocalVariable(matchResult: MatchResult): void { + if (matchResult.context.matchType.id !== LOCAL_VAR.id) return; // exit if not a local var match + const lineNum = matchResult.context.line.number; + const blockIdentifier = getBlockScopeIdentifier(lineNum); + if (!blockIdentifier) return; + const blockKey = resolveKeyFromIdentifier(blockIdentifier); + + if (matchResult.context.declaration) { + // Add the new (declarations always come first) local variable to the cache + const localVarIden = createLocalVariableIdentifier(matchResult); + if (!localVarIden) return; + const blockVariables = localVarCache.get(blockKey) ?? new Map(); + blockVariables.set(localVarIden.name, localVarIden); + localVarCache.set(blockKey, blockVariables); + } else { + // Get the local variable identifier and add the reference to it + const blockVariables = localVarCache.get(blockKey); + if (!blockVariables) return; + const localVarIden = blockVariables.get(matchResult.word); + if (!localVarIden) return; + const fileKey = resolveFileKey(matchResult.context.uri); + if (!fileKey) return; + const refs = addReference(localVarIden, fileKey, lineNum, matchResult.context.word.start, matchResult.context.word.end); + localVarIden.references[fileKey] = refs; + } +} + +/** + * Creates the Identifier object for the given local variable match result + * @param matchResult the match result for the local variable + * @returns the built identifier + */ +function createLocalVariableIdentifier(matchResult: MatchResult): Identifier | undefined { + const fileKey = resolveFileKey(matchResult.context.uri); + if (!fileKey) return undefined; + let code: string = `${matchResult.context.extraData!.type} $${matchResult.word}`; + if (matchResult.context.extraData!.param) code += ` (parameter)`; + const text: IdentifierText = { lines: [code], start: 0 }; + const localVarIdentifier = buildFromDeclaration(matchResult.word, matchResult.context, text); + const refs = addReference(localVarIdentifier, fileKey, matchResult.context.line.number, matchResult.context.word.start, matchResult.context.word.end); + localVarIdentifier.references[fileKey] = refs; + return localVarIdentifier; +} + +/** + * Get a local variable from the cache given the variable name and a line number + * @param name name of the local variable + * @param lineNum line number of the local variable + * @returns the matching local variable, if any + */ +function getLocalVar(result: MatchResult, lineNum: number): Identifier | undefined { + const blockIdentifier = getBlockScopeIdentifier(lineNum); + if (!blockIdentifier) return; + const blockKey = resolveKeyFromIdentifier(blockIdentifier); + return localVarCache.get(blockKey)?.get(result.word); +} + +/** + * Get all of the local variable names within the scope a line number is in + * @param lineNum a line number used to filter the existing local variables in the scope the line is on + * @returns a set of local variable names + */ +export function getLocalVariableNames(lineNum: number): Set<{ name: string, desc: string }> { + const namesInScriptBlock = new Set<{ name: string, desc: string }>(); + const blockIdentifier = getBlockScopeIdentifier(lineNum); + if (!blockIdentifier) return namesInScriptBlock; + const blockKey = resolveKeyFromIdentifier(blockIdentifier); + const localVarsInScope = localVarCache.get(blockKey); + if (!localVarsInScope) return namesInScriptBlock; + for (const localVar of localVarsInScope.values()) { + if (!localVar.declaration) continue; + const declarationLocation = decodeReferenceToLocation(localVar.declaration.uri, localVar.declaration.ref); + if (!declarationLocation || declarationLocation.range.start.line > lineNum) continue; + const desc = (localVar.extraData?.param) ? `parameter (${localVar.extraData?.type})` : `local variable (${localVar.extraData?.type})`; + namesInScriptBlock.add({ name: localVar.name, desc: desc }); + } + return namesInScriptBlock; +} + +// ==== Script Block Stuff ==== // + +/** + * When > -1 it signals that the next word is a script identifier (proc, label, etc...). Value is the lineNum. + */ +let newCodeblockFlag = -1; + +/** + * Caches the lines that a script identifier is defined on. + * Allows quick lookup of the block any given line in the file is part of. + * Used for things such as accurate local variables (same var name can be used in multiple block scopes). + */ +const codeBlockCache = new LineReferenceCache(); + +function cacheCodeBlock(result: MatchResult): void { + result.context.file.type === 'rs2' ? cacheRs2Block(result) : cacheNonRs2Block(result); +} + +function cacheRs2Block(result: MatchResult) { + if (result.context.matchType.id === TRIGGER.id) { + newCodeblockFlag = result.context.line.number; + } + else if (result.context.line.number === newCodeblockFlag && result.context.word.index === 1) { + codeBlockCache.put(result.context.line.number, result); + newCodeblockFlag = -1; + } +} + +function cacheNonRs2Block(result: MatchResult) { + if (result.context.line.text.startsWith('[') && CONFIG_DECLARATION_REGEX.test(result.context.line.text)) { + codeBlockCache.put(result.context.line.number, result); + } +} + +/** + * Returns the identifier of the block/script a line is in + * @param lineNum line number to return the block scope identifier it is part of + */ +export function getBlockScopeIdentifier(lineNum: number): Identifier | undefined { + const result = codeBlockCache.get(lineNum); + if (result) { + return buildItem(result).identifier; + } +} + +// ==== Switch Statement Stuff ==== // + +/** + * Caches the switch statements a line and particular brace depth is part of + * The key is the brace depth that the switch applies to + * The value is a line reference cache + */ +const switchStmtCache: Map> = new Map(); + +export function cacheSwitchStmt(result: MatchResult): void { + if (result.context.matchType.id === KEYWORD.id && result.word.startsWith('switch_')) { + const braceDepth = result.context.word.braceDepth + 1; + const lineRef = switchStmtCache.get(braceDepth) || new LineReferenceCache(); + const type = dataTypeToMatchType(result.word.substring(7)); + lineRef.put(result.context.line.number, (type.id === UNKNOWN.id) ? SKIP : type); + switchStmtCache.set(braceDepth, lineRef); + } +} + +export function getSwitchStmtType(lineNum: number, braceDepth: number): MatchType | undefined { + return switchStmtCache.get(braceDepth)?.get(lineNum); +} diff --git a/src/cache/class/LineReferenceCache.ts b/src/cache/class/LineReferenceCache.ts new file mode 100644 index 0000000..d03bcf4 --- /dev/null +++ b/src/cache/class/LineReferenceCache.ts @@ -0,0 +1,88 @@ +import type { DataRange } from "../../types"; +import { findMatchInRange } from '../../utils/matchUtils'; + +interface LineReferenceData { + processed: boolean + data: DataRange[] +} + +/** + * Line reference cache is expected to be populated in a sequential manner where one data always comes in + * assuming lower line number to higher line number (like how the lines are read for a file) + * The line reference cache is used to retrieve some data given a line number and a file uri + * For example, finding out which script block your are in on a given line number in a file + * Note: it does not automatically support data which can be nested, such as nested switch statements + * unless you manually handle the inputs carefully (i.e. if a switch has a nested switch inside of it + * you will need 2 entries. One for the first part, one for the second part after the nested switch) + */ +export class LineReferenceCache { + private cache: LineReferenceData; + + constructor() { + this.cache = getDefault(); + } + + /** + * Add a new data range to the cache, we add the data value and the start line, the end lines will be processed on retrieval + * @param startLine the start line of the data + * @param data the data itself to be returned when requesting data for a line + */ + put(startLine: number, data: T): void { + if (!data) return; + const dataRanges = this.cache.data; + dataRanges.push({ start: startLine, end: -1, data: data }); + this.cache.data = dataRanges; + } + + /** + * Returns the data at a particular line number in a particular file, if exists. + * This method can be used even if the cache is not fully completed yet and still being built (sequentially). + * @param lineNum the line number to check what data it belongs to + * @returns the data found, if any + */ + get(lineNum: number): T | undefined { + const cacheItem = this.cache; + let match: DataRange | undefined; + for (const range of cacheItem.data) { + if (lineNum < range.start) break; + match = range; + } + return match?.data; + } + + /** + * Returns the data with its start and end line numbers at a particular line number in a particular file, if exists. + * This method can only be used when the cache data is complete for the file. + * @param lineNum the line number to check what data it belongs to + * @returns the data with its start and end indexes + */ + getWithRange(lineNum: number): DataRange | undefined { + return findMatchInRange(lineNum, this.processIfNeeded()?.data); + } + + /** + * Process the cache data ranges to build the end values + */ + processIfNeeded(): LineReferenceData | undefined { + const cacheItem = this.cache; + if (!cacheItem.processed) { + cacheItem.data.sort((a, b) => a.start - b.start); + for (let i = 0; i < cacheItem.data.length; i++) { + cacheItem.data[i].end = (cacheItem.data[i + 1]?.start ?? 100000) - 1; + } + cacheItem.processed = true; + } + return cacheItem; + } + + /** + * Clear the line reference values of all files in the cache + */ + clear(): void { + this.cache = getDefault(); + } +} + +function getDefault(): LineReferenceData { + return { processed: false, data: [] }; +} diff --git a/src/cache/class/Trie.ts b/src/cache/class/Trie.ts new file mode 100644 index 0000000..ccb0079 --- /dev/null +++ b/src/cache/class/Trie.ts @@ -0,0 +1,128 @@ +class TrieNode { + children: Map; + value: string; + endOfWord: boolean; + + constructor(value: string = '') { + this.children = new Map(); + this.value = value; + this.endOfWord = false; + } +} + +export class Trie { + private root: TrieNode; + + constructor() { + this.root = new TrieNode(); + } + + insert(word: string): TrieNode | false { + if (!word) return false; + let currNode = this.root; + for (const letter of word) { + if (!currNode.children.has(letter)) { + currNode.children.set(letter, new TrieNode(letter)); + } + currNode = currNode.children.get(letter)!; + } + currNode.endOfWord = true; + return currNode; + } + + getLastNode(letters: string, start: TrieNode = this.root): TrieNode | false { + let currNode = start; + for (const letter of letters) { + if (!currNode.children.has(letter)) return false; + currNode = currNode.children.get(letter)!; + } + return currNode; + } + + hasWord(word: string, start: TrieNode = this.root): boolean { + let node = this.getLastNode(word, start); + return node && node !== this.root ? node.endOfWord : false; + } + + findAllWithPrefix(prefix: string, start: TrieNode = this.root): string[] { + let words: string[] = []; + let currNode = this.getLastNode(prefix, start); + if (currNode) { + if (currNode.endOfWord) words.push(prefix); + currNode.children.forEach((child) => + this.getWordsFrom(child, prefix, words) + ); + } + return words; + } + + getWordsFrom(node: TrieNode = this.root, stringAcc: string = '', array: string[] = []): string[] | undefined { + if (!node) return undefined; + stringAcc += node.value; + if (node.endOfWord) array.push(stringAcc); + node.children.forEach((child) => { + this.getWordsFrom(child, stringAcc, array); + }); + return array; + } + + removeWord(word: string): boolean { + if (!word) return false; + let currNode = this.root; + let stack: TrieNode[] = []; + for (const letter of word) { + if (!currNode.children.has(letter)) return false; + currNode = currNode.children.get(letter)!; + if (word[word.length - 1] !== currNode.value) stack.push(currNode); + } + currNode.endOfWord = false; + while (stack.length > 0 && !currNode.endOfWord) { + let prevNode = currNode; + currNode = stack.pop()!; + if (prevNode.children.size > 0) { + break; + } + currNode.children.delete(prevNode.value); + } + return true; + } + + clear(): void { + this.root.children.clear(); + } + + getAllWords(): string[] { + return this.getWordsFrom(this.root, '', []) ?? []; + } + + getAllWordsCount(): number { + return this.countWordsFrom(this.root); + } + + private countWordsFrom(node: TrieNode): number { + let count = node.endOfWord ? 1 : 0; + node.children.forEach((child) => { + count += this.countWordsFrom(child); + }); + return count; + } + + matchLongest(text: string, startIndex: number = 0): number { + if (!text || startIndex < 0 || startIndex >= text.length) return 0; + let currNode = this.root; + let maxLen = 0; + let length = 0; + for (let i = startIndex; i < text.length; i++) { + const ch = text[i]!; + if (!currNode.children.has(ch)) { + break; + } + currNode = currNode.children.get(ch)!; + length++; + if (currNode.endOfWord) { + maxLen = length; + } + } + return maxLen; + } +} diff --git a/src/cache/completionCache.ts b/src/cache/completionCache.ts new file mode 100644 index 0000000..527e9c2 --- /dev/null +++ b/src/cache/completionCache.ts @@ -0,0 +1,68 @@ +import { Trie } from './class/Trie'; + +/** +* One trie per matchType, stores the names of all identifiers of a matchtype in a trie datastructure +* This is used for quicker code completion lookups +* Note: The completion cache is fully controlled/populated by the identifier cache. +*/ +const completionCache: Record = {}; + +function put(name: string, matchTypeId: string): void { + if (!completionCache[matchTypeId]) { + completionCache[matchTypeId] = new Trie(); + } + completionCache[matchTypeId].insert(name); + const colonIndex = name.indexOf(':'); + if (colonIndex >= 0) { + completionCache[matchTypeId].insert(name.substring(colonIndex + 1)); + } +} + +function getAllWithPrefix(prefix: string, matchTypeId: string): string[] | undefined { + const matchTrie = completionCache[matchTypeId]; + if (matchTrie) { + return matchTrie.findAllWithPrefix(prefix); + } + return undefined; +} + +function contains(name: string, matchTypeId: string): boolean { + const matchTrie = completionCache[matchTypeId]; + if (matchTrie) { + return matchTrie.hasWord(name); + } + return false; +} + +function remove(name: string, matchTypeId: string): void { + const matchTrie = completionCache[matchTypeId]; + if (matchTrie) { + matchTrie.removeWord(name); + } +} + +function clear(matchTypeId?: string): void { + if (matchTypeId) { + delete completionCache[matchTypeId]; + } else { + for (const key of Object.keys(completionCache)) { + delete completionCache[key]; + } + } +} + +function getTypes(): string[] { + return Object.keys(completionCache); +} + +function getTypesCount(): string[] { + const labelWidth = 12; + return getTypes() + .sort((a, b) => completionCache[b].getAllWordsCount() - completionCache[a].getAllWordsCount()) + .map(type => { + const label = `${type}:`.padEnd(labelWidth); + return ` ${label} ${completionCache[type].getAllWordsCount()}`; + }); +} + +export { put, getAllWithPrefix, getTypes, getTypesCount, contains, remove, clear }; diff --git a/src/cache/idCache.ts b/src/cache/idCache.ts new file mode 100644 index 0000000..98eca9f --- /dev/null +++ b/src/cache/idCache.ts @@ -0,0 +1,28 @@ +import type { Uri } from "vscode"; +import { LOC, NPC, OBJ } from "../matching/matchType"; +import { getFileInfo } from "../utils/fileUtils"; + +const cache: Map> = new Map(); + +const cachedTypes: string[] = [NPC.id, OBJ.id, LOC.id]; + +export function add(matchTypeId: string, id: string, name: string): void { + if (cachedTypes.includes(matchTypeId)) { + cache.get(matchTypeId)!.set(id, name); + } +} + +export function get(matchTypeId: string, id: string): string | undefined { + return cache.get(matchTypeId)?.get(id); +} + +export function clear(uri: Uri): void { + const fileInfo = getFileInfo(uri); + if (fileInfo.type === 'pack' && cachedTypes.includes(fileInfo.name.toUpperCase())) { + cache.set(fileInfo.name.toUpperCase(), new Map()); + } +} + +export function clearAll(): void { + cachedTypes.forEach(type => cache.set(type, new Map())); +} diff --git a/src/cache/identifierCache.ts b/src/cache/identifierCache.ts new file mode 100644 index 0000000..69a6d6e --- /dev/null +++ b/src/cache/identifierCache.ts @@ -0,0 +1,245 @@ +const sizeof = require('object-sizeof'); +import type { Uri } from 'vscode'; +import type { FileIdentifiers, FileKey, Identifier, IdentifierKey, IdentifierText, MatchContext, MatchType } from '../types'; +import { addReference, buildFromDeclaration, buildFromReference, serializeIdentifier } from '../resource/identifierFactory'; +import { resolveFileKey, resolveIdentifierKey } from '../utils/cacheUtils'; +import { clear as clearCompletionCache, put as putCompletionCache, remove as removeCompletionCache } from './completionCache'; + +/** + * The identifierCache stores all matched identifiers in the workspace + * identifierCache = {key [name+matchTypeId]: identifier} + * See identifierFactory.js for the object structure + */ +const identifierCache = new Map(); + +/** + * The fileToIdentifierMap keeps track of all declarations and references within a file + * This is used to invalidate any identifiers within a file and reprocess the file when changes have been made + */ +const fileToIdentifierMap = new Map(); + +/** + * Get the cached identifier using the identifier name and match type + * @param name Name of the identifier + * @param match MatchType of the identifier + * @returns Identifier if found, undefined otherwise + */ +function get(name: string, match: MatchType): Identifier | undefined { + const key = resolveIdentifierKey(name, match); + return key !== undefined ? identifierCache.get(key) : undefined; +} + +/** + * Get the cacched identifier using the identifier key + * @param key IdentifierKey + * @returns Identifier if found, undefined otherwise + */ +function getByKey(key: IdentifierKey): Identifier | undefined { + return identifierCache.get(key); +} + +/** + * Put (declaration) identifier into the cache. Creates the identifier from the given data. + * @param name Identifier name + * @param context Identifier match type + * @param declaration Identifier declaration location + * @param text Identifier text, full file text as lines and line number it is found on + * @returns The new identifier or undefined if unable to resolve + */ +function put(name: string, context: MatchContext, text: IdentifierText): Identifier | undefined { + // Make sure cache keys resolve correctly + const key = resolveIdentifierKey(name, context.matchType); + const fileKey = resolveFileKey(context.uri); + if (!key || !fileKey) { + return; + } + + // Retrieve current identifier from cache (if any) + let curIdentifier: Identifier | undefined = identifierCache.get(key); + + // If the current identifier in cache already is the declaration, don't overwrite (happens on 2nd cache populating pass) + if (curIdentifier && curIdentifier.declaration) { + return curIdentifier; + } + + // Build the identifier to insert + const identifier = buildFromDeclaration(name, context, text); + + // Copy existing (refernces only) identifier values (reference & id) into the new declaration identifier + if (curIdentifier && curIdentifier.id) identifier.id = curIdentifier.id; + if (curIdentifier && !curIdentifier.declaration) identifier.references = curIdentifier.references; + + // Add the declarartion to the file map + addToFileMap(fileKey, key, true); + + // Add the identifier to the cache + identifierCache.set(key, identifier); + + // Add the info to the completion cache + putCompletionCache(name, context.matchType.id); + + // Also insert the declaration as a reference + putReference(name, context, context.uri, context.line.number, context.word.start, context.word.end); + + // Return the created identifier + return identifier; +} + +/** + * Put (reference) identifier into the cache. Adds a reference if identifier already exists, creates it if not. + * @param name Identifier name + * @param context Context of the match this identifier was found in + * @param uri file URI the reference is found in + * @param lineNum line number within the file the reference is found on + * @param startIndex the index within the line where the reference is found + * @param packId the pack id, if any (ex: Obj id 1234) + */ +function putReference(name: string, context: MatchContext, uri: Uri, lineNum: number, startIndex: number, endIndex: number): void { + // Make sure cache keys resolve correctly + const key = resolveIdentifierKey(name, context.matchType); + const fileKey = resolveFileKey(uri); + if (!key || !fileKey) { + return; + } + + // If the identifier doesn't yet exist in the cache, build the identifier with minimal necessary data + if (!identifierCache.has(key)) { + const ref = buildFromReference(name, context); + if (!ref.matchId) return; + identifierCache.set(key, { ...ref, references: {} }); + } + + // Get the current references for this identifier in the current file (if any) and add this new reference + const curIdentifier = identifierCache.get(key); + if (!curIdentifier) return; + const fileReferences = addReference(curIdentifier, fileKey, lineNum, startIndex, endIndex, context); + + // Add the reference to the file map + addToFileMap(fileKey, key, false); + + // Update the identifier in the identifier cache with the new references + curIdentifier.references[fileKey] = fileReferences; + + // If the matchType of this identifier is reference only, add the data to the completion cache (others will get added when the declaration is added) + if (context.matchType.referenceOnly) putCompletionCache(name, context.matchType.id); +} + +/** + * Clears the identifier cache and relevant supporting caches + */ +function clear(): void { + identifierCache.clear(); + fileToIdentifierMap.clear(); + clearCompletionCache(); +} + +/** + * Clears out all references and declarations from the cache of a given file + * @param uri The file URI to clear out of the cache + */ +function clearFile(uri: Uri): void { + // Make sure cache keys resolve correctly + const fileKey = resolveFileKey(uri); + if (!fileKey) { + return; + } + + // Get the identifiers in the file + const identifiersInFile = fileToIdentifierMap.get(fileKey) || { declarations: new Set(), references: new Set() }; + + // Iterate thru the references in the file + identifiersInFile.references.forEach((key) => { + const iden = identifierCache.get(key); + if (iden) { + // Delete references to the cleared file from every identifier which referenced the file + if (iden.references[fileKey]) { + delete iden.references[fileKey]; + } + // Cleanup/Delete identifiers without a declaration who no longer have any references + if (Object.keys(iden.references).length === 0 && !iden.declaration) { + if (iden.matchId) removeCompletionCache(iden.name, iden.matchId); + identifierCache.delete(key); + } + } + }); + + // Iterate thru the declarations in the file + identifiersInFile.declarations.forEach((key) => { + const iden = identifierCache.get(key); + if (iden) { + // If the identifier has orphaned references, then we only delete the declaration and keep the identifier w/references + // Otherwise, we delete the entire identifier (no declaration and no references => no longer exists in any capacity) + if (iden.matchId) removeCompletionCache(iden.name, iden.matchId); + const hasOrphanedRefs = Object.keys(iden.references).length > 0; + if (hasOrphanedRefs) { + delete iden.declaration; + } else { + identifierCache.delete(key); + } + } + }); + + // Remove the entry for the file from the fileToIdentifierMap + fileToIdentifierMap.delete(fileKey); +} + +/** + * Update the fileMap with the file of a new identifier declared or referenced within said file + * @param fileKey fileKey where this identifier declaration or reference is found + * @param identifierKey identifierKey of this identifier + * @param declaration boolean: true if inserting a declaration, false if inserting a reference + */ +function addToFileMap(fileKey: FileKey, identifierKey: IdentifierKey, declaration = true): void { + // Get the current identifiers in a file, or a new default empty set for both declarations and reference if nothing exists + const identifiersInFile = fileToIdentifierMap.get(fileKey) || { declarations: new Set(), references: new Set() }; + + // If we are inserting a declaration update declaration identifiers, else update reference identifiers of the file + (declaration) ? identifiersInFile.declarations.add(identifierKey) : identifiersInFile.references.add(identifierKey); + + // Update the cache with the new data + fileToIdentifierMap.set(fileKey, identifiersInFile); +} + +/** + * Serialize the contents of the identifier cache, used for the export cache debug command + * @returns cache records + */ +function serializeCache(): Record { + const serialized: Record = {}; + identifierCache.forEach((identifier, key) => { + serialized[key] = serializeIdentifier(identifier); + }); + return serialized; +} + +/** + * Return all of the cache keys in the identifier cache, used for the export cache keys debug command + * @returns cache keys + */ +function getCacheKeys(): string[] { + return Array.from(identifierCache.keys()).sort(); +} + +function getCacheKeyCount(identifiers = true): number { + return identifiers ? identifierCache.size : fileToIdentifierMap.size; +} + +function appriximateSize() { + return sizeof(identifierCache) + sizeof(fileToIdentifierMap); +} + +function getTotalReferences(): number { + let total = 0; + for (const identifier of identifierCache.values()) { + for (const references of Object.values(identifier.references ?? {})) { + total += references.size; + } + } + return total; +} + +function getFileIdentifiers(uri: Uri): FileIdentifiers | undefined { + return fileToIdentifierMap.get(uri.fsPath); +} + +export { get, getByKey, put, putReference, clear, clearFile, serializeCache, getCacheKeys, getCacheKeyCount, appriximateSize, getTotalReferences, getFileIdentifiers }; diff --git a/src/cache/projectFilesCache.ts b/src/cache/projectFilesCache.ts new file mode 100644 index 0000000..72dfe23 --- /dev/null +++ b/src/cache/projectFilesCache.ts @@ -0,0 +1,46 @@ +import { basename } from "path"; +import type { Uri } from "vscode"; +import { workspace } from "vscode"; +import { getAllMatchTypes } from "../matching/matchType"; +import { fileNamePostProcessor } from "../resource/postProcessors"; +import { getFileInfo } from "../utils/fileUtils"; +import { addExceptionWord } from "../parsing/wordExceptions"; + +const filesInDataSrc: Set = new Set(); + +export async function rebuild(): Promise { + filesInDataSrc.clear(); + const uris = await workspace.findFiles("**/*", "**/{node_modules,out,dist,build}/**"); + const filesToCheckNames = getFileTypesToCheckNames(); + uris.forEach(uri => { + filesInDataSrc.add(basename(uri.fsPath)) + const fileInfo = getFileInfo(uri); + if (filesToCheckNames.has(fileInfo.type)) { + addExceptionWord(fileInfo.name); + } +}); +} + +export function exists(name?: string, uri?: Uri): boolean { + const fileName = name ?? (uri ? basename(uri.fsPath) : undefined); + return fileName ? filesInDataSrc.has(fileName) : false; +} + +export function removeUris(uris: Uri[]) { + uris.map(uri => uri.fsPath).forEach(file => filesInDataSrc.delete(file)); +} + +export function addUris(uris: Uri[]) { + uris.map(uri => uri.fsPath).forEach(file => filesInDataSrc.add(file)); +} + +export function clear(): void { + filesInDataSrc.clear(); +} + +function getFileTypesToCheckNames(): Set { + return new Set(getAllMatchTypes() + .filter(type => type.postProcessor === fileNamePostProcessor) + .map(type => (type.fileTypes ?? [])[0]) + .filter(Boolean)); +} diff --git a/src/core/commands.ts b/src/core/commands.ts new file mode 100644 index 0000000..b6e5a49 --- /dev/null +++ b/src/core/commands.ts @@ -0,0 +1,139 @@ +import type { ExtensionContext, QuickPickItem } from 'vscode'; +import { commands, ExtensionMode, Position, Range, Selection, StatusBarAlignment, TextEditorRevealType, window, workspace } from 'vscode'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; +import { getCacheKeys, serializeCache } from '../cache/identifierCache'; +import { processAllFiles } from './manager'; +import { showIdentifierLookupView } from '../webview/identifierLookupView'; + +/** + * The interface for registering a new command. + * The id value must be the same value as the "command" value used to register the command in package.json + * Set debugOnly to true if this command should not be available in normal production releases + */ +interface Command { + id: string; + debug?: { label: string }; + command: (...args: any[]) => Promise; +} + +/** + * Register the commands - use during extension activation to register the commands depending on if dev mode or not + * @param context Extension context + */ +export function registerCommands(context: ExtensionContext) { + Object.values(extensionCommands) + .filter(command => context.extensionMode === ExtensionMode.Development || command.debug) + .forEach(command => context.subscriptions.push(commands.registerCommand(command.id, command.command))); + + if (context.extensionMode === ExtensionMode.Development) { + const item = window.createStatusBarItem('runescriptDebug', StatusBarAlignment.Right, 100); + item.text = 'Runescript: Debug'; + item.command = 'RuneScriptLanguage.debugMenu'; + item.show(); + context.subscriptions.push(item); + } +} + +/** + * The list of commands to be registered + * Commands need to be added to package.json to show in the command palatte (use same id) + * Important: Debug commands should NOT be added to package.json + */ +export const extensionCommands: Record = { + rebuildCache: { + id: 'RuneScriptLanguage.rebuildCache', + command: rebuildCache + }, + debugMenu: { + id: 'RuneScriptLanguage.debugMenu', + command: debugMenu + }, + dumpCache: { + id: 'RuneScriptLanguage.dumpCache', + debug: { label: 'Dump full identifier cache' }, + command: dumpCache + }, + dumpCacheKeys: { + id: 'RuneScriptLanguage.dumpCacheKeys', + debug: { label: 'Dump identifier cache keys' }, + command: dumpCacheKeys + }, + lookupIdentifier: { + id: 'RuneScriptLanguage.lookupIdentifier', + debug: { label: 'Lookup identifier (webview)' }, + command: lookupIdentifier + }, + jumpToMapSection: { + id: 'RuneScriptLanguage.jumpToMapSection', + command: jumpToMapSection + } +}; + +/* ======================== COMMAND FUNCTIONS BELOW ======================== */ + +async function rebuildCache() { + processAllFiles(); +} + +async function dumpCache() { + void writeToFile(JSON.stringify(serializeCache(), undefined, 2), 'identifier cache', 'identifier-cache.json'); +} + +async function dumpCacheKeys() { + void writeToFile(JSON.stringify(getCacheKeys(), undefined, 2), 'identifier cache keys', 'identifier-cache-keys.json'); +} + +async function lookupIdentifier() { + showIdentifierLookupView(); +} + +async function jumpToMapSection(line?: number) { + const editor = window.activeTextEditor; + if (!editor || line === undefined) return; + const position = new Position(line, 0); + editor.selection = new Selection(position, position); + editor.revealRange(new Range(position, position), TextEditorRevealType.AtTop); +} + +interface DebugMenuItem extends QuickPickItem { + commandId: string; +} + +async function debugMenu() { + const debugCommands: DebugMenuItem[] = Object.values(extensionCommands) + .filter(command => command.debug) + .map(command => ({ label: command.debug!.label, commandId: command.id })) + const pick = await window.showQuickPick(debugCommands, { + placeHolder: 'Runescript debug actions' + }); + if (!pick) return; + await commands.executeCommand(pick.commandId); +} + +/* ======================== HELPER FUNCTIONS BELOW ======================== */ + +/** + * Write data to a file. Prompts user for file name. Displays error/success message depending on outcome. + * @param dataToWrite The string of the data to write to the file + * @param nameOfData The name of the data used for the info prompts and display messages + * @param defaultFileName The default name to use for the file (pre-populated value when prompting for file name) + */ +async function writeToFile(dataToWrite: string, nameOfData: string, defaultFileName: string) { + const workspaceFolder = workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + window.showErrorMessage('No workspace folder found to write the file to.'); + return; + } + const defaultName = defaultFileName; + const fileName = await window.showInputBox({ + prompt: `Enter file name for ${nameOfData} dump`, + value: defaultName + }); + if (!fileName) { + return; + } + const dumpPath = join(workspaceFolder.uri.fsPath, fileName); + await writeFile(dumpPath, dataToWrite, 'utf8'); + window.showInformationMessage(`${nameOfData} written to ${dumpPath}`); +} diff --git a/src/core/devMode.ts b/src/core/devMode.ts new file mode 100644 index 0000000..599889c --- /dev/null +++ b/src/core/devMode.ts @@ -0,0 +1,237 @@ +import type { OutputChannel, Uri } from "vscode"; +import type { MatchResult } from "../types"; +import { LogLevel, window } from "vscode"; +import { version as packageVersion } from '../../package.json'; +import { processAllFiles } from "./manager"; +import { getSettingValue, Settings } from "./settings"; +import { appriximateSize, getCacheKeyCount, getTotalReferences } from "../cache/identifierCache"; +import { getTypesCount } from "../cache/completionCache"; +import { getExceptionWords } from "../parsing/wordExceptions"; +import { getFileName } from "../utils/fileUtils"; + +export enum Events { + FileSaved = 'file saved', + ActiveFileTextChanged = 'active file text changed', + ActiveFileChanged = 'active document changed', + FileDeleted = 'file deleted', + FileCreated = 'file created', + FileChanged = 'file changed', + SettingsChanged = 'settings changed', + GitBranchChanged = 'git branch changed', + FileParsed = 'file parsed', + FileMatched = 'matched parsed file', + MapFileOpened = 'map file opened', + MapFileEdited = 'map file edited' +} + +interface initializationMetrics { + fileCount: number, + wordCount: number, + fileReadDuration: number, + exceptionWordScanDuration: number, + fileParsingDuration: number, + firstPassDuration: number, + secondPassDuration: number +} + +/** + * Output channel to write debug/dev messages to + */ +const outputChannel: OutputChannel = window.createOutputChannel('Runescript'); + +/** + * Contains the last rebuild all metrics + */ +export const rebuildMetrics: initializationMetrics = { + fileCount: 0, + wordCount: 0, + fileReadDuration: 0, + exceptionWordScanDuration: 0, + fileParsingDuration: 0, + firstPassDuration: 0, + secondPassDuration: 0 +} + +/** + * An easy check to see if dev mode is enabled or not + */ +export function isDevMode() { + return getSettingValue(Settings.DevMode); +}; + +/** + * Called only during extenstion activation + */ +export function registerDevMode() { + if (isDevMode()) { + outputChannel.clear(); + appendOutput([`Runescript Extension v${packageVersion ?? 'unknown'} [Dev Mode]`]); + } +} + +/** + * Called when dev mode is toggled on + */ +export function initDevMode() { + outputChannel.clear(); + appendOutput([`Runescript Extension v${packageVersion ?? 'unknown'} [Dev Mode]`]); + processAllFiles(); + outputChannel.show(); +} + +/** + * Append lines of text to the dev mode output channel + * @param lines lines of text to append + */ +function appendOutput(lines: string[]) { + lines.forEach(line => outputChannel.appendLine(line)); +} + +/** + * Clear out the dev mode output channel, and append new lines + * @param lines lines of text to append + */ +export function replaceOutput(lines: string[]) { + outputChannel.clear(); + appendOutput([`Runescript Extension v${packageVersion ?? 'unknown'} [Dev Mode]`]); + appendOutput(lines); +} + +export function clearDevModeOutput() { + outputChannel.clear(); +} + +/** + * Log all of the metrics related to full file cache rebuild to the dev mode output channel + */ +export function reportRebuildMetrics(): void { + const exceptionWords = getExceptionWords(); + const lines = [ + ``, + `=== Rebuild all files metrics ===`, + `Total duration: ${formatToSec(rebuildMetrics.fileReadDuration + rebuildMetrics.fileParsingDuration + rebuildMetrics.exceptionWordScanDuration + rebuildMetrics.firstPassDuration + rebuildMetrics.secondPassDuration)}`, + ` Read all files: ${formatMs(rebuildMetrics.fileReadDuration)}`, + ` Scan for exception words: ${formatMs(rebuildMetrics.exceptionWordScanDuration)}`, + ` Parse all files: ${formatMs(rebuildMetrics.fileParsingDuration)}`, + ` Match words (first pass): ${formatMs(rebuildMetrics.firstPassDuration)}`, + ` Match words (second pass): ${formatMs(rebuildMetrics.secondPassDuration)}`, + ``, + `Parsing:`, + ` Files read, parsed, and matched: ${rebuildMetrics.fileCount}`, + ` Valid found word count: ${rebuildMetrics.wordCount}`, + ` Exception words discovered: ${exceptionWords.length}`, + ` Exception word values: ${exceptionWords.sort((a, b) => a.localeCompare(b)).map(w => `${w}`).join(', ')}`, + ``, + `Cache info:`, + ` Approximate size: ${(appriximateSize() / (1024 * 1024)).toFixed(2)} MB`, + ` Identifiers (total): ${getCacheKeyCount(true)} (total references ${getTotalReferences()})`, + ...getTypesCount(), + `=== End rebuild all metrics ===`, + `` + ] + appendOutput(lines); +} + +/** + * Converts a number of milliseconds into a string showing the value in seconds to 2 decimal places + * @param ms milliseconds + * @returns formatted string + */ +function formatToSec(ms: number) { + return `${(ms / 1000).toFixed(2)} s`; +} + +/** + * Converts a number of milliseconds into a string + * @param ms milliseconds + * @returns formatted string + */ +function formatMs(ms: number) { + return `${Math.round(ms)} ms`; +} + +/** + * Converts a number of milliseconds into a string to 2 decimal places + * @param ms milliseconds + * @returns formatted string + */ +function formatMs2(ms: number) { + return `${ms.toFixed(2)} ms`; +} + +export function logFileEvent(uri: Uri, event: Events, extra?: string) { + const resolver = () => { + return `on file ${getFileName(uri)}${extra ? ` [${extra}]` : ''}`; + } + logEvent(event, resolver); +} + +export function logSettingsEvent(setting: Settings) { + const resolver = () => `setting ${setting} updated to ${getSettingValue(setting)}`; + logEvent(Events.SettingsChanged, resolver); +} + +export function logEvent(event: Events, msgResolver: () => string) { + const resolver = () => { + const msg = msgResolver(); + return `Event [${event}]${msg ? ' ' + msg : ''}` + } + log(resolver, LogLevel.Info); +} + +export function logFileParsed(startTime: number, uri: Uri, lines: number, partial = false) { + const resolver = () => { + const msg = partial ? 'Partial reparse of file' : 'Parsed file'; + return `${msg} ${getFileName(uri)} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`; + } + log(resolver, LogLevel.Debug); +} + +export function logMapFileProcessed(startTime: number, uri: Uri, lines: number, partial = false) { + const resolver = () => { + const msg = partial ? 'Processed partial reparse of map file' : 'Processed full parse of map file'; + return `${msg} ${getFileName(uri)} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`; + } + log(resolver, LogLevel.Debug); +} + +export function logFileRebuild(startTime: number, uri: Uri, matches: MatchResult[]) { + const resolver = () => { + return `Rebuilt file ${getFileName(uri)} in ${formatMs2(performance.now() - startTime)} [matches: ${matches.length}]`; + } + log(resolver, LogLevel.Debug); +} + +export function logDebug(message: string) { + log(() => message, LogLevel.Debug); +} + +export function logInfo(message: string) { + log(() => message, LogLevel.Info); +} + +export function logWarning(message: string) { + log(() => message, LogLevel.Warning); +} + +export function logError(message: string) { + log(() => message, LogLevel.Warning); +} + +function log(msgResolver: () => string, logLevel: LogLevel) { + if (!isDevMode()) return; + const msg = msgResolver(); + if (!msg) return; + let level = ''; + switch (logLevel) { + case LogLevel.Error: level = 'error'; break; + case LogLevel.Warning: level = 'warn '; break; + case LogLevel.Info: level = 'info '; break; + case LogLevel.Debug: level = 'debug'; break; + } + const now = new Date(); + const time = now.toLocaleTimeString('en-US', { hour12: false }); + const ms = String(now.getMilliseconds()).padStart(3, '0'); + const message = `[${time}.${ms}] ${level}: ${msg}`; + appendOutput([message]); +} diff --git a/src/core/diagnostics.ts b/src/core/diagnostics.ts new file mode 100644 index 0000000..c82460b --- /dev/null +++ b/src/core/diagnostics.ts @@ -0,0 +1,166 @@ +import type { DiagnosticCollection, ExtensionContext, Diagnostic } from 'vscode'; +import type { FileIdentifiers, IdentifierKey, MatchResult } from '../types'; +import type { RunescriptDiagnostic } from '../diagnostics/RunescriptDiagnostic'; +import { languages, Range, Uri } from 'vscode'; +import { getSettingValue, Settings } from './settings'; +import { UnknownIdentifierDiagnostic } from '../diagnostics/unknownIdentifierDiagnostic'; +import { UnknownFileDiagnostic } from '../diagnostics/unknownFileDiagnostic'; +import { get as getIdentifier, getByKey } from '../cache/identifierCache'; +import { decodeReferenceToRange } from '../utils/cacheUtils'; +import { getFileInfo, getFileName } from '../utils/fileUtils'; +import { getAllMatchTypes } from '../matching/matchType'; + +let diagnostics: DiagnosticCollection | undefined; + +const unknownIdenDiagnostic = new UnknownIdentifierDiagnostic(); +const unknownFileDiagnostic = new UnknownFileDiagnostic(); + +const runescriptDiagnostics: RunescriptDiagnostic[] = [ + unknownIdenDiagnostic, + unknownFileDiagnostic +] + +export function registerDiagnostics(context: ExtensionContext): void { + diagnostics = languages.createDiagnosticCollection('runescript-extension-diagnostics'); + context.subscriptions.push({ dispose: () => disposeDiagnostics() }); +} + +function disposeDiagnostics(): void { + diagnostics?.dispose(); + diagnostics = undefined; + runescriptDiagnostics.forEach(d => d.clearAll()); +} + +export function clearAllDiagnostics(): void { + diagnostics?.clear(); + runescriptDiagnostics.forEach(d => d.clearAll()); +} + +export function clearFileDiagnostics(uri: Uri): void { + diagnostics?.delete(uri); + runescriptDiagnostics.forEach(d => d.clearFile(uri)); +} + +export function getFileDiagnostics(uri: Uri): readonly Diagnostic[] { + return diagnostics?.get(uri) || []; +} + +export function setCustomDiagnostics(uri: Uri, diagnosticsList: Diagnostic[]): void { + if (!getSettingValue(Settings.ShowDiagnostics) || !diagnostics) return; + diagnostics.set(uri, diagnosticsList); +} + +export async function rebuildFileDiagnostics(uri: Uri, matchResults: MatchResult[]): Promise { + if (!getSettingValue(Settings.ShowDiagnostics) || !diagnostics) return; + const diagnosticsList: Diagnostic[] = []; + for (const result of matchResults) { + // Skip these types as they never have diagnostics + if (result.context.matchType.noop || !result.context.matchType.cache) { + continue; + } + + // Build the range for the diagnostic + const { line: { number: lineNum }, word: { start, end } } = result.context; + const range = new Range(lineNum, start, lineNum, end + 1); + + // Check all match result against all diagnostics, add if detected + runescriptDiagnostics.forEach(diag => { + if (diag.check(result)) { + const newDiagnostic = diag.createDiagnostic(range, result); + newDiagnostic.source = 'runescript'; + diagnosticsList.push(newDiagnostic); + } + }); + } + diagnostics.set(uri, diagnosticsList); +} + +export function handleFileUpdate(before?: FileIdentifiers, after?: FileIdentifiers): void { + if (!diagnostics) return; + const beforeDecs = before?.declarations ?? new Set(); + const afterDecs = after?.declarations ?? new Set(); + const addedDeclarations: IdentifierKey[] = []; + const removedDeclarations: IdentifierKey[] = []; + + for (const key of beforeDecs) { + if (!afterDecs.has(key)) { + removedDeclarations.push(key); + } + } + for (const key of afterDecs) { + if (!beforeDecs.has(key)) { + addedDeclarations.push(key); + } + } + + // New declaration added: clear any cached "unknown identifier" diagnostics for this identifier key. + for (const key of addedDeclarations) { + const cleared = unknownIdenDiagnostic.clearUnknowns(key); + if (!cleared) continue; + for (const [fileKey, ranges] of cleared) { + removeDiagnostics(Uri.file(fileKey), ranges) + } + } + + // Removed declarations: get the identifier, add "unknown identifier" diagnostic to every reference it has + for (const key of removedDeclarations) { + const iden = getByKey(key); + if (!iden) continue; + for (const [fsPath, locations] of Object.entries(iden.references)) { + const uri = Uri.file(fsPath); + const fileDiagnostics = [...(diagnostics.get(uri) ?? [])]; + for (const location of locations) { + const range = decodeReferenceToRange(location); + if (!range) continue; + const exists = fileDiagnostics.some(d => d.range.isEqual(range)); + if (!exists) { + fileDiagnostics.push(unknownIdenDiagnostic.createByRangeIden(range, iden, fsPath)); + } + } + diagnostics.set(uri, fileDiagnostics); + } + } +} + +export function handleFileCreated(uri: Uri) { + const fileKey = getFileName(uri); + const diagnostics = unknownFileDiagnostic.clearUnknowns(fileKey); + for (const [uri, ranges] of diagnostics) { + removeDiagnostics(Uri.file(uri), ranges); + } +} + +export function handleFileDeleted(uri: Uri) { + // If I delete name.if, I know that the identifier with cache key nameINTERFACE reference ranges + // need to add the "unknown file" diagnostic + if (!diagnostics) return; + const fileInfo = getFileInfo(uri); + if (fileInfo.type === 'rs2') return; + const fileKey = `${fileInfo.name}.${fileInfo.type}`; + const match = getAllMatchTypes().find(m => m.fileTypes?.includes(fileInfo.type)); + if (!match) return; + const identifier = getIdentifier(fileInfo.name, match); + if (!identifier) return; + for (const [fsPath, locations] of Object.entries(identifier.references)) { + const uri = Uri.file(fsPath); + const fileDiagnostics = [...(diagnostics.get(uri) ?? [])]; + for (const location of locations) { + const range = decodeReferenceToRange(location); + if (!range) continue; + const exists = fileDiagnostics.some(d => d.range.isEqual(range)); + if (!exists) { + fileDiagnostics.push(unknownFileDiagnostic.createByFileKey(range, fileKey, fsPath)); + } + } + diagnostics.set(uri, fileDiagnostics); + } +} + +function removeDiagnostics(uri: Uri, ranges: Range[]): void { + if (!diagnostics) return; + const existing = diagnostics.get(uri) ?? []; + const filtered = existing.filter(diag => + !ranges.some(r => r.isEqual(diag.range)) + ); + diagnostics.set(uri, filtered); +} diff --git a/src/core/eventHandlers.ts b/src/core/eventHandlers.ts new file mode 100644 index 0000000..45fdfcc --- /dev/null +++ b/src/core/eventHandlers.ts @@ -0,0 +1,156 @@ +import type { ConfigurationChangeEvent, ExtensionContext, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, TextEditor, Uri } from "vscode"; +import { window, workspace } from "vscode"; +import { clearAllDiagnostics, handleFileCreated as handleFileCreatedDiagnostics, handleFileDeleted as handleFileDeletedDiagnostics, handleFileUpdate as handleFileUpdateDiagnostics } from "./diagnostics"; +import { getFileText, isActiveFile, isValidFile } from "../utils/fileUtils"; +import { addUris, removeUris } from "../cache/projectFilesCache"; +import { eventAffectsSetting, getSettingValue, Settings } from "./settings"; +import { clearDevModeOutput, initDevMode, logEvent, logFileEvent, logSettingsEvent, Events } from "./devMode"; +import { getLines } from "../utils/stringUtils"; +import { allFileTypes, clearFile, processAllFiles, queueFileRebuild } from "./manager"; +import { parseFile, reparseFileWithChanges } from "../parsing/fileParser"; +import { getFileIdentifiers } from "../cache/identifierCache"; +import { handleMapFileClosed, handleMapFileEdited, handleMapFileOpened, isMapFile } from "./mapManager"; + + +const debounceTimeMs = 150; // debounce time for normal active file text changes + +export function registerEventHandlers(context: ExtensionContext): void { + const patterns = Array.from(allFileTypes, ext => `**/*.${ext}`); + const fileWatcher = workspace.createFileSystemWatcher(`{${patterns.join(',')}}`); + const gitBranchWatcher = workspace.createFileSystemWatcher('**/.git/HEAD'); + gitBranchWatcher.onDidCreate(onGitBranchChange); + fileWatcher.onDidChange(onChangeFile); + fileWatcher.onDidCreate(onCreateFile); + fileWatcher.onDidDelete(onDeleteFile); + const activeFileTextChanged = workspace.onDidChangeTextDocument(onActiveFileTextChange); + const activeDocumentChanged = window.onDidChangeActiveTextEditor(onActiveDocumentChange); + const settingsChanged = workspace.onDidChangeConfiguration(onSettingsChange); + + context.subscriptions.push( + gitBranchWatcher, + fileWatcher, + activeFileTextChanged, + activeDocumentChanged, + settingsChanged, + ); +} + +let pendingChanges: TextDocumentContentChangeEvent[] = []; +let pendingDocument: TextDocument | undefined; +let pendingTimer: NodeJS.Timeout | undefined; +let pendingRebuildPromise: Promise | undefined; +let pendingRebuildResolve: (() => void) | undefined; +const lastRebuildVersionByUri = new Map(); +const rebuildWaiters: Array<{ uri: string; version: number; resolve: () => void }> = []; +function onActiveFileTextChange(textChangeEvent: TextDocumentChangeEvent): void { + if (!isActiveFile(textChangeEvent.document.uri)) return; + if (isMapFile(textChangeEvent.document.uri)) return handleMapFileEdited(textChangeEvent); + if (!isValidFile(textChangeEvent.document.uri)) return; + + pendingDocument = textChangeEvent.document; + pendingChanges.push(...textChangeEvent.contentChanges); + + if (pendingTimer) clearTimeout(pendingTimer); + if (!pendingRebuildPromise) { + pendingRebuildPromise = new Promise((resolve) => { + pendingRebuildResolve = resolve; + }); + } + pendingTimer = setTimeout(() => { + const doc = pendingDocument; + if (!doc) return; + logFileEvent(doc.uri, Events.ActiveFileTextChanged, `partial reparse`); + const changes = pendingChanges; + pendingChanges = []; + pendingTimer = undefined; + const parsedFile = reparseFileWithChanges(doc, changes)!; + void queueFileRebuild(doc.uri, getLines(doc.getText()), parsedFile).finally(() => { + lastRebuildVersionByUri.set(doc.uri.fsPath, doc.version); + for (let i = rebuildWaiters.length - 1; i >= 0; i--) { + const waiter = rebuildWaiters[i]!; + if (waiter.uri === doc.uri.fsPath && waiter.version <= doc.version) { + rebuildWaiters.splice(i, 1); + waiter.resolve(); + } + } + const resolve = pendingRebuildResolve; + pendingRebuildPromise = undefined; + pendingRebuildResolve = undefined; + resolve?.(); + }); + }, debounceTimeMs); +} + +export function waitForActiveFileRebuild(document: TextDocument, version = document.version): Promise { + const uri = document.uri.fsPath; + const lastVersion = lastRebuildVersionByUri.get(uri) ?? -1; + if (lastVersion >= version) return Promise.resolve(); + return new Promise((resolve) => { + rebuildWaiters.push({ uri, version, resolve }); + }); +} + +async function onActiveDocumentChange(editor: TextEditor | undefined): Promise { + if (!editor) return; + if (isMapFile(editor.document.uri)) { + return handleMapFileOpened(editor.document); + } else { + handleMapFileClosed(); + } + if (!isValidFile(editor.document.uri)) return; + logFileEvent(editor.document.uri, Events.ActiveFileChanged, 'full reparse'); + updateFileFromDocument(editor.document); +} + +function onDeleteFile(uri: Uri) { + logFileEvent(uri, Events.FileDeleted, 'relevant cache entries invalidated'); + handleFileDeletedDiagnostics(uri); + removeUris([uri]); + if (!isValidFile(uri)) return; + handleFileUpdateDiagnostics(getFileIdentifiers(uri), undefined); + clearFile(uri); +} + +function onCreateFile(uri: Uri) { + logFileEvent(uri, Events.FileCreated, 'full parse'); + handleFileCreatedDiagnostics(uri); + addUris([uri]); + if (!isValidFile(uri)) return; + void updateFileFromUri(uri); +} + +function onChangeFile(uri: Uri) { + if (isActiveFile(uri)) return; // let the active document text change event handle active file changes + if (!isValidFile(uri)) return; + logFileEvent(uri, Events.FileChanged, 'full reparse'); + void updateFileFromUri(uri); +} + +function onGitBranchChange() { + logEvent(Events.GitBranchChanged, () => 'full cache rebuild'); + processAllFiles(); +} + +async function updateFileFromUri(uri: Uri): Promise { + if (!isValidFile(uri)) return; + const fileText = await getFileText(uri); + void queueFileRebuild(uri, fileText, parseFile(uri, fileText)); +} + +function updateFileFromDocument(document: TextDocument): void { + if (!isValidFile(document.uri)) return; + const fileText = getLines(document.getText()); + void queueFileRebuild(document.uri, fileText, parseFile(document.uri, fileText)); +} + +function onSettingsChange(event: ConfigurationChangeEvent) { + if (eventAffectsSetting(event, Settings.ShowHover)) logSettingsEvent(Settings.ShowHover); + if (eventAffectsSetting(event, Settings.ShowDiagnostics)) { + logSettingsEvent(Settings.ShowDiagnostics); + getSettingValue(Settings.ShowDiagnostics) ? processAllFiles() : clearAllDiagnostics(); + } + if (eventAffectsSetting(event, Settings.DevMode)) { + logSettingsEvent(Settings.DevMode); + getSettingValue(Settings.DevMode) ? initDevMode() : clearDevModeOutput(); + } +} diff --git a/src/core/highlights.ts b/src/core/highlights.ts new file mode 100644 index 0000000..3f6a199 --- /dev/null +++ b/src/core/highlights.ts @@ -0,0 +1,115 @@ +import { Position, Range, type TextEditor } from 'vscode'; +import { DecorationRangeBehavior, window } from "vscode"; +import { getAllInterpolationRanges, getAllMatches, getAllOperatorTokens, getAllParsedWords, getAllStringRanges } from '../cache/activeFileCache'; +import { isDevMode } from './devMode'; + +const matchDecoration = window.createTextEditorDecorationType({ + backgroundColor: 'rgba(80, 200, 120, 0.20)', + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +const wordDecoration = window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 80, 80, 0.20)', + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +const operatorDecoration = window.createTextEditorDecorationType({ + backgroundColor: 'rgba(160, 80, 255, 0.25)', + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +const stringDecoration = window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 170, 60, 0.20)', + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +enum HighlightMode { + Disabled = 'disabled', + Matches = 'matches', + AllWords = 'allWords', +} + +export function rebuildHighlights(): void { + if (isDevMode()) { + const editor = window.activeTextEditor; + if (!editor) return; + buildHighlights(editor); + } +} + +function buildHighlights(editor: TextEditor, mode = HighlightMode.AllWords) { + editor.setDecorations(matchDecoration, []); + editor.setDecorations(wordDecoration, []); + editor.setDecorations(operatorDecoration, []); + editor.setDecorations(stringDecoration, []); + switch (mode) { + case HighlightMode.Matches: + editor.setDecorations(matchDecoration, getMatchRanges()); + break; + case HighlightMode.AllWords: + editor.setDecorations(matchDecoration, getMatchRanges()); + editor.setDecorations(wordDecoration, getWordRanges()); + editor.setDecorations(operatorDecoration, getOperatorTokenRanges()); + editor.setDecorations(stringDecoration, getStringRanges()); + break; + } +} + +function getMatchRanges(): Range[] { + return getAllMatches().map(match => new Range(new Position(match.context.line.number, match.context.word.start), new Position(match.context.line.number, match.context.word.end + 1))); +} + +function getWordRanges(): Range[] { + const matchRanges = getMatchRanges(); + const wordRanges: Range[] = []; + getAllParsedWords().forEach((parsedLineWords, lineNum) => { + parsedLineWords.forEach(word => wordRanges.push(new Range(new Position(lineNum, word.start), new Position(lineNum, word.end + 1)))); + }); + return wordRanges.filter(range => !matchRanges.some(match => match.intersection(range))); +} + +function getOperatorTokenRanges(): Range[] { + const operatorRanges: Range[] = []; + getAllOperatorTokens().forEach((operatorTokens, lineNum) => { + operatorTokens.forEach(operator => operatorRanges.push(new Range(new Position(lineNum, operator.index), new Position(lineNum, operator.index + operator.token.length)))); + }); + return operatorRanges; +} + +function getStringRanges(): Range[] { + const stringRanges: Range[] = []; + const interpolationRanges = getAllInterpolationRanges(); + getAllStringRanges().forEach((strings, lineNum) => { + const interp = interpolationRanges.get(lineNum) ?? []; + strings.forEach(stringRange => { + const segments = subtractRanges(stringRange, interp); + segments.forEach(segment => { + stringRanges.push(new Range(new Position(lineNum, segment.start), new Position(lineNum, segment.end + 1))); + }); + }); + }); + return stringRanges; +} + +function subtractRanges(base: { start: number; end: number; }, exclusions: { start: number; end: number; }[]): { start: number; end: number; }[] { + let segments: { start: number; end: number; }[] = [base]; + for (const exclusion of exclusions) { + segments = segments.flatMap(segment => subtractSingleRange(segment, exclusion)); + if (segments.length === 0) break; + } + return segments; +} + +function subtractSingleRange(base: { start: number; end: number; }, exclusion: { start: number; end: number; }): { start: number; end: number; }[] { + if (exclusion.end < base.start || exclusion.start > base.end) { + return [base]; + } + const result: { start: number; end: number; }[] = []; + if (exclusion.start > base.start) { + result.push({ start: base.start, end: exclusion.start - 1 }); + } + if (exclusion.end < base.end) { + result.push({ start: exclusion.end + 1, end: base.end }); + } + return result; +} diff --git a/src/core/manager.ts b/src/core/manager.ts new file mode 100644 index 0000000..3bbeda8 --- /dev/null +++ b/src/core/manager.ts @@ -0,0 +1,200 @@ +import type { Uri, ExtensionContext } from "vscode"; +import type { MatchResult, ParsedFile } from "../types"; +import { ProgressLocation, window, workspace } from "vscode"; +import { getActiveFile, getFileText, isActiveFile } from "../utils/fileUtils"; +import { matchFile } from "../matching/matchingEngine"; +import { clear as clearActiveFileCache, init as initActiveFilecache } from "../cache/activeFileCache"; +import { clearAllDiagnostics, clearFileDiagnostics, handleFileUpdate, rebuildFileDiagnostics, registerDiagnostics } from "./diagnostics"; +import { rebuildSemanticTokens } from "../provider/semanticTokensProvider"; +import { rebuildHighlights } from "./highlights"; +import { clearFile as clearIdentifierFile, clear as clearIdentifierCache, getFileIdentifiers } from '../cache/identifierCache'; +import { clear as clearProjectFilesCache, rebuild as rebuildProjectFilesCache } from '../cache/projectFilesCache'; +import { registerCommands } from "./commands"; +import { registerEventHandlers } from "./eventHandlers"; +import { registerProviders } from "./providers"; +import { parseFile } from "../parsing/fileParser"; +import { findFileExceptionWords } from "../parsing/wordExceptions"; +import { isDevMode, logFileRebuild, rebuildMetrics, registerDevMode, reportRebuildMetrics } from "./devMode"; +import { clear as clearIdCache, clearAll as clearAllIds } from "../cache/idCache"; +import { clear as clearMap, handleMapFileOpened, isMapFile } from "./mapManager"; +import { getAllMatchTypes } from "../matching/matchType"; + +export function initializeExtension(context: ExtensionContext) { + buildMonitoredFileTypes(); + buildAllFileTypes(); + + registerDiagnostics(context); + registerCommands(context); + registerEventHandlers(context); + registerProviders(context); + registerDevMode(); + + void rebuildProjectFilesCache(); + void processAllFiles(); + + context.subscriptions.push({ dispose: () => dispose() }); +} + +/** + * Process all files and rebuild all caches in the project + */ +export function processAllFiles() { + // Since it takes a while, show the progress notification + window.withProgress({ + location: ProgressLocation.Notification, + title: "Runescript Extension: Scanning files and building the cache...", + cancellable: false + }, async () => { + clearAll(); + await rebuildProjectFilesCache(); + await rebuildAllFiles(); + void rebuildActiveFile(); + reportRebuildMetrics(); + }); +} + +/** + * Add a file to get rebuilt into the rebuild file queue + */ +let rebuildFileQueue = Promise.resolve(); +export function queueFileRebuild(uri: Uri, fileText: string[], parsedFile: ParsedFile, quiet = false): Promise { + rebuildFileQueue = rebuildFileQueue.then(() => rebuildFile(uri, fileText, parsedFile, quiet)); + return rebuildFileQueue; +} + +/** + * Read, parse, match, and cache items from all relevant files in the project workspace + * @param recordMetrics Whether or not to record metrics related to the rebuild + */ +async function rebuildAllFiles(recordMetrics = isDevMode()): Promise { + // Get all the relevant files to read/parse + const fileTypesToScan = Array.from(monitoredFileTypes, fileType => `**/*.${fileType}`); + const uris = await workspace.findFiles(`{${[...fileTypesToScan].join(',')}}`); + if (recordMetrics) rebuildMetrics.fileCount = uris.length; + if (recordMetrics) rebuildMetrics.wordCount = 0; + + // Read and parse all of the relevant project files + let startTime = performance.now(); + type File = { uri: Uri; lines: string[]; parsedFile?: ParsedFile }; + const files: File[] = await Promise.all(uris.map(async uri => ({ uri: uri, lines: await getFileText(uri) }))); + if (recordMetrics) rebuildMetrics.fileReadDuration = performance.now() - startTime; + + // Scan the files for exception words + startTime = performance.now(); + files.forEach(file => findFileExceptionWords(file.lines)); + if (recordMetrics) rebuildMetrics.exceptionWordScanDuration = performance.now() - startTime; + + // Parse the files into words with deeper parsing context + startTime = performance.now(); + files.forEach(file => file.parsedFile = parseFile(file.uri, file.lines, true)); + if (recordMetrics) rebuildMetrics.fileParsingDuration = performance.now() - startTime; + + // First pass => finds all the declarations & exception words so second pass will be complete + startTime = performance.now(); + for (const file of files) { + initActiveFilecache(file.uri, file.parsedFile!); + matchFile(file.uri, file.parsedFile!, file.lines, true); + if (recordMetrics) rebuildMetrics.wordCount += [...file.parsedFile!.parsedWords!.values()].reduce((sum, words) => sum + words.length, 0); + } + if (recordMetrics) rebuildMetrics.firstPassDuration = performance.now() - startTime; + + // Second pass => now that the declarations and exception words are known full matching can be done + startTime = performance.now(); + for (const file of files) { + initActiveFilecache(file.uri, file.parsedFile!); + const matchResults = matchFile(file.uri, file.parsedFile!, file.lines, false); + await rebuildFileDiagnostics(file.uri, matchResults); + } + if (recordMetrics) rebuildMetrics.secondPassDuration = performance.now() - startTime; +} + +/** + * Rebuild a file: clear all cache data related to the file, parse & match it, build diagnostics, + * build semantic tokens and highlights if active file + * @param uri Uri of the file getting rebuilt + * @param lines Text of the file getting rebuilt + */ +async function rebuildFile(uri: Uri, lines: string[], parsedFile: ParsedFile, quiet = false): Promise { + if (!parsedFile) return; + const startTime = performance.now(); + const startIdentifiers = getFileIdentifiers(uri); + clearFile(uri); + initActiveFilecache(uri, parsedFile); + const fileMatches: MatchResult[] = matchFile(uri, parsedFile, lines, false); + await rebuildFileDiagnostics(uri, fileMatches); + handleFileUpdate(startIdentifiers, getFileIdentifiers(uri)); + if (isActiveFile(uri)) { + rebuildSemanticTokens(); + rebuildHighlights(); + } + if (!quiet) logFileRebuild(startTime, uri, fileMatches); +} + +/** + * Rebuilds the active/viewed file + */ +async function rebuildActiveFile(): Promise { + const activeFile = getActiveFile(); + if (activeFile) { + if (isMapFile(activeFile)) return handleMapFileOpened(await workspace.openTextDocument(activeFile)); + const fileText = await getFileText(activeFile); + void queueFileRebuild(activeFile, fileText, parseFile(activeFile, fileText)); + } +} + +/** + * Clear all of the caches + */ +export function clearAll() { + clearIdentifierCache(); + clearAllDiagnostics(); + clearActiveFileCache(); + clearAllIds(); + clearMap(); +} + +/** + * Dispose the caches + */ +function dispose() { + clearAll(); + clearProjectFilesCache(); +} + +/** + * Clear all of the caches relevant to a file + * @param uri the file to be cleared + */ +export function clearFile(uri: Uri) { + clearIdentifierFile(uri); + clearFileDiagnostics(uri); + clearIdCache(uri); +} + +/** +* Files which this extension is interested in +*/ +export const monitoredFileTypes = new Set(); +function buildMonitoredFileTypes(): void { + monitoredFileTypes.add('pack'); + getAllMatchTypes().filter(match => !match.referenceOnly).forEach(match => { + const fileTypes = match.fileTypes || []; + for (const fileType of fileTypes) { + monitoredFileTypes.add(fileType); + } + }); +} + +/** +* Files which this extension is interested in +*/ +export const allFileTypes = new Set(); +function buildAllFileTypes(): void { + allFileTypes.add('pack'); + getAllMatchTypes().forEach(match => { + const fileTypes = match.fileTypes || []; + for (const fileType of fileTypes) { + allFileTypes.add(fileType); + } + }); +} diff --git a/src/core/mapManager.ts b/src/core/mapManager.ts new file mode 100644 index 0000000..1cccbfc --- /dev/null +++ b/src/core/mapManager.ts @@ -0,0 +1,335 @@ +import { DecorationRangeBehavior, Diagnostic, DiagnosticSeverity, Position, Range, window, Uri } from "vscode"; +import type { DecorationOptions, TextDocument, TextDocumentChangeEvent, TextEditor } from "vscode"; +import { parseMapFile, type MapEntry, type MapParseError, type MapParseResult, type MapEntryKind } from "../parsing/mapParser"; +import { get as getIdName } from "../cache/idCache"; +import { LOC, NPC, OBJ } from "../matching/matchType"; +import { getLines } from "../utils/stringUtils"; +import { getByKey as getIdentifierByKey } from "../cache/identifierCache"; +import type { Identifier } from "../types"; +import { clearFileDiagnostics, setCustomDiagnostics } from "./diagnostics"; +import { Events, logEvent, logMapFileProcessed } from "./devMode"; +import { getFileName } from "../utils/fileUtils"; + +export function isMapFile(uri: Uri) { + return uri.fsPath.endsWith('.jm2'); +} + +let activeMapFile: string | undefined; +let activeChunkX: number | undefined; +let activeChunkZ: number | undefined; +let entriesByLine = new Map(); +let errorsByLine = new Map(); +let sectionsByLine = new Map(); +let pendingEditTimer: NodeJS.Timeout | undefined; +let pendingEditDocument: TextDocument | undefined; +let pendingStartLine = Number.MAX_SAFE_INTEGER; +let pendingEndLine = 0; + +const MAP_EDIT_DEBOUNCE_MS = 150; + +const mapDecoration = window.createTextEditorDecorationType({ + after: { + color: 'rgba(160, 160, 160, 0.75)', + margin: '0 0 0 1.5em', + fontStyle: 'italic' + }, + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +export function getIdentifierAtPosition(position: Position): Identifier | undefined { + const entry = entriesByLine.get(position.line); + if (!entry) return undefined; + const { start, end } = entry.idRange; + if (position.line !== start.line) return; + if (position.character < start.character || position.character >= end.character) return; + const type = entry.kind.toUpperCase(); + const name = getIdName(type, String(entry.id)); + if (!name) return undefined; + return getIdentifierByKey(name + type); +} + +export function getMapSectionHeaders(document: TextDocument): Array<{ line: number; name: string }> { + if (activeMapFile !== document.uri.fsPath) return []; + const sections: Array<{ line: number; name: string }> = []; + for (const [line, name] of sectionsByLine) { + sections.push({ line, name }); + } + return sections.sort((a, b) => a.line - b.line); +} + + +export function handleMapFileOpened(document: TextDocument) { + logEvent(Events.MapFileOpened, () => `on map file ${getFileName(document.uri)}`); + const start = performance.now(); + clear(); + activeMapFile = document.uri.fsPath; + const chunk = parseChunkFromPath(activeMapFile); + activeChunkX = chunk?.x; + activeChunkZ = chunk?.z; + const lines = getLines(document.getText()); + const result = parseMapFile(lines); + indexResult(result, 0); + applyTextDecorations(document); + applyDiagnostics(); + logMapFileProcessed(start, document.uri, lines.length, false); +} + +function applyTextDecorations(document?: TextDocument) { + const editor = document ? findEditorForDocument(document) : window.activeTextEditor; + if (!editor || activeMapFile !== editor.document.uri.fsPath) { + if (editor) editor.setDecorations(mapDecoration, []); + return; + } + editor.setDecorations(mapDecoration, buildDecorations(editor.document)); +} + +function applyDiagnostics() { + const editor = window.activeTextEditor; + if (!editor || activeMapFile !== editor.document.uri.fsPath) return; + const diagnosticsList: Diagnostic[] = []; + for (const entry of entriesByLine.values()) { + const matchId = entry.kind === 'npc' ? NPC.id : entry.kind === 'loc' ? LOC.id : OBJ.id; + const name = getIdName(matchId, entry.id.toString()); + if (name) continue; + const diag = new Diagnostic(entry.idRange, `${matchId} id ${entry.id} not found`, DiagnosticSeverity.Warning); + diag.source = 'map'; + diagnosticsList.push(diag); + } + setCustomDiagnostics(editor.document.uri, diagnosticsList); +} + +export function handleMapFileEdited(changeEvent: TextDocumentChangeEvent) { + const document = changeEvent.document; + logEvent(Events.MapFileEdited, () => `on map file ${getFileName(document.uri)}`); + if (!activeMapFile && isMapFile(document.uri)) { + handleMapFileOpened(document); + return; + } + if (activeMapFile !== document.uri.fsPath) return; + if (pendingEditDocument && pendingEditDocument.uri.fsPath !== document.uri.fsPath) { + flushPendingEdits(); + } + pendingEditDocument = document; + const { startLine, endLine } = getChangedLineRange(changeEvent); + pendingStartLine = Math.min(pendingStartLine, startLine); + pendingEndLine = Math.max(pendingEndLine, endLine); + + if (pendingEditTimer) clearTimeout(pendingEditTimer); + pendingEditTimer = setTimeout(() => flushPendingEdits(), MAP_EDIT_DEBOUNCE_MS); +} + +export function handleMapFileClosed() { + if (activeMapFile) clearFileDiagnostics(Uri.file(activeMapFile)); + clear(); +} + +export function clear() { + activeMapFile = undefined; + activeChunkX = undefined; + activeChunkZ = undefined; + entriesByLine = new Map(); + errorsByLine = new Map(); + sectionsByLine = new Map(); + if (pendingEditTimer) { + clearTimeout(pendingEditTimer); + pendingEditTimer = undefined; + } + pendingEditDocument = undefined; + pendingStartLine = Number.MAX_SAFE_INTEGER; + pendingEndLine = 0; +} + +function getChangedLineRange(changeEvent: TextDocumentChangeEvent): { startLine: number; endLine: number } { + let startLine = Number.MAX_SAFE_INTEGER; + let endLine = 0; + for (const change of changeEvent.contentChanges) { + startLine = Math.min(startLine, change.range.start.line); + const addedLines = change.text.split(/\r?\n/).length - 1; + const changeEndLine = Math.max(change.range.end.line, change.range.start.line + addedLines); + endLine = Math.max(endLine, changeEndLine); + } + if (startLine === Number.MAX_SAFE_INTEGER) startLine = 0; + return { startLine, endLine }; +} + +function flushPendingEdits() { + if (!pendingEditDocument || activeMapFile !== pendingEditDocument.uri.fsPath) { + pendingEditDocument = undefined; + pendingStartLine = Number.MAX_SAFE_INTEGER; + pendingEndLine = 0; + pendingEditTimer = undefined; + return; + } + const startLine = pendingStartLine === Number.MAX_SAFE_INTEGER ? 0 : pendingStartLine; + const endLine = pendingEndLine; + pendingStartLine = Number.MAX_SAFE_INTEGER; + pendingEndLine = 0; + pendingEditTimer = undefined; + applyIncrementalParse(pendingEditDocument, startLine, endLine); +} + +function applyIncrementalParse(document: TextDocument, startLine: number, endLine: number) { + const start = performance.now(); + const lines = getLines(document.getText()); + const bounds = findSectionBounds(lines, startLine, endLine); + if (!bounds) return; + const { sectionStart, sectionEnd } = bounds; + const slice = lines.slice(sectionStart, sectionEnd + 1); + const result = parseMapFile(slice); + replaceRange(sectionStart, sectionEnd, result); + applyTextDecorations(document); + applyDiagnostics(); + logMapFileProcessed(start, document.uri, sectionEnd - sectionStart, true); +} + +function findSectionBounds(lines: string[], startLine: number, endLine: number): { sectionStart: number; sectionEnd: number } | undefined { + const headerRegex = /^====\s*(\w+)\s*====\s*$/; + let sectionStart = -1; + let sectionKind: MapEntryKind | undefined; + for (let i = Math.min(startLine, lines.length - 1); i >= 0; i--) { + const match = headerRegex.exec(lines[i] ?? ''); + if (match) { + const name = match[1]?.toLowerCase(); + if (name === 'loc' || name === 'npc' || name === 'obj') { + sectionStart = i; + sectionKind = name; + } + break; + } + } + if (sectionStart < 0) { + for (let i = Math.max(0, startLine); i < lines.length; i++) { + const match = headerRegex.exec(lines[i] ?? ''); + if (match) { + const name = match[1]?.toLowerCase(); + if (name === 'loc' || name === 'npc' || name === 'obj') { + sectionStart = i; + sectionKind = name; + break; + } + } + } + } + if (sectionStart < 0 || !sectionKind) return undefined; + let sectionEnd = lines.length - 1; + for (let i = Math.min(endLine + 1, lines.length - 1); i < lines.length; i++) { + if (headerRegex.test(lines[i] ?? '')) { + sectionEnd = i - 1; + break; + } + } + return { sectionStart, sectionEnd }; +} + +function indexResult(result: MapParseResult, lineOffset: number) { + for (const entry of result.entries) { + const line = entry.line + lineOffset; + entriesByLine.set(line, offsetEntry(entry, lineOffset)); + } + for (const error of result.errors) { + const line = error.line + lineOffset; + errorsByLine.set(line, offsetError(error, lineOffset)); + } + for (const section of result.sections) { + const line = section.line + lineOffset; + sectionsByLine.set(line, section.name); + } +} + +function replaceRange(startLine: number, endLine: number, result: MapParseResult) { + for (let line = startLine; line <= endLine; line++) { + entriesByLine.delete(line); + errorsByLine.delete(line); + sectionsByLine.delete(line); + } + indexResult(result, startLine); +} + +function offsetEntry(entry: MapEntry, lineOffset: number): MapEntry { + return { + ...entry, + line: entry.line + lineOffset, + range: offsetRange(entry.range, lineOffset), + idRange: offsetRange(entry.idRange, lineOffset) + }; +} + +function offsetError(error: MapParseError, lineOffset: number): MapParseError { + return { + ...error, + line: error.line + lineOffset, + range: error.range ? offsetRange(error.range, lineOffset) : undefined + }; +} + +function offsetRange(range: Range, lineOffset: number): Range { + return new Range( + new Position(range.start.line + lineOffset, range.start.character), + new Position(range.end.line + lineOffset, range.end.character) + ); +} + +function buildDecorations(document: TextDocument): DecorationOptions[] { + const decorations: DecorationOptions[] = []; + for (const [line, entry] of entriesByLine) { + const lineText = document.lineAt(line).text; + const label = formatEntry(entry); + if (!label) continue; + const range = new Range(new Position(line, lineText.length), new Position(line, lineText.length)); + decorations.push({ + range, + renderOptions: { + after: { contentText: label } + } + }); + } + return decorations; +} + +function findEditorForDocument(document: TextDocument): TextEditor | undefined { + return window.visibleTextEditors.find(editor => editor.document.uri.fsPath === document.uri.fsPath); +} + +function formatEntry(entry: MapEntry): string { + const name = resolveName(entry); + const coord = formatCoord(entry); + switch (entry.kind) { + case 'obj': { + const quantity = entry.extras[0]; + const qtyText = quantity !== undefined ? `, quantity: ${quantity}` : ''; + return `OBJ: ${name} (coordinates: ${coord}${qtyText})`; + } + case 'npc': + return `NPC: ${name} (coordinates: ${coord})`; + case 'loc': { + const type = entry.extras[0]; + const rotation = entry.extras[1]; + const extraText = (type !== undefined || rotation !== undefined) + ? `, type: ${type ?? 'n/a'}, rotation: ${rotation ?? 'n/a'}` + : ''; + return `LOC: ${name} (coordinates: ${coord}${extraText})`; + } + } +} + +function resolveName(entry: MapEntry): string { + const matchId = entry.kind === 'npc' ? NPC.id : entry.kind === 'loc' ? LOC.id : OBJ.id; + return getIdName(matchId, entry.id.toString()) ?? 'Unknown'; +} + +function formatCoord(entry: MapEntry): string { + if (activeChunkX === undefined || activeChunkZ === undefined) { + return `${entry.level}_${entry.x}_${entry.z}`; + } + return `${entry.level}_${activeChunkX}_${activeChunkZ}_${entry.x}_${entry.z}`; +} + +function parseChunkFromPath(fsPath: string): { x: number; z: number } | undefined { + const baseName = fsPath.split(/[/\\]/).pop() ?? ''; + const match = /^m(\d+)_(\d+)\.jm2$/i.exec(baseName); + if (!match) return undefined; + const x = Number(match[1]); + const z = Number(match[2]); + if (!Number.isFinite(x) || !Number.isFinite(z)) return undefined; + return { x, z }; +} diff --git a/src/core/providers.ts b/src/core/providers.ts new file mode 100644 index 0000000..5842566 --- /dev/null +++ b/src/core/providers.ts @@ -0,0 +1,65 @@ +import type { ExtensionContext } from "vscode"; +import { languages } from "vscode"; +import { hoverProvider } from "../provider/hoverProvider"; +import { renameProvider } from "../provider/renameProvider"; +import { completionProvider, completionTriggers } from "../provider/completion/runescriptCompletionProvider"; +import { completionProvider as configCompletionProvider, completionTriggers as configCompletionTriggers } from "../provider/completion/configCompletionProvider"; +import { gotoDefinitionProvider } from "../provider/gotoDefinitionProvider"; +import { referenceProvider } from "../provider/referenceProvider"; +import { color24Provider } from "../provider/color/color24Provider"; +import { recolProvider } from "../provider/color/recolorProvider"; +import { signatureHelpProvider, signatureMetadata } from "../provider/signatureHelp/runescriptSignatureHelpProvider"; +import { configHelpProvider, configMetadata } from "../provider/signatureHelp/configSignatureHelpProvider"; +import { semanticTokensLegend, semanticTokensProvider } from "../provider/semanticTokensProvider"; +import { languageIds } from "../runescriptExtension"; +import { mapCodelensProvider } from "../provider/mapCodelensProvider"; + +export function registerProviders(context: ExtensionContext) { + for (const language of languageIds) { + registerUniversalProviders(language, context); + registerColorProviders(language, context); + registerSignatureHelpProviders(language, context); + registerCompletionProviders(language, context); + } + registerMapProviders(context); +} + +function registerMapProviders(context: ExtensionContext): void { + context.subscriptions.push(languages.registerHoverProvider('jm2', hoverProvider(context))); + context.subscriptions.push(languages.registerDefinitionProvider('jm2', gotoDefinitionProvider)); + context.subscriptions.push(languages.registerCodeLensProvider('jm2', mapCodelensProvider)); +} + +function registerUniversalProviders(language: string, context: ExtensionContext): void { + context.subscriptions.push( + languages.registerHoverProvider(language, hoverProvider(context)), + languages.registerRenameProvider(language, renameProvider), + languages.registerDefinitionProvider(language, gotoDefinitionProvider), + languages.registerReferenceProvider(language, referenceProvider), + languages.registerDocumentSemanticTokensProvider(language, semanticTokensProvider, semanticTokensLegend), + ); +} + +function registerColorProviders(language: string, context: ExtensionContext): void { + if (language === 'floconfig' || language === 'interface') { + context.subscriptions.push(languages.registerColorProvider(language, color24Provider)); + } else if (language.endsWith('config')) { + context.subscriptions.push(languages.registerColorProvider(language, recolProvider)); + } +} + +function registerSignatureHelpProviders(language: string, context: ExtensionContext): void { + if (language.endsWith('config') || language === 'interface') { + context.subscriptions.push(languages.registerSignatureHelpProvider(language, configHelpProvider, configMetadata)); + } else if (language === 'runescript') { + context.subscriptions.push(languages.registerSignatureHelpProvider(language, signatureHelpProvider, signatureMetadata)); + } +} + +function registerCompletionProviders(language: string, context: ExtensionContext): void { + if (language.endsWith('config') || language === 'interface') { + context.subscriptions.push(languages.registerCompletionItemProvider(language, configCompletionProvider, ...configCompletionTriggers)); + } else if (language === 'runescript') { + context.subscriptions.push(languages.registerCompletionItemProvider(language, completionProvider, ...completionTriggers)); + } +} diff --git a/src/core/settings.ts b/src/core/settings.ts new file mode 100644 index 0000000..2319e48 --- /dev/null +++ b/src/core/settings.ts @@ -0,0 +1,40 @@ +import type { ConfigurationChangeEvent } from "vscode"; +import { workspace } from "vscode"; + +interface Setting { + id: string; + getValue: () => T; +} + +export enum Settings { + ShowDiagnostics = 'enable diagnostics', + ShowHover = 'enable hover', + DevMode = 'enable dev mode', +} + +export function getSettingValue(setting: Settings): boolean { + return extensionSettings[setting].getValue(); +} + +export function getSettingId(setting: Settings): string { + return extensionSettings[setting].id; +} + +export function eventAffectsSetting(event: ConfigurationChangeEvent, setting: Settings): boolean { + return event.affectsConfiguration(getSettingId(setting)); +} + +const extensionSettings = { + [Settings.ShowDiagnostics]: { + id: 'runescript.diagnostics.enabled', + getValue: () => workspace.getConfiguration('runescript').get('diagnostics.enabled', true) as boolean + }, + [Settings.ShowHover]: { + id: 'runescript.hover.enabled', + getValue: () => workspace.getConfiguration('runescript').get('hover.enabled', true) as boolean + }, + [Settings.DevMode]: { + id: 'runescript.devMode.enabled', + getValue: () => workspace.getConfiguration('runescript').get('devMode.enabled', false) as boolean + }, +} satisfies Record>; diff --git a/src/diagnostics/RunescriptDiagnostic.ts b/src/diagnostics/RunescriptDiagnostic.ts new file mode 100644 index 0000000..fb10f94 --- /dev/null +++ b/src/diagnostics/RunescriptDiagnostic.ts @@ -0,0 +1,9 @@ +import type { Diagnostic, Range, Uri } from 'vscode'; +import type { MatchResult } from '../types'; + +export abstract class RunescriptDiagnostic { + clearAll(): void { } + clearFile(_uri: Uri): void { } + abstract check(result: MatchResult): boolean; + abstract createDiagnostic(range: Range, result: MatchResult): Diagnostic; +} diff --git a/src/diagnostics/unknownFileDiagnostic.ts b/src/diagnostics/unknownFileDiagnostic.ts new file mode 100644 index 0000000..9e7c863 --- /dev/null +++ b/src/diagnostics/unknownFileDiagnostic.ts @@ -0,0 +1,55 @@ +import type { Range } from "vscode"; +import type { MatchResult } from "../types"; +import { Diagnostic, DiagnosticSeverity } from "vscode"; +import { RunescriptDiagnostic } from "./RunescriptDiagnostic"; +import { fileNamePostProcessor } from "../resource/postProcessors"; +import { exists as projectFileExists } from '../cache/projectFilesCache'; + +export class UnknownFileDiagnostic extends RunescriptDiagnostic { + // Tempoary holds file name between check() and create() calls + fileName: string = ''; + // For this file (key), here are all the diagnostics that look for it (uri + ranges) + cache: Map> = new Map(); + + check(result: MatchResult): boolean { + if (result.context.matchType.postProcessor !== fileNamePostProcessor) return false; + this.fileName = resultToFileKey(result); + return !projectFileExists(this.fileName) + } + + createDiagnostic(range: Range, result: MatchResult): Diagnostic { + this.cacheDiagnostic(range, this.fileName, result.context.uri.fsPath); + return this.create(range, this.fileName); + } + + createByFileKey(range: Range, fileKey: string, fsPath: string): Diagnostic { + this.cacheDiagnostic(range, fileKey, fsPath); + return this.create(range, fileKey); + } + + create(range: Range, fileKey: string) { + return new Diagnostic(range, `Refers to file ${fileKey}, but it doesn't exist`, DiagnosticSeverity.Warning); + } + + cacheDiagnostic(range: Range, fileKey: string, fsPath: string) { + const fileDiagnostics = this.cache.get(fileKey) ?? new Map(); + const diagnostics = fileDiagnostics.get(fsPath) ?? []; + diagnostics.push(range); + fileDiagnostics.set(fsPath, diagnostics); + this.cache.set(fileKey, fileDiagnostics); + } + + getDiagnosticsForFile(fileKey: string): Map { + return this.cache.get(fileKey) ?? new Map(); + } + + clearUnknowns(fileKey: string): Map { + const diagnosticsForFile = this.cache.get(fileKey); + this.cache.delete(fileKey); + return diagnosticsForFile ?? new Map(); + } +} + +function resultToFileKey(result: MatchResult) { + return `${result.word}.${(result.context.matchType.fileTypes || [])[0] ?? 'rs2'}`; +} diff --git a/src/diagnostics/unknownIdentifierDiagnostic.ts b/src/diagnostics/unknownIdentifierDiagnostic.ts new file mode 100644 index 0000000..cab26f6 --- /dev/null +++ b/src/diagnostics/unknownIdentifierDiagnostic.ts @@ -0,0 +1,57 @@ +import { Diagnostic, DiagnosticSeverity, type Range, type Uri } from "vscode"; +import type { Identifier, IdentifierKey, MatchResult } from "../types"; +import { RunescriptDiagnostic } from "./RunescriptDiagnostic"; +import { get as getIdentifier } from "../cache/identifierCache"; +import { getFullName, resolveIdentifierKey } from "../utils/cacheUtils"; + +export class UnknownIdentifierDiagnostic extends RunescriptDiagnostic { + /** + * Cache the diagnostics by identifierKey, value is a map keyed by URI and range of references in that URI + */ + cache: Map> = new Map(); + + clearAll(): void { + this.cache.clear(); + } + + clearFile(uri: Uri): void { + for (const [key, uris] of this.cache) { + uris.delete(uri.fsPath); + if (uris.size === 0) this.cache.delete(key); + } + } + + check(result: MatchResult): boolean { + const identifier: Identifier | undefined = getIdentifier(result.word, result.context.matchType); + if (!identifier) return false; + return !result.context.matchType.referenceOnly && !result.context.declaration && !identifier.declaration; + } + + createDiagnostic(range: Range, result: MatchResult): Diagnostic { + this.cacheDiagnostic(range, resolveIdentifierKey(result.context.word.value, result.context.matchType), result.context.uri.fsPath); + return this.create(range, result.context.matchType.id, result.word); + } + + createByRangeIden(range: Range, iden: Identifier, fsPath: string): Diagnostic { + this.cacheDiagnostic(range, iden.cacheKey, fsPath); + return this.create(range, iden.matchId, getFullName(iden)); + } + + create(range: Range, matchTypeId: string, name: string): Diagnostic { + return new Diagnostic(range, `Unknown ${matchTypeId.toLowerCase()}: ${name}`, DiagnosticSeverity.Warning); + } + + cacheDiagnostic(range: Range, idenKey: string, fsPath: string) { + const idenDiagnostics = this.cache.get(idenKey) ?? new Map(); + const fileIdenDiags = idenDiagnostics.get(fsPath) ?? []; + fileIdenDiags.push(range); + idenDiagnostics.set(fsPath, fileIdenDiags); + this.cache.set(idenKey, idenDiagnostics); + } + + clearUnknowns(identifierKey: IdentifierKey): Map | undefined { + const cached = this.cache.get(identifierKey); + this.cache.delete(identifierKey); + return cached; + } +} diff --git a/src/enum/hoverDisplayItems.ts b/src/enum/hoverDisplayItems.ts new file mode 100644 index 0000000..9ecd0cc --- /dev/null +++ b/src/enum/hoverDisplayItems.ts @@ -0,0 +1,20 @@ +// In order for a display item to be shown in hover texts, the matchType to which the identifier belongs to +// must define a declaration or reference config which includes the desired hoverDisplay item in its displayItems array +// Note: in order to get identifier.value to display you must define a custom postProcessor for the matchType which +// populates identifier.value, there is no default value parsing like there is with the others + +export const TITLE = 'title'; +export const INFO = 'info'; +export const VALUE = 'value'; +export const SIGNATURE = 'signature'; +export const CODEBLOCK = 'codeblock'; + +export const hoverDisplay = { + TITLE, // hover text title display : fileType.png matchType.id identifier.name + INFO, // hover text info display : identifier.info (in italics) + VALUE, // hover text value display : identifier.value (plain text) + SIGNATURE, // signature display : identifier.params
identifier.returns (in code syntax) + CODEBLOCK // block display : identifier.block (in code syntax) +} as const; + +export type HoverDisplayItem = typeof hoverDisplay[keyof typeof hoverDisplay]; diff --git a/src/enum/regex.ts b/src/enum/regex.ts new file mode 100644 index 0000000..3539cb5 --- /dev/null +++ b/src/enum/regex.ts @@ -0,0 +1,24 @@ +export const COORD_REGEX = /(\d+_){4}\d+/; +export const COLOR_REGEX = /\d{6}/; +export const KEYWORD_REGEX = /\b(?:if|while|for|return|else|case)\b/; +export const BOOLEAN_REGEX = /\b(?:true|false)\b/; +export const TYPE_REGEX = /\b(?:def_)?(?:int|string|boolean|seq|locshape|component|idk|midi|npc_mode|namedobj|synth|stat|npc_stat|fontmetrics|enum|loc|model|npc|obj|player_uid|spotanim|npc_uid|inv|category|struct|dbrow|interface|dbtable|coord|mesanim|param|queue|weakqueue|timer|softtimer|char|dbcolumn|proc|label)\b/; +export const ALPHA_NUMERIC = /^[a-z0-9]$/; +export const RECOLOR_REGEX = /(recol[1-6][sd])=(\d+)/g; +export const NUMBER_REGEX = /^(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?)$/; +export const END_OF_BLOCK_REGEX = /(\r\n|\r|\n)(\[.+|val=.+|\^.+|\d+=.+)(?:$|(\r\n|\r|\n))/; +export const END_OF_BLOCK_LINE_REGEX = /^(\[|\^|\d+=)/; +export const START_OF_LINE_REGEX = /(?<=[\n])(?!.*[\n]).*/; +export const END_OF_LINE_REGEX = /\r\n|\r|\n/; +export const WORD_REGEX = /(\.\w+)|(\w+:\w+)|([^\`\~\!\@\#\%\^\&\*\(\)\-\$\=\+\[\{\]\}\\\|\;\:\'\\"\,\.\<\>\/\?\s]+)/g; +export const LOCAL_VAR_WORD_REGEX = /(\$\w+)|(\.\w+)|(\w+:\w+)|([^\`\~\!\@\#\%\^\&\*\(\)\-\$\=\+\[\{\]\}\\\|\;\:\'\\"\,\.\<\>\/\?\s]+)/g; +export const CONFIG_LINE_REGEX = /^\w+=.+$/; +export const CONFIG_DECLARATION_REGEX = /\[[^\]]+\]/; +export const TRIGGER_LINE_REGEX = /\[\w+,(\.)?[\w*-]+(:[\w*-]+)?\]/; +export const TRIGGER_DEFINITION_REGEX = /\[.+,.+\](\([\w, :\.$]*\))?(\([\w, :\.$]*\))?/; +export const INFO_MATCHER_REGEX = /\/\/[ ]{0,1}(desc|info):(.+)/; +export const SWITCH_TYPE_REGEX = /switch_[a-z]+/; +export const SWITCH_CASE_REGEX = /\s*case.+ :/; +export const COLOR24_REGEX = /(colour|mapcolour|activecolour|overcolour|activeovercolour)=(\w+)/g; +export const LOC_MODEL_REGEX = /^(?!model_[a-z0-9]$)\w+_[a-z0-9]\b/; +export const QUEUE_REGEX = /^\.?\w*queue\*?$/; diff --git a/src/enum/semanticTokens.ts b/src/enum/semanticTokens.ts new file mode 100644 index 0000000..da253f0 --- /dev/null +++ b/src/enum/semanticTokens.ts @@ -0,0 +1,24 @@ +export enum SemanticTokenType { + Namespace = 'namespace', + Class = 'class', + Enum = 'enum', + Interface = 'interface', + Struct = 'struct', + TypeParameter = 'typeParameter', + Type = 'type', + Parameter = 'parameter', + Variable = 'variable', + Property = 'property', + EnumMember = 'enumMember', + Event = 'event', + Function = 'function', + Method = 'method', + Macro = 'macro', + Keyword = 'keyword', + Modifier = 'modifier', + Comment = 'comment', + String = 'string', + Number = 'number', + Regexp = 'regexp', + Operator = 'operator' +} diff --git a/src/matching/matchType.ts b/src/matching/matchType.ts new file mode 100644 index 0000000..c797a44 --- /dev/null +++ b/src/matching/matchType.ts @@ -0,0 +1,250 @@ +import type { MatchType } from '../types'; +import { globalVarPostProcessor, enumPostProcessor, columnPostProcessor, rowPostProcessor, componentPostProcessor, + fileNamePostProcessor, coordPostProcessor, configKeyPostProcessor, triggerPostProcessor, categoryPostProcessor, + paramPostProcessor, + localVarPostProcessor} from '../resource/postProcessors'; +import { CODEBLOCK, INFO, SIGNATURE, TITLE, VALUE } from "../enum/hoverDisplayItems"; +import { SemanticTokenType } from '../enum/semanticTokens'; + +const matchTypesById = new Map(); + +function defineMatchType(match: MatchType): MatchType { + matchTypesById.set(match.id, match); + return match; +} + +export const LOCAL_VAR: MatchType = defineMatchType({ + id: 'LOCAL_VAR', types: [], fileTypes: ['rs2'], cache: false, allowRename: true, + hoverConfig: { declarationItems: [TITLE, CODEBLOCK], referenceItems: [TITLE, CODEBLOCK], language: 'runescript', blockSkipLines: 0 }, + postProcessor: localVarPostProcessor +}); + +export const GLOBAL_VAR: MatchType = defineMatchType({ + id: 'GLOBAL_VAR', types: ['var'], fileTypes: ['varp', 'varbit', 'vars', 'varn'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'varpconfig' }, + postProcessor: globalVarPostProcessor +}); + +export const CONSTANT: MatchType = defineMatchType({ + id: 'CONSTANT', types: [], fileTypes: ['constant'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'constants', blockSkipLines: 0 }, +}); + +export const LABEL: MatchType = defineMatchType({ + id: 'LABEL', types: ['label'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const PROC: MatchType = defineMatchType({ + id: 'PROC', types: ['proc'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const TIMER: MatchType = defineMatchType({ + id: 'TIMER', types: ['timer'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const SOFTTIMER: MatchType = defineMatchType({ + id: 'SOFTTIMER', types: ['softtimer'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const QUEUE: MatchType = defineMatchType({ + id: 'QUEUE', types: ['queue'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const SEQ: MatchType = defineMatchType({ + id: 'SEQ', types: ['seq'], fileTypes: ['seq'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO], language: 'seqconfig' }, +}); + +export const SPOTANIM: MatchType = defineMatchType({ + id: 'SPOTANIM', types: ['spotanim'], fileTypes: ['spotanim'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO], language: 'spotanimconfig' }, +}); + +export const HUNT: MatchType = defineMatchType({ + id: 'HUNT', types: ['hunt'], fileTypes: ['hunt'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'huntconfig', configInclusions: ['type'] }, +}); + +export const LOC: MatchType = defineMatchType({ + id: 'LOC', types: ['loc'], fileTypes: ['loc'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'locconfig', configInclusions: ['name', 'desc', 'category'] }, + semanticTokenConfig: { declaration: SemanticTokenType.Function }, +}); + +export const NPC: MatchType = defineMatchType({ + id: 'NPC', types: ['npc'], fileTypes: ['npc'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'npcconfig', configInclusions: ['name', 'desc', 'category'] }, +}); + +export const OBJ: MatchType = defineMatchType({ + id: 'OBJ', types: ['namedobj', 'obj'], fileTypes: ['obj'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'objconfig', configInclusions: ['name', 'desc', 'category'] }, +}); + +export const INV: MatchType = defineMatchType({ + id: 'INV', types: ['inv'], fileTypes: ['inv'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'invconfig', configInclusions: ['scope', 'size'] }, + semanticTokenConfig: { declaration: SemanticTokenType.Function, reference: SemanticTokenType.Property }, +}); + +export const ENUM: MatchType = defineMatchType({ + id: 'ENUM', types: ['enum'], fileTypes: ['enum'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'enumconfig', configInclusions: ['inputtype', 'outputtype'] }, + postProcessor: enumPostProcessor +}); + +export const DBCOLUMN: MatchType = defineMatchType({ + id: 'DBCOLUMN', types: ['dbcolumn'], fileTypes: ['dbtable'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO, CODEBLOCK], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'runescript', blockSkipLines: 0 }, + postProcessor: columnPostProcessor +}); + +export const DBROW: MatchType = defineMatchType({ + id: 'DBROW', types: ['dbrow'], fileTypes: ['dbrow'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'dbrowconfig', configInclusions: ['table'] }, + semanticTokenConfig: { declaration: SemanticTokenType.Function }, + postProcessor: rowPostProcessor +}); + +export const DBTABLE: MatchType = defineMatchType({ + id: 'DBTABLE', types: ['dbtable'], fileTypes: ['dbtable'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'dbtableconfig' }, +}); + +export const INTERFACE: MatchType = defineMatchType({ + id: 'INTERFACE', types: ['interface'], fileTypes: ['if'], cache: true, allowRename: false, referenceOnly: true, + hoverConfig: { referenceItems: [TITLE, INFO], language: 'interface' }, + postProcessor: fileNamePostProcessor +}); + +export const COMPONENT: MatchType = defineMatchType({ + id: 'COMPONENT', types: ['component'], fileTypes: ['if'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO], language: 'interface' }, + postProcessor: componentPostProcessor +}); + +export const PARAM: MatchType = defineMatchType({ + id: 'PARAM', types: ['param'], fileTypes: ['param'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'paramconfig' }, + postProcessor: paramPostProcessor +}); + +export const COMMAND: MatchType = defineMatchType({ + id: 'COMMAND', types: [], fileTypes: ['rs2'], cache: true, allowRename: false, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, + semanticTokenConfig: { declaration: SemanticTokenType.Function, reference: SemanticTokenType.Function } +}); + +export const SYNTH: MatchType = defineMatchType({ + id: 'SYNTH', types: ['synth'], fileTypes: ['synth'], cache: true, allowRename: true, referenceOnly: true, renameFile: true, + hoverConfig: { referenceItems: [TITLE, INFO] }, + postProcessor: fileNamePostProcessor +}); + +// export const FRAME: MatchType = defineMatchType({ +// id: 'FRAME', types: ['frame'], fileTypes: ['frame'], cache: true, allowRename: true, referenceOnly: true, renameFile: true, +// hoverConfig: { referenceItems: [TITLE, INFO] }, +// postProcessor: fileNamePostProcessor +// }); + +export const MODEL: MatchType = defineMatchType({ + id: 'MODEL', types: ['ob2', 'model'], fileTypes: ['ob2'], cache: true, allowRename: true, referenceOnly: true, renameFile: true, + hoverConfig: { referenceItems: [TITLE, INFO] }, +}); + +export const WALKTRIGGER: MatchType = defineMatchType({ + id: 'WALKTRIGGER', types: ['walktrigger'], fileTypes: ['rs2'], cache: true, allowRename: true, callable: true, + hoverConfig: { declarationItems: [TITLE, INFO, SIGNATURE], referenceItems: [TITLE, INFO, SIGNATURE] }, +}); + +export const IDK: MatchType = defineMatchType({ + id: 'IDK', types: ['idk', 'idkit'], fileTypes: ['idk'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO, CODEBLOCK], language: 'idkconfig' }, +}); + +export const MESANIM: MatchType = defineMatchType({ + id: 'MESANIM', types: ['mesanim'], fileTypes: ['mesanim'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO], language: 'mesanimconfig' }, +}); + +export const STRUCT: MatchType = defineMatchType({ + id: 'STRUCT', types: ['struct'], fileTypes: ['struct'], cache: true, allowRename: true, + hoverConfig: { declarationItems: [TITLE, INFO], referenceItems: [TITLE, INFO], language: 'structconfig' }, +}); + +export const CATEGORY: MatchType = defineMatchType({ + id: 'CATEGORY', types: ['category'], cache: true, allowRename: true, referenceOnly: true, + hoverConfig: { referenceItems: [TITLE, VALUE] }, + postProcessor: categoryPostProcessor +}); + +// Hover only match types that are only used for displaying hover displays (no finding references/declarations) +// Useful for terminating word searches early when detected. Postprocessing can be done on these. +// Specify referenceConfig to select which displayItems should be shown on hover. +export const COORDINATES: MatchType = defineMatchType({ + id: 'COORDINATES', types: ['coord'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE, VALUE] }, + postProcessor: coordPostProcessor +}); + +export const CONFIG_KEY: MatchType = defineMatchType({ + id: 'CONFIG_KEY', types: [], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE, INFO] }, + postProcessor: configKeyPostProcessor +}); + +export const TRIGGER: MatchType = defineMatchType({ + id: 'TRIGGER', types: [], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE, INFO] }, + postProcessor: triggerPostProcessor +}); + +export const STAT: MatchType = defineMatchType({ + id: 'STAT', types: ['stat'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE] }, +}); + +export const NPC_STAT: MatchType = defineMatchType({ + id: 'NPC_STAT', types: ['npc_stat'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE] }, +}); + +export const NPC_MODE: MatchType = defineMatchType({ + id: 'NPC_MODE', types: ['npc_mode'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE] }, +}); + +export const LOCSHAPE: MatchType = defineMatchType({ + id: 'LOCSHAPE', types: ['locshape'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE] }, +}); + +export const FONTMETRICS: MatchType = defineMatchType({ + id: 'FONTMETRICS', types: ['fontmetrics'], cache: false, allowRename: false, + hoverConfig: { referenceItems: [TITLE] }, +}); + +// NOOP Match types that might get detected, but nothing is done with them (no hover display, no finding references/declarations) +// Useful for terminating word searching early when detected, and possibly doing something with them at a later date +export const UNKNOWN: MatchType = defineMatchType({ id: 'UNKNOWN', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); +export const SKIP: MatchType = defineMatchType({ id: 'SKIP', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); +export const NUMBER: MatchType = defineMatchType({ id: 'NUMBER', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); +export const KEYWORD: MatchType = defineMatchType({ id: 'KEYWORD', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); +export const TYPE: MatchType = defineMatchType({ id: 'TYPE', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); +export const BOOLEAN: MatchType = defineMatchType({ id: 'BOOLEAN', types: [], fileTypes: [], cache: false, allowRename: false, noop: true, comparisonType: 'boolean' }); +export const NULL: MatchType = defineMatchType({ id: 'NULL', types: [], fileTypes: [], cache: false, allowRename: false, noop: true }); + +function getMatchTypeById(id: string): MatchType | undefined { + return matchTypesById.get(id); +} + +function getAllMatchTypes(): MatchType[] { + return [...matchTypesById.values()]; +} + +export { getMatchTypeById, getAllMatchTypes, matchTypesById }; diff --git a/src/matching/matchers/booleanMatcher.ts b/src/matching/matchers/booleanMatcher.ts new file mode 100644 index 0000000..8d347ae --- /dev/null +++ b/src/matching/matchers/booleanMatcher.ts @@ -0,0 +1,16 @@ +import type { MatchContext, Matcher } from '../../types'; +import { BOOLEAN_REGEX } from "../../enum/regex"; +import { BOOLEAN } from "../matchType"; +import { reference } from "../../utils/matchUtils"; + +/** +* Looks for matches with direct word regex checks, such as for coordinates +*/ +function booleanMatcherFn(context: MatchContext): void { + const word = context.word.value; + if (BOOLEAN_REGEX.test(word)) { + return reference(BOOLEAN, context); + } +} + +export const booleanMatcher: Matcher = { priority: 11500, fn: booleanMatcherFn }; diff --git a/src/matching/matchers/columnDeclarationMatcher.ts b/src/matching/matchers/columnDeclarationMatcher.ts new file mode 100644 index 0000000..3196e1c --- /dev/null +++ b/src/matching/matchers/columnDeclarationMatcher.ts @@ -0,0 +1,12 @@ +import type { MatchContext, Matcher } from "../../types"; +import { declaration } from "../../utils/matchUtils"; +import { DBCOLUMN, SKIP } from "../matchType"; + +function columnDeclarationMatcherFn(context: MatchContext): void { + if (context.file.type === 'dbtable') { + if (context.word.index === 1) return declaration(DBCOLUMN, context); + if (context.word.index > 1) return declaration(SKIP, context); + } +} + +export const columnDeclarationMatcher: Matcher = { priority: 2000, fn: columnDeclarationMatcherFn}; diff --git a/src/matching/matchers/commandMatcher.ts b/src/matching/matchers/commandMatcher.ts new file mode 100644 index 0000000..f05f01b --- /dev/null +++ b/src/matching/matchers/commandMatcher.ts @@ -0,0 +1,23 @@ +import type { MatchContext, Matcher } from '../../types'; +import { get as getIdentifier } from "../../cache/identifierCache"; +import { COMMAND } from "../matchType"; +import { reference, declaration } from "../../utils/matchUtils"; +import { TRIGGER_LINE_REGEX } from "../../enum/regex"; + +/** +* Looks for matches of known engine commands +*/ +const commandMatcherFn = (context: MatchContext): void => { + const command = getIdentifier(context.word.value, COMMAND); + if (command) { + if (TRIGGER_LINE_REGEX.test(context.line.text)) { + if (context.word.index === 1) return declaration(COMMAND, context); + } + if (command.signature && command.signature.params.length > 0 && context.nextChar !== '('){ + return undefined; + } + reference(COMMAND, context); + } +} + +export const commandMatcher: Matcher = { priority: 8000, fn: commandMatcherFn }; diff --git a/src/matching/matchers/configDeclarationMatcher.ts b/src/matching/matchers/configDeclarationMatcher.ts new file mode 100644 index 0000000..f411054 --- /dev/null +++ b/src/matching/matchers/configDeclarationMatcher.ts @@ -0,0 +1,30 @@ +import { CONFIG_DECLARATION_REGEX } from "../../enum/regex"; +import type { MatchContext, Matcher } from "../../types"; +import { declaration } from "../../utils/matchUtils"; +import { COMPONENT, DBROW, DBTABLE, ENUM, GLOBAL_VAR, HUNT, IDK, INV, LOC, MESANIM, NPC, OBJ, PARAM, SEQ, SPOTANIM, STRUCT } from "../matchType"; + +function configDeclarationMatcherFn(context: MatchContext): void { + // Check for config file declarations (i.e. declarations with [NAME]) + if (context.file.type !== 'rs2' && context.line.text.startsWith('[') && CONFIG_DECLARATION_REGEX.test(context.line.text)) { + switch (context.file.type) { + case "varp": case "varbit": case "varn": case "vars": return declaration(GLOBAL_VAR, context); + case "obj": return declaration(OBJ, context); + case "loc": return declaration(LOC, context); + case "npc": return declaration(NPC, context); + case "param": return declaration(PARAM, context); + case "seq": return declaration(SEQ, context); + case "struct": return declaration(STRUCT, context); + case "dbrow": return declaration(DBROW, context); + case "dbtable": return declaration(DBTABLE, context); + case "enum": return declaration(ENUM, context); + case "hunt": return declaration(HUNT, context); + case "inv": return declaration(INV, context); + case "spotanim": return declaration(SPOTANIM, context); + case "idk": return declaration(IDK, context); + case "mesanim": return declaration(MESANIM, context); + case "if": return declaration(COMPONENT, context); + } + } +} + +export const configDeclarationMatcher: Matcher = { priority: 3000, fn: configDeclarationMatcherFn}; diff --git a/src/matching/matchers/configMatcher.ts b/src/matching/matchers/configMatcher.ts new file mode 100644 index 0000000..c507e9d --- /dev/null +++ b/src/matching/matchers/configMatcher.ts @@ -0,0 +1,66 @@ +import { ConfigVarArgSrc, learnConfigKey, getConfigData } from "../../resource/configKeys"; +import type { MatchContext, Matcher, Identifier, ConfigLineData } from '../../types'; +import { CONFIG_KEY, SKIP, getMatchTypeById } from "../matchType"; +import { reference } from "../../utils/matchUtils"; +import { dataTypeToMatchId } from "../../resource/dataTypeToMatchId"; +import { getBlockScopeIdentifier, getByLineIndex } from '../../cache/activeFileCache'; + +/** +* Looks for matches on config files, both config declarations and config line items +*/ +function configMatcherFn(context: MatchContext): void { + // Check if the line we are matching is a config line + getConfigLineMatch(context); +} + +export function getConfigLineMatch(context: MatchContext): ConfigLineData | undefined { + // If we are on the first word of a standard config key=val line, return the config_key match type + if (context.word.index === 0 && context.nextChar === '=') { + learnConfigKey(context.word.value); + reference(CONFIG_KEY, context); + return undefined; + } + + // At this point only words which have a configKey and paramIndex context are valid to continue + if (context.word.configKey === undefined || context.word.paramIndex === undefined) return undefined; + + // Get the configData from the configKeys static object [defined in configKeys.ts] + const configKey = context.word.configKey; + const paramIndex = context.word.paramIndex; + const configData = getConfigData(configKey, context.file.type); + if (!configData || (configData.ignoreValues ?? []).includes(context.word.value)) { + reference(SKIP, context); + return undefined; + } + + // If the configData has vararg params and the word index is on a vararg index, figure out the match type + if (configData.varArgs && context.word.paramIndex >= configData.varArgs.startIndex) { + // get varags source identifier + let iden: Identifier | undefined; + if (configData.varArgs.idenSrc === ConfigVarArgSrc.BlockName) { + iden = getBlockScopeIdentifier(context.line.number); + } + else if (configData.varArgs.idenSrc === ConfigVarArgSrc.FirstParam) { + iden = getByLineIndex(context.uri, context.line.number, context.words[1].start)?.identifier; + } + // get the param match types from the identifier signature + const varArgIndex = paramIndex - configData.varArgs.startIndex; + if (!iden?.signature?.params) return undefined; + const configLineData: ConfigLineData = { key: configKey, params: [...configData.params, ...iden.signature.params.map(p => p.type)], index: context.word.index }; + if (configData.varArgs.idenSrc === ConfigVarArgSrc.FirstParam) configLineData.params[0] = context.words[1].value; + const varArgParam = iden.signature.params[varArgIndex]; + if (!varArgParam) return configLineData; + reference(getMatchTypeById(dataTypeToMatchId(varArgParam.type)) ?? SKIP, context); + return configLineData; + } + + // Resolve the match type from the data type of the type at the param index + if (paramIndex < configData.params.length) { + const paramType = configData.params[paramIndex]; + const resolvedMatchType = getMatchTypeById(dataTypeToMatchId(paramType)) ?? SKIP; + reference(resolvedMatchType, context); + return { key: configKey, params: configData.params, index: context.word.index }; + } +} + +export const configMatcher: Matcher = { priority: 10000, fn: configMatcherFn }; diff --git a/src/matching/matchers/constMatcher.ts b/src/matching/matchers/constMatcher.ts new file mode 100644 index 0000000..552f1d9 --- /dev/null +++ b/src/matching/matchers/constMatcher.ts @@ -0,0 +1,9 @@ +import type { MatchContext, Matcher } from "../../types"; +import { declaration } from "../../utils/matchUtils"; +import { CONSTANT } from "../matchType"; + +function constDeclarationMatcherFn(context: MatchContext): void { + if (context.prevChar === '^' && context.file.type === "constant") declaration(CONSTANT, context); +} + +export const constDeclarationMatcher: Matcher = { priority: 4000, fn: constDeclarationMatcherFn}; diff --git a/src/matching/matchers/keyWordTypeMatcher.ts b/src/matching/matchers/keyWordTypeMatcher.ts new file mode 100644 index 0000000..85b8975 --- /dev/null +++ b/src/matching/matchers/keyWordTypeMatcher.ts @@ -0,0 +1,19 @@ +import type { MatchContext, Matcher } from '../../types'; +import { KEYWORD_REGEX, TYPE_REGEX } from "../../enum/regex"; +import { KEYWORD, TYPE } from "../matchType"; +import { reference } from "../../utils/matchUtils"; + +/** +* Looks for matches with direct word regex checks, such as for coordinates +*/ +function keywordTypeMatcherFn(context: MatchContext): void { + const word = context.word.value; + if (KEYWORD_REGEX.test(word)) { + return reference(KEYWORD, context); + } + if (TYPE_REGEX.test(word)) { + return reference(TYPE, context); + } +} + +export const keywordTypeMatcher: Matcher = { priority: 13000, fn: keywordTypeMatcherFn }; diff --git a/src/matching/matchers/localVarMatcher.ts b/src/matching/matchers/localVarMatcher.ts new file mode 100644 index 0000000..c0cc2bb --- /dev/null +++ b/src/matching/matchers/localVarMatcher.ts @@ -0,0 +1,25 @@ +import type { MatchContext, Matcher } from '../../types'; +import { LOCAL_VAR } from "../matchType"; +import { reference, declaration, addExtraData } from "../../utils/matchUtils"; +import { TRIGGER_LINE_REGEX } from '../../enum/regex'; +import { typeKeywords } from '../../runescriptExtension'; + +/** +* Looks for matches of local variables +*/ +function matchLocalVarFn(context: MatchContext): void { + if (context.prevChar === '$') { + if (!context.prevWord) { + return reference(LOCAL_VAR, context); + } + const type = context.prevWord.value.startsWith("def_") ? context.prevWord.value.substring(4) : context.prevWord.value; + const isDeclaration = typeKeywords.has(type); + if (isDeclaration) { + addExtraData(context, { param: TRIGGER_LINE_REGEX.test(context.line.text), type: type }); + return declaration(LOCAL_VAR, context); + } + return reference(LOCAL_VAR, context); + } +} + +export const matchLocalVar: Matcher = { priority: 6000, fn: matchLocalVarFn }; diff --git a/src/matching/matchers/packMatcher.ts b/src/matching/matchers/packMatcher.ts new file mode 100644 index 0000000..3284de7 --- /dev/null +++ b/src/matching/matchers/packMatcher.ts @@ -0,0 +1,28 @@ +import type { MatchContext, MatchType, Matcher } from '../../types'; +import { reference } from "../../utils/matchUtils"; +import { dataTypeToMatchId } from "../../resource/dataTypeToMatchId"; +import { COMPONENT, GLOBAL_VAR, SKIP, UNKNOWN, getMatchTypeById } from "../matchType"; + +/** +* Looks for matches in pack files +*/ +function packMatcherFn(context: MatchContext): void { + if (context.file.type === 'pack' && context.word.index === 1) { + let match: MatchType; + if (context.word.value.startsWith("null")) { + match = SKIP; + } else if (GLOBAL_VAR.fileTypes?.includes(context.file.name)) { + match = GLOBAL_VAR; + } else if (context.file.name === 'interface' && context.word.value.includes(':')) { + match = COMPONENT; + } else { + match = getMatchTypeById(dataTypeToMatchId(context.file.name)) ?? SKIP; + } + if (match.id !== SKIP.id && match.id !== UNKNOWN.id) { + context.packId = context.words[0].value; + } + reference(match, context); + } +} + +export const packMatcher: Matcher = { priority: 1000, fn: packMatcherFn }; diff --git a/src/matching/matchers/parametersMatcher.ts b/src/matching/matchers/parametersMatcher.ts new file mode 100644 index 0000000..8ece4d3 --- /dev/null +++ b/src/matching/matchers/parametersMatcher.ts @@ -0,0 +1,34 @@ +import type { MatchContext, Matcher } from '../../types'; +import { SKIP, getMatchTypeById } from '../matchType'; +import { reference } from '../../utils/matchUtils'; +import { getCallIdentifier, getBlockScopeIdentifier } from '../../cache/activeFileCache'; + +/** +* Looks for matches of values inside of parenthesis +* This includes return statement params, engine command parameters, proc parameters, label parameters, and queue parameters +*/ +function parametersMatcherFn(context: MatchContext): void { + if (context.file.type !== 'rs2') { + return; + } + if (!context.word.callName || context.word.callNameIndex === undefined || context.word.paramIndex === undefined) return undefined; + const paramIndex = context.word.paramIndex + + if (context.word.callName === 'return') { + const iden = getBlockScopeIdentifier(context.line.number); + if (iden && iden.signature && iden.signature.returns.length > paramIndex) { + const resolvedMatchType = getMatchTypeById(iden.signature.returns[paramIndex]) ?? SKIP; + return reference(resolvedMatchType, context); + } + return undefined; + } + + const iden = getCallIdentifier(context.uri, context.line.number, context.word.callName, context.word.callNameIndex); + if (iden?.signature && iden.signature.params.length > paramIndex) { + const matchKey = iden.signature.params[paramIndex].matchTypeId; + const resolvedMatchType = getMatchTypeById(matchKey) ?? SKIP; + return reference(resolvedMatchType, context); + } +} + +export const parametersMatcher: Matcher = { priority: 12000, fn: parametersMatcherFn }; diff --git a/src/matching/matchers/prevCharMatcher.ts b/src/matching/matchers/prevCharMatcher.ts new file mode 100644 index 0000000..d3dd775 --- /dev/null +++ b/src/matching/matchers/prevCharMatcher.ts @@ -0,0 +1,34 @@ +import type { MatchContext, Matcher } from '../../types'; +import { CONSTANT, GLOBAL_VAR, LABEL, MESANIM, PROC } from "../matchType"; +import { reference } from "../../utils/matchUtils"; + +/** +* Looks for matches based on the previous character, such as ~WORD indicates a proc reference +*/ +function prevCharMatcherFn(context: MatchContext): void { + switch (context.prevChar) { + case '^': return reference(CONSTANT, context); + case '%': return reference(GLOBAL_VAR, context); + case '@': return labelMatcher(context); + case '~': return reference(PROC, context); + case ',': return (context.prevWord && context.prevWord.value === "p") ? reference(MESANIM, context) : undefined; + default: return undefined; + } +} + +function labelMatcher(context: MatchContext): void { + if (context.nextChar === '@' && context.word.value.length === 3) { + return; + } + if (context.prevWord) { + const prevHasLeadingAt = context.line.text.charAt(context.prevWord.start - 1) === '@'; + const prevHasTrailingAt = context.line.text.charAt(context.prevWord.end + 1) === '@'; + const tagTouchesWord = context.prevWord.end + 1 === context.word.start - 1; + if (prevHasLeadingAt && prevHasTrailingAt && tagTouchesWord) { + return; + } + } + reference(LABEL, context); +} + +export const prevCharMatcher: Matcher = { priority: 7000, fn: prevCharMatcherFn }; diff --git a/src/matching/matchers/regexWordMatcher.ts b/src/matching/matchers/regexWordMatcher.ts new file mode 100644 index 0000000..f0cecc4 --- /dev/null +++ b/src/matching/matchers/regexWordMatcher.ts @@ -0,0 +1,22 @@ +import type { MatchContext, Matcher } from '../../types'; +import { COORD_REGEX, NUMBER_REGEX, SWITCH_TYPE_REGEX } from "../../enum/regex"; +import { COORDINATES, NUMBER, KEYWORD } from "../matchType"; +import { reference } from "../../utils/matchUtils"; + +/** +* Looks for matches with direct word regex checks, such as for coordinates +*/ +function regexWordMatcherFn(context: MatchContext): void { + const word = context.word.value; + if (NUMBER_REGEX.test(word)) { + return reference(NUMBER, context); // extension doesnt need to know if word is a number, but we can short circuit the matchers here by returning SKIP + } + if (COORD_REGEX.test(word)) { + return reference(COORDINATES, context); + } + if (SWITCH_TYPE_REGEX.test(word)) { + return reference(KEYWORD, context); + } +} + +export const regexWordMatcher: Matcher = { priority: 5000, fn: regexWordMatcherFn }; diff --git a/src/matching/matchers/switchCaseMatcher.ts b/src/matching/matchers/switchCaseMatcher.ts new file mode 100644 index 0000000..00f4f20 --- /dev/null +++ b/src/matching/matchers/switchCaseMatcher.ts @@ -0,0 +1,18 @@ +import type { MatchContext, Matcher } from '../../types'; +import { SKIP } from "../matchType"; +import { reference } from "../../utils/matchUtils"; +import { getSwitchStmtType } from '../../cache/activeFileCache'; +import { SWITCH_CASE_REGEX } from '../../enum/regex'; + +/** +* Looks for matches in case statements +*/ +function switchCaseMatcherFn(context: MatchContext): void { + if (context.word.index > 0 && SWITCH_CASE_REGEX.test(context.line.text) && context.lineIndex < context.line.text.indexOf(' :')) { + if (context.word.value === 'default') return reference(SKIP, context); + const resolved = getSwitchStmtType(context.line.number, context.word.braceDepth); + resolved ? reference(resolved, context) : reference(SKIP, context); + } +} + +export const switchCaseMatcher: Matcher = { priority: 11000, fn: switchCaseMatcherFn }; diff --git a/src/matching/matchers/triggerMatcher.ts b/src/matching/matchers/triggerMatcher.ts new file mode 100644 index 0000000..7271315 --- /dev/null +++ b/src/matching/matchers/triggerMatcher.ts @@ -0,0 +1,46 @@ +import type { MatchContext, Matcher } from '../../types'; +import { TRIGGER_LINE_REGEX } from "../../enum/regex"; +import { CATEGORY, TRIGGER, TYPE } from "../matchType"; +import { runescriptTrigger } from "../../resource/triggers"; +import { reference, declaration, addExtraData } from "../../utils/matchUtils"; + +/** +* Looks for matches with known runescript triggers, see triggers.ts +*/ +function triggerMatcherFn(context: MatchContext): void { + if (context.file.type !== 'rs2') { + return undefined; + } + if (TRIGGER_LINE_REGEX.test(context.line.text)) { + if (context.word.index <= 1) { + const trigger = runescriptTrigger[context.words[0].value.toLowerCase()]; + if (trigger) { + if (context.word.index === 0) { + addExtraData(context, { triggerName: context.words[1].value }) + return reference(TRIGGER, context); + } + if (context.word.value.charAt(0) === '_') { + addExtraData(context, { matchId: trigger.match.id, categoryName: context.word.value.substring(1) }) + return reference(CATEGORY, context); + } + return trigger.declaration ? declaration(trigger.match, context) : reference(trigger.match, context); + } + } + // This means the trigger has defined params (and maybe return types), lets parse these + else if (context.line.text.charAt(context.words[1].end + 2) === '(') { + const endParamsIndex = context.line.text.indexOf(')'); + if (context.word.start < endParamsIndex) { + // These are the type keywords in the trigger line params (Else, its a local var parameter, picked by that matcher) + if (context.word.index % 2 === 0) return reference(TYPE, context); + return undefined; + } + // This means the trigger has defined return types, lets parse these + if (context.line.text.charAt(endParamsIndex + 1) === '(') { + const endReturnsIndex = context.line.text.indexOf(')', endParamsIndex + 1); + if (context.word.start < endReturnsIndex) return reference(TYPE, context); + } + } + } +} + +export const triggerMatcher: Matcher = { priority: 7500, fn: triggerMatcherFn }; diff --git a/src/matching/matchingEngine.ts b/src/matching/matchingEngine.ts new file mode 100644 index 0000000..bde967c --- /dev/null +++ b/src/matching/matchingEngine.ts @@ -0,0 +1,198 @@ +import type { Uri } from 'vscode'; +import type { MatchContext, MatchResult, ParsedFile, ParsedWord } from '../types'; +import { CATEGORY, COMPONENT, DBCOLUMN, DBROW, DBTABLE, MODEL, NULL, OBJ, SKIP, UNKNOWN } from './matchType'; +import { buildMatchContext, reference } from '../utils/matchUtils'; +import { LOC_MODEL_REGEX, TRIGGER_DEFINITION_REGEX } from '../enum/regex'; +import { packMatcher } from './matchers/packMatcher'; +import { regexWordMatcher } from './matchers/regexWordMatcher'; +import { commandMatcher } from './matchers/commandMatcher'; +import { matchLocalVar } from './matchers/localVarMatcher'; +import { prevCharMatcher } from './matchers/prevCharMatcher'; +import { triggerMatcher } from './matchers/triggerMatcher'; +import { configMatcher } from './matchers/configMatcher'; +import { switchCaseMatcher } from './matchers/switchCaseMatcher'; +import { parametersMatcher } from './matchers/parametersMatcher'; +import { configDeclarationMatcher } from './matchers/configDeclarationMatcher'; +import { getFileInfo } from '../utils/fileUtils'; +import { getBlockScopeIdentifier, processMatch as addToActiveFileCache } from '../cache/activeFileCache'; +import { columnDeclarationMatcher } from './matchers/columnDeclarationMatcher'; +import { constDeclarationMatcher } from './matchers/constMatcher'; +import { buildAndCacheIdentifier } from '../resource/identifierFactory'; +import { keywordTypeMatcher } from './matchers/keyWordTypeMatcher'; +import { booleanMatcher } from './matchers/booleanMatcher'; + +export const enum Engine { + Config = 'config', + Runescript = 'runescript', +} + +const engines = { + [Engine.Config]: { + declarationMatchers: [ + configDeclarationMatcher, + columnDeclarationMatcher, + constDeclarationMatcher, + ].slice().sort((a, b) => a.priority - b.priority), + fullMatchers: [ + packMatcher, + regexWordMatcher, + prevCharMatcher, + configMatcher, + configDeclarationMatcher, + columnDeclarationMatcher, + constDeclarationMatcher, + ].slice().sort((a, b) => a.priority - b.priority), + }, + [Engine.Runescript]: { + declarationMatchers: [ + commandMatcher, + triggerMatcher, + ].slice().sort((a, b) => a.priority - b.priority), + fullMatchers: [ + regexWordMatcher, + commandMatcher, + matchLocalVar, + prevCharMatcher, + triggerMatcher, + switchCaseMatcher, + parametersMatcher, + keywordTypeMatcher, + booleanMatcher, + ].slice().sort((a, b) => a.priority - b.priority), + } +} as const + +/** + * Finds all of the matches in a file and caches the matches/identifiers found + * @param uri File uri to be matched + * @param parsedFile The parsed file to be matched + * @param lines The file lines to be matched + * @param declarationsOnly whether only declarations should be matched (default value = false) + * @param engineOverride explicity define the matching engine to use (derived from file type if not provided) + * @returns An array of all the matchResults in the file + */ +export function matchFile(uri: Uri, parsedFile: ParsedFile, lines: string[], declarationsOnly = false, engineOverride?: Engine): MatchResult[] { + const fileMatches: MatchResult[] = []; + const fileInfo = getFileInfo(uri); + const isRunescript = fileInfo.type === 'rs2'; + const engine = engineOverride ?? isRunescript ? Engine.Runescript : Engine.Config; + let parsedLines = Array.from(parsedFile.parsedWords, ([lineNum, parsedWords]) => ({ lineNum, parsedWords })); + + // Process definition lines first if runescript file, because scripts can be refereneced ahead of their declaration + if (isRunescript) { + const isTriggerLine = (line: string) => line.startsWith('[') && TRIGGER_DEFINITION_REGEX.test(line); + const triggerLines: typeof parsedLines = []; + const otherLines: typeof parsedLines = []; + for (const line of parsedLines) { + if (isTriggerLine(lines[line.lineNum])) triggerLines.push(line); + else otherLines.push(line); + } + parsedLines = [...triggerLines, ...otherLines]; + } + + // Iterate thru each line + for (const { lineNum, parsedWords } of parsedLines) { + const lineText = lines[lineNum]; + // Iterate thru each parsed word on the line to find its match type, if any + for (let wordIndex = 0; wordIndex < parsedWords.length; wordIndex++) { + const match = matchWord(buildMatchContext(uri, parsedWords, lineText, lineNum, wordIndex, fileInfo), engine, declarationsOnly); + if (!match) continue; + addToActiveFileCache(match); + fileMatches.push(match); + buildAndCacheIdentifier(match, uri, lineNum, lines); + } + // Operator matching is separately handled and occurs per line, using the now processed matchResults + if (isRunescript) { + // const lineOperators = parsedFile.operatorTokens.get(lineNum); + // operator matching + } + } + return fileMatches; +} + +/** + * Return the match results for a single word + * @param uri uri of the file the match is being found in + * @param parsedLineWords the parsed words on the line the match is being found on + * @param lineText the text of the line the match is being found on + * @param lineNum the line number within the file the match is being found on + * @param wordIndex the index of the *parsed word* that is trying to be matched + * @param declarationsOnly whether only declarations should be matched (default value = false) + * @param engineOverride explicity define the matching engine to use (derived from file type if not provided) + * @returns the resolved matchResult, if any + */ +export function singleWordMatch(uri: Uri, parsedLineWords: ParsedWord[], lineText: string, lineNum: number, wordIndex: number, declarationsOnly = false, engineOverride?: Engine): MatchResult | undefined { + if (wordIndex < 0) return undefined; + const fileInfo = getFileInfo(uri); + const engine = engineOverride ?? fileInfo.type === 'rs2' ? Engine.Runescript : Engine.Config; + return matchWord(buildMatchContext(uri, parsedLineWords, lineText, lineNum, wordIndex, fileInfo), engine, declarationsOnly); +} + +/** + * Runs a word thru the matching engine to try to find a match, short circuits early if a match is made + * @param context The match context needed for proper matching, can use buildMatchContext() helper function + * @param engine The matching engine to use for matching + * @param declarationsOnly Whether to only use the declaration matchers (default value = false) + * @returns A matchResult if a match is made, undefined otherwise + */ +function matchWord(context: MatchContext, engine: Engine, declarationsOnly = false): MatchResult | undefined { + if (!context.word) { + return undefined; + } + if (context.word.value === 'null') { + reference(NULL, context); + return response(context); + } + const matchers = declarationsOnly ? engines[engine].declarationMatchers : engines[engine].fullMatchers; + for (const matcher of matchers) { + matcher.fn(context); + if (context.matchType.id !== UNKNOWN.id) { + return response(context); + } + } +} + +/** +* Build the response object for a match response +*/ +function response(ctx?: MatchContext): MatchResult | undefined { + if (!ctx || ctx.matchType.id === SKIP.id) { + return undefined; + } + const context = { ...ctx, word: { ...ctx.word } }; // shallow clone + deep clone word since we modify it + if (context.matchType.id === COMPONENT.id && !context.word.value.includes(':')) { + context.originalWord = context.word.value; + context.word.value = `${context.file.name}:${context.word.value}`; + } + if (context.matchType.id === DBCOLUMN.id && !context.word.value.includes(':')) { + const requiredType = context.file.type === 'dbtable' ? DBTABLE.id : DBROW.id; + const iden = getBlockScopeIdentifier(context.line.number); + if (!iden || iden.matchId !== requiredType) { + return undefined; + } + const tableName = (context.file.type === 'dbrow') ? iden.extraData?.table : iden.name; + context.originalWord = context.word.value; + context.word.value = `${tableName}:${context.word.value}`; + } + if (context.matchType.id === OBJ.id && context.word.value.startsWith('cert_')) { + context.originalWord = context.word.value; + context.word.value = context.word.value.substring(5); + context.word.start = context.word.start + 5; + context.originalPrefix = 'cert_'; + context.cert = true; + } + if (context.matchType.id === CATEGORY.id && context.word.value.startsWith('_')) { + context.originalWord = context.word.value; + context.word.value = context.word.value.substring(1); + context.word.start = context.word.start + 1; + context.originalPrefix = '_'; + } + // If model match type, determine if it is a loc model and if so remove the suffix part (_0 or _q, etc...) + if (context.matchType.id === MODEL.id && LOC_MODEL_REGEX.test(context.word.value)) { + const lastUnderscore = context.word.value.lastIndexOf("_"); + context.originalSuffix = context.word.value.slice(lastUnderscore); + context.originalWord = context.word.value; + context.word.value = context.word.value.slice(0, lastUnderscore); + } + return { context: context, word: context.word.value }; +} diff --git a/src/matching/operatorMatching.ts b/src/matching/operatorMatching.ts new file mode 100644 index 0000000..4273a05 --- /dev/null +++ b/src/matching/operatorMatching.ts @@ -0,0 +1 @@ +// export function \ No newline at end of file diff --git a/src/parsing/fileParser.ts b/src/parsing/fileParser.ts new file mode 100644 index 0000000..3ca9059 --- /dev/null +++ b/src/parsing/fileParser.ts @@ -0,0 +1,55 @@ +import { type TextDocument, type TextDocumentContentChangeEvent, type Uri } from "vscode"; +import type { ParsedFile } from "../types"; +import { applyLineChanges, getParsedFile, parseLine, resetLineParser } from "./lineParser"; +import { getFileText } from "../utils/fileUtils"; +import { logFileParsed } from "../core/devMode"; + +/** + * This parses a file to find all of the words in it, and caches the words for later retrieval + * Calling this method will rebuild the file entirely if it previously existed + * @param uri The file uri to parse + * @param fileText The text of the file to parse, if not provided it is read from the uri + * @returns All of the parsed words for the file + */ +export function parseFile(uri: Uri, fileText: string[], quiet = false): ParsedFile { + const startTime = performance.now(); + const parsedWords = []; + resetLineParser(uri); + const lines = fileText ?? getFileText(uri); + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + parsedWords.push(...parseLine(lines[lineNum], lineNum, uri)); + } + if (!quiet) logFileParsed(startTime, uri, lines.length); + return cloneParsedFile(); +} + +/** + * Rebuilds the active file based on the actual document changes, reparses only modified lines + * up until the parser state is restored, avoiding a full file reparse. + * Matches are still done for the whole parsed file. + */ +export function reparseFileWithChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[], quiet = false): ParsedFile | undefined { + if (changes.length === 0) return undefined; + const startTime = performance.now(); + let linesAffected = 0; + for (const change of changes) { + const startLine = change.range.start.line; + const endLine = change.range.end.line; + const removedLines = endLine - startLine; + const addedLines = change.text.split(/\r?\n/).length - 1; + const lineDelta = addedLines - removedLines; + linesAffected = applyLineChanges(document, startLine, endLine, lineDelta); + } + if (!quiet) logFileParsed(startTime, document.uri, linesAffected); + return cloneParsedFile();; +} + +function cloneParsedFile(): ParsedFile { + const parsedFile = getParsedFile(); + return { + parsedWords: new Map(parsedFile.parsedWords), + operatorTokens: new Map(parsedFile.operatorTokens), + stringRanges: new Map(parsedFile.stringRanges), + interpolationRanges: new Map(parsedFile.interpolationRanges) + }; +} diff --git a/src/parsing/lineParser.ts b/src/parsing/lineParser.ts new file mode 100644 index 0000000..e2f5d2c --- /dev/null +++ b/src/parsing/lineParser.ts @@ -0,0 +1,698 @@ +import type { TextDocument, Uri } from 'vscode'; +import type { OperatorToken, ParsedFile, ParsedWord } from '../types'; +import { resolveFileKey } from '../utils/cacheUtils'; +import { getFileInfo } from '../utils/fileUtils'; +import { matchLongestException } from './wordExceptions'; + +type Range = { start: number; end: number; }; + +type ParserState = { + fileKey: string | undefined; + isConfig: boolean; + inBlockComment: boolean; + inString: boolean; + inInterpolationString: boolean; + interpDepth: number; + parenDepth: number; + braceDepth: number; + callStack: string[]; + callIndexStack: number[]; + paramIndexStack: number[]; + interpParenDepthStack: number[]; +}; + +type LineParseStateResult = { + words: ParsedWord[]; + operators: OperatorToken[]; + stringRanges: Range[]; + interpRanges: Range[]; + blockCommentRanges: Range[]; + nextState: ParserState; +}; + +const stringRangesByLine = new Map(); +const interpRangesByLine = new Map(); +const blockCommentRangesByLine = new Map(); +const wordsByLine = new Map(); +const operatorsByLine = new Map(); +const endStateByLine = new Map(); +const state: ParserState = { + fileKey: undefined, + isConfig: false, + inBlockComment: false, + inString: false, + inInterpolationString: false, + interpDepth: 0, + parenDepth: 0, + braceDepth: 0, + callStack: [], + callIndexStack: [], + paramIndexStack: [], + interpParenDepthStack: [] +}; + +function getDefaultState(fileKey?: string, isConfig = false): ParserState { + return { + fileKey, + isConfig, + inBlockComment: false, + inString: false, + inInterpolationString: false, + interpDepth: 0, + parenDepth: 0, + braceDepth: 0, + callStack: [], + callIndexStack: [], + paramIndexStack: [], + interpParenDepthStack: [] + }; +} + +export function resetLineParser(uri?: Uri): void { + const fileKey = uri ? resolveFileKey(uri) : undefined; + const isConfig = uri ? getFileInfo(uri).type !== 'rs2' : false; + const resetState = getDefaultState(fileKey, isConfig); + state.fileKey = resetState.fileKey; + state.isConfig = resetState.isConfig; + state.inBlockComment = resetState.inBlockComment; + state.inString = resetState.inString; + state.inInterpolationString = resetState.inInterpolationString; + state.interpDepth = resetState.interpDepth; + state.parenDepth = resetState.parenDepth; + state.braceDepth = resetState.braceDepth; + state.callStack = resetState.callStack; + state.callIndexStack = resetState.callIndexStack; + state.paramIndexStack = resetState.paramIndexStack; + state.interpParenDepthStack = resetState.interpParenDepthStack; + stringRangesByLine.clear(); + interpRangesByLine.clear(); + blockCommentRangesByLine.clear(); + wordsByLine.clear(); + operatorsByLine.clear(); + endStateByLine.clear(); +} + +export function getStringRanges(lineNum?: number): Range[] | Map { + if (lineNum === undefined) return stringRangesByLine; + return stringRangesByLine.get(lineNum) ?? []; +} + +export function getAllStringRanges(): Map { + return stringRangesByLine; +} + +export function getAllInterpolationRanges(): Map { + return interpRangesByLine; +} + +export function getBlockCommentRanges(lineNum?: number): Range[] | Map { + if (lineNum === undefined) return blockCommentRangesByLine; + return blockCommentRangesByLine.get(lineNum) ?? []; +} + +export function getLineWords(lineNum: number): ParsedWord[] { + return wordsByLine.get(lineNum) ?? []; +} + +export function getAllWords(): Map { + return wordsByLine; +} + +export function getParsedFile(): ParsedFile { + return { + parsedWords: getAllWords(), + operatorTokens: getAllOperators(), + stringRanges: getAllStringRanges(), + interpolationRanges: getAllInterpolationRanges() + }; +} + +export function getLineOperators(lineNum: number): OperatorToken[] { + return operatorsByLine.get(lineNum) ?? []; +} + +export function getAllOperators(): Map { + return operatorsByLine; +} + +export function getLineEndState(lineNum: number): ParserState | undefined { + return endStateByLine.get(lineNum); +} + +export function applyLineChanges(document: TextDocument, startLine: number, endLine: number, lineDelta: number): number { + const fileKey = resolveFileKey(document.uri); + if (!fileKey) return 0; + if (state.fileKey && state.fileKey !== fileKey) { + return 0; + } + if (lineDelta !== 0) { + shiftLineMap(wordsByLine, startLine, endLine, lineDelta); + shiftLineMap(stringRangesByLine, startLine, endLine, lineDelta); + shiftLineMap(interpRangesByLine, startLine, endLine, lineDelta); + shiftLineMap(blockCommentRangesByLine, startLine, endLine, lineDelta); + shiftLineMap(operatorsByLine, startLine, endLine, lineDelta); + shiftLineMap(endStateByLine, startLine, endLine, lineDelta); + } else { + wordsByLine.delete(startLine); + stringRangesByLine.delete(startLine); + interpRangesByLine.delete(startLine); + blockCommentRangesByLine.delete(startLine); + operatorsByLine.delete(startLine); + endStateByLine.delete(startLine); + } + + const originalStart = Math.max(0, startLine); + let currentStart = originalStart; + if (currentStart > 0 && !endStateByLine.get(currentStart - 1)) { + resetLineParser(document.uri); + currentStart = 0; + } + + let linesParsed = 0; + for (let lineNum = currentStart; lineNum < document.lineCount; lineNum++) { + linesParsed++; + const prevState = endStateByLine.get(lineNum); + parseLineFromCache(document.lineAt(lineNum).text, lineNum, document.uri); + const nextState = endStateByLine.get(lineNum); + if (lineNum >= originalStart && prevState && nextState && statesEqual(prevState, nextState)) { + return linesParsed; + } + } + return linesParsed; +} + +export function parseLine(lineText: string, lineNum: number, uri: Uri): ParsedWord[] { + const fileKey = resolveFileKey(uri); + if (!fileKey) return []; + if (state.fileKey !== fileKey) { + resetLineParser(uri); + } + + const result = parseLineWithState(lineText, lineNum, { + ...state, + fileKey + }); + + state.fileKey = fileKey; + state.inBlockComment = result.nextState.inBlockComment; + state.inString = result.nextState.inString; + state.inInterpolationString = result.nextState.inInterpolationString; + state.interpDepth = result.nextState.interpDepth; + state.parenDepth = result.nextState.parenDepth; + state.braceDepth = result.nextState.braceDepth; + state.callStack = [...result.nextState.callStack]; + state.callIndexStack = [...result.nextState.callIndexStack]; + state.paramIndexStack = [...result.nextState.paramIndexStack]; + state.interpParenDepthStack = [...result.nextState.interpParenDepthStack]; + endStateByLine.set(lineNum, cloneParserState(result.nextState)); + + if (result.stringRanges.length > 0) stringRangesByLine.set(lineNum, result.stringRanges); + else stringRangesByLine.delete(lineNum); + if (result.interpRanges.length > 0) interpRangesByLine.set(lineNum, result.interpRanges); + else interpRangesByLine.delete(lineNum); + if (result.blockCommentRanges.length > 0) blockCommentRangesByLine.set(lineNum, result.blockCommentRanges); + else blockCommentRangesByLine.delete(lineNum); + if (result.words.length > 0) wordsByLine.set(lineNum, result.words); + else wordsByLine.delete(lineNum); + if (result.operators.length > 0) operatorsByLine.set(lineNum, result.operators); + else operatorsByLine.delete(lineNum); + + return result.words; +} + +export function parseLineFromCache(lineText: string, lineNum: number, uri: Uri): ParsedWord[] { + const fileKey = resolveFileKey(uri); + if (!fileKey) return []; + if (state.fileKey !== fileKey) { + resetLineParser(uri); + } + + const startState = (lineNum === 0) ? getDefaultState(fileKey, state.isConfig) : endStateByLine.get(lineNum - 1); + if (!startState) { + return []; + } + + const result = parseLineWithState(lineText, lineNum, { ...startState, fileKey }); + if (result.stringRanges.length > 0) stringRangesByLine.set(lineNum, result.stringRanges); + else stringRangesByLine.delete(lineNum); + if (result.interpRanges.length > 0) interpRangesByLine.set(lineNum, result.interpRanges); + else interpRangesByLine.delete(lineNum); + if (result.blockCommentRanges.length > 0) blockCommentRangesByLine.set(lineNum, result.blockCommentRanges); + else blockCommentRangesByLine.delete(lineNum); + if (result.words.length > 0) wordsByLine.set(lineNum, result.words); + else wordsByLine.delete(lineNum); + if (result.operators.length > 0) operatorsByLine.set(lineNum, result.operators); + else operatorsByLine.delete(lineNum); + endStateByLine.set(lineNum, cloneParserState(result.nextState)); + return result.words; +} + +/** + * Parse a line using the cached state of the previous line without mutating cache state. + * Useful for transient parsing (ex: completion providers). + */ +export function parseLineWithStateSnapshot(lineText: string, lineNum: number, uri: Uri): ParsedWord[] { + const fileKey = resolveFileKey(uri); + if (!fileKey) return []; + + const isConfig = getFileInfo(uri).type !== 'rs2'; + const startState = (lineNum === 0) ? getDefaultState(fileKey, isConfig) : endStateByLine.get(lineNum - 1); + if (!startState) { + return []; + } + if (startState.fileKey && startState.fileKey !== fileKey) { + return []; + } + const result = parseLineWithState(lineText, lineNum, { ...cloneParserState(startState), fileKey }); + return result.words; +} + +export type CallStateSnapshot = { + callName?: string; + callNameIndex?: number; + paramIndex?: number; + parenDepth: number; +}; + +/** + * Parse a line up to a cursor position and return the call state at that position. + * Useful when the cursor is inside a string and no word is produced. + */ +export function getCallStateAtPosition(lineText: string, lineNum: number, uri: Uri, character: number): CallStateSnapshot | undefined { + const fileKey = resolveFileKey(uri); + if (!fileKey) return undefined; + + const isConfig = getFileInfo(uri).type !== 'rs2'; + const startState = (lineNum === 0) ? getDefaultState(fileKey, isConfig) : endStateByLine.get(lineNum - 1); + if (!startState) { + return undefined; + } + if (startState.fileKey && startState.fileKey !== fileKey) { + return undefined; + } + + const clampedChar = Math.max(0, Math.min(character, lineText.length)); + const result = parseLineWithState(lineText.slice(0, clampedChar), lineNum, { ...cloneParserState(startState), fileKey }); + const callStack = result.nextState.callStack; + const callIndexStack = result.nextState.callIndexStack; + const paramIndexStack = result.nextState.paramIndexStack; + return { + callName: callStack[callStack.length - 1], + callNameIndex: callIndexStack[callIndexStack.length - 1], + paramIndex: paramIndexStack[paramIndexStack.length - 1], + parenDepth: result.nextState.parenDepth + }; +} + +function parseLineWithState(lineText: string, _lineNum: number, startState: ParserState): LineParseStateResult { + if (lineText.startsWith("text=") || lineText.startsWith("activetext=")) { + return { words: [], operators: [], stringRanges: [], interpRanges: [], blockCommentRanges: [], nextState: cloneParserState(startState) }; + } + const words: ParsedWord[] = []; + const operators: OperatorToken[] = []; + const stringRanges: Range[] = []; + const interpRanges: Range[] = []; + const blockCommentRanges: Range[] = []; + const nextState = cloneParserState(startState); + + let parenDepth = nextState.parenDepth; + let braceDepth = nextState.braceDepth; + const callStack = nextState.callStack; + const callIndexStack = nextState.callIndexStack; + const paramIndexStack = nextState.paramIndexStack; + const interpParenDepthStack = nextState.interpParenDepthStack; + let lastWordValue: string | undefined; + let lastWordIndex: number | undefined; + let wordStart = -1; + let wordHasColon = false; + let wordParenDepth = 0; + let wordBraceDepth = 0; + let wordInInterpolation = false; + let wordCallName: string | undefined; + let wordCallNameIndex: number | undefined; + let wordParamIndex: number | undefined; + let wordConfigKey: string | undefined; + let configKeyValue: string | undefined; + let configKeyIndex: number | undefined; + let configParamIndex = 0; + let inConfigValue = false; + + let stringStart: number | undefined = nextState.inString ? 0 : undefined; + const interpStartStack: number[] = []; + let blockStart: number | undefined = nextState.inBlockComment ? 0 : undefined; + + const finalizeWord = (endIndex: number) => { + if (wordStart < 0 || endIndex < wordStart) { + wordStart = -1; + wordHasColon = false; + wordCallName = undefined; + wordCallNameIndex = undefined; + wordParamIndex = undefined; + return; + } + const value = lineText.slice(wordStart, endIndex + 1); + const index = words.length; + words.push({ + value, + start: wordStart, + end: endIndex, + index, + inString: false, + inInterpolation: wordInInterpolation, + parenDepth: wordParenDepth, + braceDepth: wordBraceDepth, + callName: wordCallName, + callNameIndex: wordCallNameIndex, + paramIndex: wordParamIndex, + configKey: wordConfigKey + }); + lastWordValue = value; + lastWordIndex = index; + wordStart = -1; + wordHasColon = false; + wordCallName = undefined; + wordCallNameIndex = undefined; + wordParamIndex = undefined; + wordConfigKey = undefined; + }; + + const isAlphaNum = (ch: string) => /[A-Za-z0-9_]/.test(ch); + const canStartWord = (ch: string, next: string) => + isAlphaNum(ch) || (ch === '.' && isAlphaNum(next)); + const addOperator = (op: string, index: number) => { + operators.push({ token: op, index, parenDepth }); + }; + + for (let i = 0; i < lineText.length; i++) { + const ch = lineText[i]!; + const next = lineText[i + 1] ?? ''; + + if (nextState.inBlockComment) { + if (ch === '*' && next === '/') { + const end = i + 1; + blockCommentRanges.push({ start: blockStart ?? 0, end }); + blockStart = undefined; + nextState.inBlockComment = false; + i++; + } + continue; + } + + if (!nextState.inString && ch === '/' && next === '/') { + finalizeWord(i - 1); + break; + } + + if (!nextState.inString && ch === '/' && next === '*') { + finalizeWord(i - 1); + nextState.inBlockComment = true; + blockStart = i; + i++; + continue; + } + + if (nextState.inString && nextState.interpDepth > 0 && ch === '"') { + nextState.inInterpolationString = !nextState.inInterpolationString; + if (nextState.inInterpolationString) { + stringStart = i; + } else if (stringStart !== undefined) { + stringRanges.push({ start: stringStart, end: i }); + stringStart = undefined; + } + continue; + } + + if (!nextState.inInterpolationString && ch === '"') { + finalizeWord(i - 1); + if (nextState.inString) { + if (stringStart !== undefined) { + stringRanges.push({ start: stringStart, end: i }); + stringStart = undefined; + } + nextState.inString = false; + } else { + nextState.inString = true; + stringStart = i; + } + continue; + } + + if (nextState.inString && !nextState.inInterpolationString && ch === '<') { + finalizeWord(i - 1); + interpStartStack.push(i); + nextState.interpDepth++; + interpParenDepthStack.push(parenDepth); + continue; + } + if (nextState.inString && !nextState.inInterpolationString && nextState.interpDepth > 0 && ch === '>') { + finalizeWord(i - 1); + const interpStart = interpStartStack.pop(); + if (interpStart !== undefined) interpRanges.push({ start: interpStart, end: i }); + nextState.interpDepth = Math.max(0, nextState.interpDepth - 1); + interpParenDepthStack.pop(); + continue; + } + + const inInterpolationCode = nextState.interpDepth > 0 && !nextState.inInterpolationString; + const inCode = !nextState.inString || inInterpolationCode; + if (!inCode) { + continue; + } + + if (ch === '{') { + finalizeWord(i - 1); + braceDepth++; + continue; + } + if (ch === '}') { + finalizeWord(i - 1); + braceDepth = Math.max(0, braceDepth - 1); + continue; + } + + if (!startState.isConfig && (ch === '<' || ch === '>')) { + if (next === '=') { + finalizeWord(i - 1); + addOperator(ch + next, i); + i++; + continue; + } + finalizeWord(i - 1); + addOperator(ch, i); + continue; + } + if (!startState.isConfig && ch === '=') { + if (next === '=') { + i++; + continue; + } + finalizeWord(i - 1); + addOperator(ch, i); + continue; + } + if (!startState.isConfig && ch === '!') { + if (next === '=') { + i++; + continue; + } + finalizeWord(i - 1); + addOperator(ch, i); + continue; + } + if (!startState.isConfig && ch === '&') { + if (next === '&') { + i++; + continue; + } + finalizeWord(i - 1); + addOperator(ch, i); + continue; + } + if (!startState.isConfig && ch === '|') { + if (next === '|') { + i++; + continue; + } + finalizeWord(i - 1); + addOperator(ch, i); + continue; + } + + if (wordStart < 0) { + const exceptionLength = matchLongestException(lineText, i); + if (exceptionLength > 0) { + wordStart = i; + wordParenDepth = parenDepth; + wordBraceDepth = braceDepth; + wordInInterpolation = nextState.interpDepth > 0; + if (startState.isConfig && inConfigValue) { + wordConfigKey = configKeyValue; + wordCallNameIndex = configKeyIndex; + wordParamIndex = configParamIndex; + } else { + wordCallName = parenDepth > 0 ? callStack[callStack.length - 1] : undefined; + wordParamIndex = parenDepth > 0 ? paramIndexStack[paramIndexStack.length - 1] : undefined; + } + finalizeWord(i + exceptionLength - 1); + i += exceptionLength - 1; + continue; + } + } + + if (startState.isConfig && ch === '=' && !nextState.inString && !nextState.inInterpolationString) { + finalizeWord(i - 1); + configKeyValue = lastWordValue; + configKeyIndex = lastWordIndex; + configParamIndex = 0; + inConfigValue = true; + continue; + } + if (startState.isConfig && inConfigValue && ch === ',' && !nextState.inString && !nextState.inInterpolationString) { + finalizeWord(i - 1); + configParamIndex++; + continue; + } + + if (ch === '(') { + finalizeWord(i - 1); + if (lastWordValue) { + callStack.push(lastWordValue); + if (lastWordIndex !== undefined) { + callIndexStack.push(lastWordIndex); + } else { + callIndexStack.push(-1); + } + } + parenDepth++; + paramIndexStack.push(0); + continue; + } + if (ch === ')') { + finalizeWord(i - 1); + parenDepth = Math.max(0, parenDepth - 1); + callStack.pop(); + callIndexStack.pop(); + paramIndexStack.pop(); + continue; + } + if (ch === ',' && parenDepth > 0) { + if (nextState.inString && inInterpolationCode) { + const interpParenDepth = interpParenDepthStack[interpParenDepthStack.length - 1] ?? parenDepth; + if (parenDepth <= interpParenDepth) { + finalizeWord(i - 1); + continue; + } + } else if (nextState.inString && !inInterpolationCode) { + continue; + } + finalizeWord(i - 1); + const current = paramIndexStack[paramIndexStack.length - 1] ?? 0; + paramIndexStack[paramIndexStack.length - 1] = current + 1; + continue; + } + + if (wordStart < 0) { + if (canStartWord(ch, next)) { + wordStart = i; + wordParenDepth = parenDepth; + wordBraceDepth = braceDepth; + wordInInterpolation = nextState.interpDepth > 0; + if (startState.isConfig && inConfigValue) { + wordConfigKey = configKeyValue; + wordCallNameIndex = configKeyIndex; + wordParamIndex = configParamIndex; + } else { + wordCallName = parenDepth > 0 ? callStack[callStack.length - 1] : undefined; + wordCallNameIndex = parenDepth > 0 ? callIndexStack[callIndexStack.length - 1] : undefined; + wordParamIndex = parenDepth > 0 ? paramIndexStack[paramIndexStack.length - 1] : undefined; + } + } + continue; + } + + if (isAlphaNum(ch)) { + continue; + } + if (ch === ':' && !wordHasColon && isAlphaNum(next)) { + wordHasColon = true; + continue; + } + + finalizeWord(i - 1); + } + + finalizeWord(lineText.length - 1); + + if (nextState.inString && stringStart !== undefined && lineText.length > 0) { + stringRanges.push({ start: stringStart, end: lineText.length - 1 }); + } + if (nextState.inBlockComment && blockStart !== undefined && lineText.length > 0) { + blockCommentRanges.push({ start: blockStart, end: lineText.length - 1 }); + } + + nextState.parenDepth = parenDepth; + nextState.braceDepth = braceDepth; + return { words, operators, stringRanges, interpRanges, blockCommentRanges, nextState }; +} + +function statesEqual(a: ParserState, b: ParserState): boolean { + return a.fileKey === b.fileKey && + a.isConfig === b.isConfig && + a.inBlockComment === b.inBlockComment && + a.inString === b.inString && + a.inInterpolationString === b.inInterpolationString && + a.interpDepth === b.interpDepth && + a.parenDepth === b.parenDepth && + a.braceDepth === b.braceDepth && + arrayEqual(a.callStack, b.callStack) && + arrayEqual(a.callIndexStack, b.callIndexStack) && + arrayEqual(a.paramIndexStack, b.paramIndexStack) && + arrayEqual(a.interpParenDepthStack, b.interpParenDepthStack); +} + +function cloneParserState(state: ParserState): ParserState { + return { + fileKey: state.fileKey, + isConfig: state.isConfig, + inBlockComment: state.inBlockComment, + inString: state.inString, + inInterpolationString: state.inInterpolationString, + interpDepth: state.interpDepth, + parenDepth: state.parenDepth, + braceDepth: state.braceDepth, + callStack: [...state.callStack], + callIndexStack: [...state.callIndexStack], + paramIndexStack: [...state.paramIndexStack], + interpParenDepthStack: [...state.interpParenDepthStack] + }; +} + +function arrayEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +function shiftLineMap(map: Map, startLine: number, endLine: number, lineDelta: number): void { + if (lineDelta === 0) return; + const next = new Map(); + for (const [line, value] of map) { + if (line < startLine) { + next.set(line, value); + continue; + } + if (line <= endLine) { + continue; + } + const newLine = line + lineDelta; + if (newLine >= 0) { + next.set(newLine, value); + } + } + map.clear(); + for (const [line, value] of next) { + map.set(line, value); + } +} diff --git a/src/parsing/mapParser.ts b/src/parsing/mapParser.ts new file mode 100644 index 0000000..5c32289 --- /dev/null +++ b/src/parsing/mapParser.ts @@ -0,0 +1,119 @@ +import { Position, Range } from 'vscode'; + +export type MapEntryKind = 'loc' | 'npc' | 'obj'; + +export type MapEntry = { + kind: MapEntryKind; + line: number; + level: number; + x: number; + z: number; + id: number; + extras: number[]; + range: Range; + idRange: Range; +}; + +export type MapParseError = { + line: number; + message: string; + range?: Range; +}; + +export type MapParseResult = { + entries: MapEntry[]; + errors: MapParseError[]; + sections: Array<{ line: number; name: string }>; +}; + +const sectionRegex = /^====\s*(\w+)\s*====\s*$/; +const lineRegex = /^(\s*)(\d+)\s+(\d+)\s+(\d+)\s*:\s*(.+)\s*$/; + +export function parseMapFile(lines: string[]): MapParseResult { + const entries: MapEntry[] = []; + const errors: MapParseError[] = []; + const sections: Array<{ line: number; name: string }> = []; + let currentSection: MapEntryKind | undefined; + + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum] ?? ''; + const sectionMatch = sectionRegex.exec(line); + if (sectionMatch) { + const name = sectionMatch[1]?.toLowerCase(); + sections.push({ line: lineNum, name: (sectionMatch[1] ?? '').toUpperCase() }); + if (name === 'loc' || name === 'npc' || name === 'obj') { + currentSection = name; + } else { + currentSection = undefined; + } + continue; + } + + if (!currentSection) continue; + if (!line.trim()) continue; + + const match = lineRegex.exec(line); + if (!match) { + errors.push({ line: lineNum, message: 'Invalid map line format', range: toRange(lineNum, line) }); + continue; + } + + const leading = match[1] ?? ''; + const level = parseIntStrict(match[2]); + const x = parseIntStrict(match[3]); + const z = parseIntStrict(match[4]); + if (level === undefined || x === undefined || z === undefined) { + errors.push({ line: lineNum, message: 'Invalid coordinates', range: toRange(lineNum, line) }); + continue; + } + + const rhs = match[5] ?? ''; + const rhsTrimmed = rhs.trim(); + const rhsIndex = line.indexOf(rhsTrimmed, leading.length); + const idMatch = /^(\d+)/.exec(rhsTrimmed); + const id = parseIntStrict(idMatch?.[1]); + if (id === undefined) { + errors.push({ line: lineNum, message: 'Missing or invalid id', range: toRange(lineNum, line) }); + continue; + } + + const idStart = rhsIndex >= 0 ? rhsIndex : line.indexOf(rhsTrimmed); + const idLength = idMatch?.[1]?.length ?? 0; + const idRange = new Range(new Position(lineNum, Math.max(0, idStart)), new Position(lineNum, Math.max(0, idStart + idLength))); + + const parts = rhsTrimmed.split(/\s+/).filter(Boolean); + const extras: number[] = []; + for (let i = 1; i < parts.length; i++) { + const value = parseIntStrict(parts[i]); + if (value === undefined) { + errors.push({ line: lineNum, message: 'Invalid extra field', range: toRange(lineNum, line) }); + break; + } + extras.push(value); + } + + entries.push({ + kind: currentSection, + line: lineNum, + level, + x, + z, + id, + extras, + range: toRange(lineNum, line), + idRange + }); + } + + return { entries, errors, sections }; +} + +function parseIntStrict(value?: string): number | undefined { + if (!value) return undefined; + if (!/^\d+$/.test(value)) return undefined; + return Number(value); +} + +function toRange(lineNum: number, line: string): Range { + return new Range(new Position(lineNum, 0), new Position(lineNum, Math.max(0, line.length))); +} diff --git a/src/parsing/operators.ts b/src/parsing/operators.ts new file mode 100644 index 0000000..532e835 --- /dev/null +++ b/src/parsing/operators.ts @@ -0,0 +1,10 @@ +export const operators = new Set([ + '<=', + '>=', + '=', + '<', + '>', + '!', + '&', + '|' +]); \ No newline at end of file diff --git a/src/parsing/wordExceptions.ts b/src/parsing/wordExceptions.ts new file mode 100644 index 0000000..7c855f9 --- /dev/null +++ b/src/parsing/wordExceptions.ts @@ -0,0 +1,37 @@ +import { Trie } from '../cache/class/Trie'; + +const exceptionsTrie = new Trie(); +const STANDARD_WORD_REGEX = /^(?:\.\w+|\w+:\w+|\w+)$/; + +export function findFileExceptionWords(lines: string[]): void { + for (const lineText of lines) { + const matches = lineText.matchAll(/\[([^\]]+)\]/g); + for (const match of matches) { + const inner = match[1]; + if (!inner) continue; + const commaIndex = inner.indexOf(','); + const word = (commaIndex >= 0 ? inner.slice(commaIndex + 1) : inner).trim(); + if (word) { + addExceptionWord(word); + } + } + } +} + +export function addExceptionWord(word: string): void { + if (!STANDARD_WORD_REGEX.test(word) && !exceptionsTrie.hasWord(word)) { + exceptionsTrie.insert(word); + } +} + +export function clearExceptionWords(): void { + exceptionsTrie.clear(); +} + +export function getExceptionWords(): string[] { + return exceptionsTrie.getAllWords(); +} + +export function matchLongestException(text: string, startIndex: number): number { + return exceptionsTrie.matchLongest(text, startIndex); +} diff --git a/src/provider/color/color24Provider.ts b/src/provider/color/color24Provider.ts new file mode 100644 index 0000000..3cee8d3 --- /dev/null +++ b/src/provider/color/color24Provider.ts @@ -0,0 +1,44 @@ +import type { CancellationToken, TextDocument, DocumentColorProvider, ColorPresentation, ColorInformation } from 'vscode'; +import { Color, Range, TextEdit } from 'vscode'; +import { COLOR24_REGEX } from '../../enum/regex'; + +const color24Provider: DocumentColorProvider = { + provideColorPresentations(color: Color, context: { range: Range }, _token: CancellationToken): ColorPresentation[] { + const r = Math.round(color.red * 255); + const g = Math.round(color.green * 255); + const b = Math.round(color.blue * 255); + const rgb = (r << 16) | (g << 8) | b; + + const colorPresentations: ColorPresentation[] = [ + { + label: 'Color Picker', + textEdit: new TextEdit(context.range, '0x' + rgb.toString(16).toUpperCase().padStart(6, '0')) + } + ]; + + return colorPresentations; + }, + + provideDocumentColors(document: TextDocument): ColorInformation[] { + const text = document.getText(); + let match: RegExpExecArray | undefined; + + const colorInfos: ColorInformation[] = []; + while ((match = COLOR24_REGEX.exec(text) ?? undefined)) { + const rgb = parseInt(match[2]!, 16); + + const r = (rgb >> 16) & 0xFF; + const g = (rgb >> 8) & 0xFF; + const b = rgb & 0xFF; + + colorInfos.push({ + color: new Color(r / 255, g / 255, b / 255, 1), + range: new Range(document.positionAt(match.index + match[1]!.length + 1), document.positionAt(match.index + match[1]!.length + match[2]!.length + 1)) + }); + } + + return colorInfos; + } +}; + +export { color24Provider }; diff --git a/src/provider/color/recolorProvider.ts b/src/provider/color/recolorProvider.ts new file mode 100644 index 0000000..79bdefd --- /dev/null +++ b/src/provider/color/recolorProvider.ts @@ -0,0 +1,44 @@ +import type { CancellationToken, ColorInformation, ColorPresentation, DocumentColorProvider, TextDocument} from 'vscode'; +import { Color, Range, TextEdit } from 'vscode'; +import { RECOLOR_REGEX } from '../../enum/regex'; + +const recolProvider: DocumentColorProvider = { + provideColorPresentations(color: Color, context: { range: Range }, _token: CancellationToken): ColorPresentation[] { + const r = Math.round(color.red * 31); + const g = Math.round(color.green * 31); + const b = Math.round(color.blue * 31); + const rgb = (r << 10) | (g << 5) | b; + + const colorPresentations: ColorPresentation[] = [ + { + label: 'Model Recolor', + textEdit: new TextEdit(context.range, rgb.toString()) + } + ]; + + return colorPresentations; + }, + + provideDocumentColors(document: TextDocument): ColorInformation[] { + const text = document.getText(); + let match: RegExpExecArray | undefined; + + const colorInfos: ColorInformation[] = []; + while ((match = RECOLOR_REGEX.exec(text) ?? undefined)) { + const rgb = parseInt(match[2]!); + + const r = (rgb >> 10) & 0x1f; + const g = (rgb >> 5) & 0x1f; + const b = rgb & 0x1f; + + colorInfos.push({ + color: new Color(r / 31, g / 31, b / 31, 1), + range: new Range(document.positionAt(match.index + match[1]!.length + 1), document.positionAt(match.index + match[1]!.length + match[2]!.length + 1)) + }); + } + + return colorInfos; + } +}; + +export { recolProvider }; diff --git a/src/provider/completion/completetionCommon.ts b/src/provider/completion/completetionCommon.ts new file mode 100644 index 0000000..2353307 --- /dev/null +++ b/src/provider/completion/completetionCommon.ts @@ -0,0 +1,108 @@ +import type { Command, TextDocument } from "vscode"; +import { Position, CompletionItem, CompletionItemKind, Range, TextEdit } from "vscode"; +import { COMMAND, CONFIG_KEY, CONSTANT, ENUM, GLOBAL_VAR, LABEL, LOCAL_VAR, MESANIM, PROC, QUEUE, SKIP, TRIGGER } from "../../matching/matchType"; +import { getAllWithPrefix, getTypes } from "../../cache/completionCache"; +import { waitForActiveFileRebuild } from "../../core/eventHandlers"; +import { parseLineWithStateSnapshot } from "../../parsing/lineParser"; +import { singleWordMatch } from "../../matching/matchingEngine"; +import { runescriptTrigger } from "../../resource/triggers"; +import { getLocalVariableNames } from "../../cache/activeFileCache"; +import { getByKey } from "../../cache/identifierCache"; +import { getObservedConfigKeys } from "../../resource/configKeys"; + +const autoTriggeredTypeIds = [ + CONSTANT.id, + GLOBAL_VAR.id, + LOCAL_VAR.id, + PROC.id, + LABEL.id +]; + +export function getCompletionItemKind(matchTypeId: string): CompletionItemKind { + switch (matchTypeId) { + case CONSTANT.id: return CompletionItemKind.Constant; + case LOCAL_VAR.id: + case GLOBAL_VAR.id: return CompletionItemKind.Variable; + case QUEUE.id: + case COMMAND.id: + case PROC.id: + case LABEL.id: return CompletionItemKind.Function; + case MESANIM.id: + case ENUM.id: return CompletionItemKind.Enum; + default: return CompletionItemKind.Text; + } +} + +export function buildCompletionItem(label: string, kind: CompletionItemKind, desc: string, range?: Range, additionalTextEdits?: TextEdit[], command?: Command): CompletionItem { + const res = new CompletionItem(label, kind); + res.label = { label: label, description: desc.toLowerCase() }; + if (range) res.range = range; + if (additionalTextEdits) res.additionalTextEdits = additionalTextEdits; + if (command) res.command = command; + return res; +} + +export function completionTypeSelector(position: Position): CompletionItem[] { + return getTypes().filter(type => !autoTriggeredTypeIds.includes(type)).map(type => { + const additionalEdits = [TextEdit.delete(new Range(position.translate(0, -1), position))]; + const command = { command: 'editor.action.triggerSuggest', title: 'Trigger Suggest' }; + return buildCompletionItem(`${type}>`, CompletionItemKind.Enum, type, undefined, additionalEdits, command); + }); +} + +let lastRequestId = 0; +export async function searchForMatchType(document: TextDocument, position: Position, defaultMatchId = SKIP.id, fromTrigger = false): Promise { + const requestId = ++lastRequestId; + await waitForActiveFileRebuild(document); + if (requestId !== lastRequestId) return []; // guard debounce, only continue with 1 result + const triggerOffset = fromTrigger ? 2 : 0; + let str = document.lineAt(position.line).text; + str = str.substring(0, position.character - triggerOffset) + 'temp' + str.substring(position.character); + const parsedWords = parseLineWithStateSnapshot(str, position.line, document.uri); + const wordIndex = parsedWords.findIndex(w => w.start <= position.character && w.end >= position.character); + const matchResult = singleWordMatch(document.uri, parsedWords, str, position.line, wordIndex); + const matchTypeId = (matchResult) ? matchResult.context.matchType.id : defaultMatchId; + if (matchTypeId === SKIP.id) return []; + const prefix = (matchResult) ? (matchResult.context.originalWord ?? matchResult.word).slice(0, -4) : ''; + const additionalTextEdits = [TextEdit.delete(new Range(position.translate(0, -triggerOffset), position))]; + return completionWithMatchid(prefix, matchTypeId, position.line, additionalTextEdits); +} + +export function completionWithMatchid(prefix: string, matchTypeId: string, lineNum: number, additionalTextEdits?: TextEdit[]): CompletionItem[] { + const completionItems: CompletionItem[] = []; + let identifierNames: { name: string, desc: string }[] = []; + switch(matchTypeId) { + case CONFIG_KEY.id: + identifierNames = [...getObservedConfigKeys()].map(configKey => ({ name: configKey, desc: CONFIG_KEY.id })); + break; + case TRIGGER.id: + identifierNames = Object.keys(runescriptTrigger).map(trigger => ({ name: trigger, desc: TRIGGER.id })); + break; + case LOCAL_VAR.id: + identifierNames = Array.from(getLocalVariableNames(lineNum)); + break; + case GLOBAL_VAR.id: + identifierNames = (getAllWithPrefix(prefix, matchTypeId) ?? []).map(iden => ({ name: iden, desc: getByKey(`${iden}${matchTypeId}`)!.fileType })); + break; + default: + identifierNames = (getAllWithPrefix(prefix, matchTypeId) ?? []).map(iden => ({ name: iden, desc: matchTypeId })); + } + const completionKind = getCompletionItemKind(matchTypeId); + identifierNames.forEach(completionData => completionItems.push( + buildCompletionItem(completionData.name, completionKind, completionData.desc, undefined, additionalTextEdits))); + return completionItems; +} + +export function completionByType(document: TextDocument, position: Position, triggerIndex: number, word: string): CompletionItem[] { + const completionItems: CompletionItem[] = []; + const prevWordRange = document.getWordRangeAtPosition(new Position(position.line, triggerIndex)); + if (!prevWordRange) { + return completionItems; + } + const additionalTextEdits = [TextEdit.delete(new Range(prevWordRange.start, position))]; + return completionWithMatchid(word, document.getText(prevWordRange), position.line, additionalTextEdits); +} + +export function doubleBacktickTrigger(document: TextDocument, position: Position, trigger?: string): boolean { + return trigger !== undefined && trigger === '`' && position.character > 1 && document.lineAt(position.line).text.charAt(position.character - 2) === '`'; +} diff --git a/src/provider/completion/configCompletionProvider.ts b/src/provider/completion/configCompletionProvider.ts new file mode 100644 index 0000000..35c6441 --- /dev/null +++ b/src/provider/completion/configCompletionProvider.ts @@ -0,0 +1,31 @@ +import type { CompletionItem, Position, CancellationToken, CompletionContext, TextDocument, CompletionItemProvider } from 'vscode'; +import { CompletionTriggerKind } from 'vscode'; +import { CONFIG_KEY } from '../../matching/matchType'; +import { completionByType, completionTypeSelector, doubleBacktickTrigger, searchForMatchType } from './completetionCommon'; + +export const completionTriggers = ['=', ',', '`', '>']; + +export const completionProvider: CompletionItemProvider = { + async provideCompletionItems(document: TextDocument, position: Position, _cancellationToken: CancellationToken, context: CompletionContext): Promise { + if (context.triggerKind === CompletionTriggerKind.TriggerCharacter) { + if (doubleBacktickTrigger(document, position, context.triggerCharacter)) { + return searchForMatchType(document, position, CONFIG_KEY.id, true); + } + return invoke(document, position, position.character - 1, ''); + } + const wordRange = document.getWordRangeAtPosition(position); + const word = (!wordRange) ? '' : document.getText(wordRange); + const triggerIndex = (!wordRange) ? position.character - 1 : wordRange.start.character - 1; + return invoke(document, position, triggerIndex, word); + } +} + +async function invoke(document: TextDocument, position: Position, triggerIndex: number, word: string): Promise { + switch (document.lineAt(position.line).text.charAt(triggerIndex)) { + case '`': return completionTypeSelector(position); + case '>': return completionByType(document, position, triggerIndex, word); + case ',': return searchForMatchType(document, position); + case '=': return searchForMatchType(document, position); + default: return searchForMatchType(document, position, CONFIG_KEY.id); + } +} diff --git a/src/provider/completion/runescriptCompletionProvider.ts b/src/provider/completion/runescriptCompletionProvider.ts new file mode 100644 index 0000000..39c28f9 --- /dev/null +++ b/src/provider/completion/runescriptCompletionProvider.ts @@ -0,0 +1,51 @@ +import type { Position, CompletionItem, CancellationToken, CompletionContext, TextDocument, CompletionItemProvider } from 'vscode'; +import { CompletionTriggerKind } from 'vscode'; +import { COMMAND, CONSTANT, GLOBAL_VAR, LABEL, LOCAL_VAR, PROC, TRIGGER } from '../../matching/matchType'; +import { completionByType, completionTypeSelector, completionWithMatchid, doubleBacktickTrigger, searchForMatchType } from './completetionCommon'; + +export const completionTriggers = ['$', '^', '%', '~', '@', '`', '>', ',', '[', '(', ' ']; + +export const completionProvider: CompletionItemProvider = { + async provideCompletionItems(document: TextDocument, position: Position, _cancellationToken: CancellationToken, context: CompletionContext): Promise { + if (context.triggerKind === CompletionTriggerKind.TriggerCharacter) { + if (doubleBacktickTrigger(document, position, context.triggerCharacter)) { + return searchForMatchType(document, position, COMMAND.id, true); + } + if (context.triggerCharacter === ' ') { + const line = document.lineAt(position.line).text; + const triggerIndex = findTriggerIndex(line, position.character - 1); + if (triggerIndex >= 0 && [','].includes(line.charAt(triggerIndex))) { // list = trigger chars where you might put a space after typing them + return invoke(document, position, triggerIndex, ''); + } + return []; + } + return invoke(document, position, position.character - 1, ''); + } + const wordRange = document.getWordRangeAtPosition(position); + const word = (!wordRange) ? '' : document.getText(wordRange); + const triggerIndex = (!wordRange) ? position.character - 1 : wordRange.start.character - 1; + return invoke(document, position, triggerIndex, word); + } +} + +async function invoke(document: TextDocument, position: Position, triggerIndex: number, word: string): Promise { + switch (document.lineAt(position.line).text.charAt(triggerIndex)) { + case '`': return completionTypeSelector(position); + case '>': return completionByType(document, position, triggerIndex, word); + case '[': return (document.uri.fsPath.endsWith('.rs2')) ? completionWithMatchid(word, TRIGGER.id, position.line) : searchForMatchType(document, position); + case '^': return completionWithMatchid(word, CONSTANT.id, position.line); + case '%': return completionWithMatchid(word, GLOBAL_VAR.id, position.line); + case '~': return completionWithMatchid(word, PROC.id, position.line); + case '@': return completionWithMatchid(word, LABEL.id, position.line); + case '$': return completionWithMatchid(word, LOCAL_VAR.id, position.line); + case ',': return searchForMatchType(document, position); + case '(': return searchForMatchType(document, position); + default: return searchForMatchType(document, position, COMMAND.id); + } +} + +function findTriggerIndex(line: string, start: number): number { + let i = start; + while (i >= 0 && line.charAt(i) === ' ') i--; + return i; +} diff --git a/src/provider/gotoDefinitionProvider.ts b/src/provider/gotoDefinitionProvider.ts new file mode 100644 index 0000000..33543d0 --- /dev/null +++ b/src/provider/gotoDefinitionProvider.ts @@ -0,0 +1,34 @@ +import type { DefinitionProvider, Position, TextDocument } from 'vscode'; +import { Location } from 'vscode'; +import { getByDocPosition } from '../cache/activeFileCache'; +import { decodeReferenceToLocation } from '../utils/cacheUtils'; +import type { Identifier } from '../types'; +import { getIdentifierAtPosition as getIdentifierAtMapPosition, isMapFile } from '../core/mapManager'; + +export const gotoDefinitionProvider: DefinitionProvider = { + async provideDefinition(document: TextDocument, position: Position): Promise { + if (isMapFile(document.uri)) { + return gotoIdentifier(getIdentifierAtMapPosition(position)); + } + + // Get the item from the active document cache, exit early if noop or non cached type + const item = getByDocPosition(document, position); + if (!item || item.context.matchType.noop || !item.context.matchType.cache) { + return undefined; + } + + // If we are already on a declaration, there is nowhere to goto. Returning current location + // indicates to vscode that we instead want to try doing "find references" + if (item.context.declaration || item.context.matchType.referenceOnly) { + return new Location(document.uri, position); + } + + // Goto the declaration if the identifier exists + return gotoIdentifier(item.identifier); + } +} + +function gotoIdentifier(identifier: Identifier | undefined): Location | undefined { + if (!identifier?.declaration) return undefined; + return decodeReferenceToLocation(identifier.declaration.uri, identifier.declaration.ref); +} diff --git a/src/provider/hoverProvider.ts b/src/provider/hoverProvider.ts new file mode 100644 index 0000000..a264da4 --- /dev/null +++ b/src/provider/hoverProvider.ts @@ -0,0 +1,101 @@ +import type { ExtensionContext, HoverProvider, MarkdownString, Position, TextDocument } from 'vscode'; +import type { Identifier, Item, MatchType } from '../types'; +import type { HoverDisplayItem } from '../enum/hoverDisplayItems'; +import { Hover } from 'vscode'; +import { buildFromDeclaration } from '../resource/identifierFactory'; +import { getDeclarationHoverItems, getReferenceHoverItems } from '../resource/hoverConfigResolver'; +import { markdownBase, appendTitle, appendInfo, appendValue, appendSignature, appendCodeBlock, appendDebugHover, appendOperatorHover, appendStringHover } from '../utils/markdownUtils'; +import { getFileDiagnostics } from '../core/diagnostics'; +import { getByDocPosition, getInterpolationRangeByDocPosition, getOperatorByDocPosition, getParsedWordByDocPosition, getStringRangeByDocPosition } from '../cache/activeFileCache'; +import { isDevMode } from '../core/devMode'; +import { getSettingValue, Settings } from '../core/settings'; +import { getIdentifierAtPosition as getMapIdentifierAtPosition, isMapFile } from '../core/mapManager'; +import { getMatchTypeById } from '../matching/matchType'; + +export const hoverProvider = function(context: ExtensionContext): HoverProvider { + return { + async provideHover(document: TextDocument, position: Position): Promise { + if (!getSettingValue(Settings.ShowHover)) return undefined; // Exit early if hover disabled + const markdown = markdownBase(context); + if (isMapFile(document.uri)) { + appendMapHover(markdown, position); + } else { + const item = getByDocPosition(document, position); + appendHover(markdown, document, position, item); + await appendDebug(markdown, document, position, item); + } + return new Hover(markdown); + } + }; +} + +function getIdentifier(item: Item) { + return item.identifier ?? (!item.context.matchType.cache ? buildFromDeclaration(item.word, item.context) : undefined); +} + +function appendMapHover(markdown: MarkdownString, position: Position) { + const identifier = getMapIdentifierAtPosition(position); + if (!identifier) return; + const match = getMatchTypeById(identifier.matchId); + if (!match) return; + const hoverDisplayItems = getHoverItems(false, match); + if (hoverDisplayItems.length === 0) return undefined; + appendIdentifierHover(markdown, identifier, hoverDisplayItems); +} + +function appendHover(markdown: MarkdownString, document: TextDocument, position: Position, item: Item | undefined): void { + // If theres a diagnostic issue at this location, exit early (do not display normal hover text) + const diagnostics = getFileDiagnostics(document.uri); + if (diagnostics.find(d => d.range.contains(position))) { + return undefined; + } + + // If no item was found exit early + if (!item || item.context.matchType.noop) { + return undefined; + } + + // If no config found, or no items to display then exit early + const hoverDisplayItems = getHoverItems(item.context.declaration, item.context.matchType); + if (hoverDisplayItems.length === 0) { + return undefined; + } + + // Try to get identifier, if not found or if hideDisplay property is set, then there is nothing to display + const identifier = getIdentifier(item); + if (!identifier || identifier.hideDisplay) { + return undefined; + } + + appendIdentifierHover(markdown, identifier, hoverDisplayItems, item.context.cert) +} + +function appendIdentifierHover(markdown: MarkdownString, identifier: Identifier, hoverItems: HoverDisplayItem[], isCert = false) { + // Append the registered hoverDisplayItems defined in the matchType for the identifier + appendTitle(identifier.name, identifier.fileType, identifier.matchId, markdown, identifier.id, isCert); + appendInfo(identifier, hoverItems, markdown); + appendValue(identifier, hoverItems, markdown); + appendSignature(identifier, hoverItems, markdown); + appendCodeBlock(identifier, hoverItems, markdown); +} + +function getHoverItems(isDeclaration: boolean, matchType: MatchType): HoverDisplayItem[] { + const hoverDisplayItems = isDeclaration ? getDeclarationHoverItems(matchType) : getReferenceHoverItems(matchType); + if (!Array.isArray(hoverDisplayItems) || hoverDisplayItems.length === 0) { + return []; + } + return hoverDisplayItems; +} + +async function appendDebug(markdown: MarkdownString, document: TextDocument, position: Position, item: Item | undefined): Promise { + if (isDevMode()) { + if (item) return appendDebugHover(markdown, item.context.word, item.context, getIdentifier(item)); + const parsedWord = getParsedWordByDocPosition(position); + if (parsedWord) return appendDebugHover(markdown, parsedWord); + const operator = getOperatorByDocPosition(position); + if (operator) return appendOperatorHover(markdown, operator); + const stringRange = getStringRangeByDocPosition(position); + const interpRange = getInterpolationRangeByDocPosition(position); + if (stringRange && !interpRange) return appendStringHover(markdown, stringRange); + } +} diff --git a/src/provider/mapCodelensProvider.ts b/src/provider/mapCodelensProvider.ts new file mode 100644 index 0000000..a3e389b --- /dev/null +++ b/src/provider/mapCodelensProvider.ts @@ -0,0 +1,27 @@ +import type { CodeLensProvider, TextDocument } from "vscode"; +import { CodeLens, Range } from "vscode"; +import { getMapSectionHeaders } from "../core/mapManager"; + +export const mapCodelensProvider: CodeLensProvider = { + provideCodeLenses(document: TextDocument): CodeLens[] { + const sections = getMapSectionHeaders(document); + if (sections.length < 2) return []; + + const lenses: CodeLens[] = []; + for (let i = 0; i < sections.length; i++) { + const current = sections[i]!; + const next = sections[(i + 1) % sections.length]!; + const range = new Range(current.line, 0, current.line, 0); + const lineLabel = next.line + 1; + const title = next.name === 'MAP' + ? 'Return to top' + : `Jump to ${next.name} section (line ${lineLabel})`; + lenses.push(new CodeLens(range, { + title, + command: 'RuneScriptLanguage.jumpToMapSection', + arguments: [next.line] + })); + } + return lenses; + } +}; diff --git a/src/provider/referenceProvider.ts b/src/provider/referenceProvider.ts new file mode 100644 index 0000000..54eb0e9 --- /dev/null +++ b/src/provider/referenceProvider.ts @@ -0,0 +1,38 @@ +import type { Location, Position, ReferenceProvider, TextDocument} from 'vscode'; +import { Uri } from 'vscode'; +import { decodeReferenceToLocation } from '../utils/cacheUtils'; +import { getByDocPosition } from '../cache/activeFileCache'; + +export const referenceProvider: ReferenceProvider = { + async provideReferences(document: TextDocument, position: Position): Promise { + // Get the item from the active document cache, exit early if noop or non cached type + const item = getByDocPosition(document, position); + if (!item || item.context.matchType.noop || !item.context.matchType.cache) { + return []; + } + + // Check that the identifier exists and has references + if (!item.identifier || !item.identifier.references) { + return []; + } + + // Decode all the references for the identifier into an array of vscode Location objects + const referenceLocations: Location[] = []; + Object.keys(item.identifier.references).forEach(fileKey => { + const uri = Uri.file(fileKey); + item.identifier!.references[fileKey].forEach(encodedReference => { + const location = decodeReferenceToLocation(uri, encodedReference); + if (location) { + referenceLocations.push(location); + } + }); + }); + + // If there is only one reference and its the declaration, return empty list as theres no other references to show + if (item.context.declaration && referenceLocations.length === 1) { + return []; + } + + return referenceLocations; + } +} diff --git a/src/provider/renameProvider.ts b/src/provider/renameProvider.ts new file mode 100644 index 0000000..3418036 --- /dev/null +++ b/src/provider/renameProvider.ts @@ -0,0 +1,97 @@ +import type { Position, Range, RenameProvider, TextDocument } from 'vscode'; +import type { MatchContext, MatchType, Identifier } from '../types'; +import { Uri, WorkspaceEdit, workspace } from 'vscode'; +import { decodeReferenceToRange } from '../utils/cacheUtils'; +import { MODEL } from '../matching/matchType'; +import { getByDocPosition } from '../cache/activeFileCache'; + +export const renameProvider: RenameProvider = { + async prepareRename(document: TextDocument, position: Position): Promise { + // Get the item from the active document cache + const item = getByDocPosition(document, position); + if (!item) { + throw new Error("Cannot rename"); + } + if (!item.context.matchType.allowRename || item.context.matchType.noop) { + throw new Error(`${item.context.matchType.id} renaming not supported`); + } + if (!item.identifier) { + throw new Error('Cannot find any references to rename'); + } + const wordRange = document.getWordRangeAtPosition(position); + if (wordRange) { + return { range: wordRange, placeholder: item.word }; + } + return wordRange; + }, + + async provideRenameEdits(document: TextDocument, position: Position, newName: string): Promise { + // Get the item from the active document cache + const item = getByDocPosition(document, position); + if (!item) { + return undefined; + } + + const adjustedNewName = adjustNewName(item.context, newName); + await renameFiles(item.context.matchType, item.word, adjustedNewName); + return renameReferences(item.identifier, item.word, adjustedNewName); + } +} + +// Decode all the references for the identifier into an array of vscode ranges, +// then use that to rename all of the references to the newName +function renameReferences(identifier: Identifier | undefined, oldName: string, newName: string): WorkspaceEdit { + const renameWorkspaceEdits = new WorkspaceEdit(); + if (identifier?.references) { + Object.keys(identifier.references).forEach(fileKey => { + const uri = Uri.file(fileKey); + identifier.references[fileKey].forEach((encodedReference: string) => { + const range = decodeReferenceToRange(encodedReference); + if (range) { + renameWorkspaceEdits.replace(uri, range, newName); + } + }); + }); + } + return renameWorkspaceEdits; +} + +function adjustNewName(context: MatchContext, newName: string): string { + // Strip the cert_ and the _ prefix on objs or categories + if (context.originalPrefix && newName.startsWith(context.originalPrefix)) { + newName = newName.substring(context.originalPrefix.length); + } + // Strip the suffixes off + if (context.originalSuffix && newName.endsWith(context.originalSuffix)) { + newName = newName.slice(0, -context.originalSuffix.length); + } + // Strip the left side of identifier names with colons in them + if (newName.indexOf(':') > -1) { + newName = newName.substring(newName.indexOf(':') + 1); + } + return newName; +} + +async function renameFiles(match: MatchType, oldName: string, newName: string): Promise { + if (match.renameFile && Array.isArray(match.fileTypes) && match.fileTypes.length > 0) { + // Find files to rename + let files: Uri[] = []; + const ext = match.fileTypes[0]; + if (match.id === MODEL.id) { + files = await workspace.findFiles(`**/${oldName}*.${ext}`) || []; + const regex = new RegExp(`^(?:${oldName}\\.${ext}|${oldName}_[^/]\\.${ext})$`); + files = files.filter(uri => regex.test(uri.path.split('/').pop()!)); + } else { + files = await workspace.findFiles(`**/${oldName}.${ext}`) || []; + } + + // Rename the files + for (const oldUri of files) { + const oldFileName = oldUri.path.split('/').pop(); + const suffix = oldFileName?.startsWith(`${oldName}_`) ? oldFileName!.slice(oldName.length + 1, oldFileName!.lastIndexOf('.')) : ''; + const newFileName = suffix ? `${newName}_${suffix}.${ext}` : `${newName}.${ext}`; + const newUri = Uri.joinPath(oldUri.with({ path: oldUri.path.replace(/\/[^/]+$/, '') }), newFileName); + await workspace.fs.rename(oldUri, newUri); + } + } +} diff --git a/src/provider/semanticTokensProvider.ts b/src/provider/semanticTokensProvider.ts new file mode 100644 index 0000000..c4a8305 --- /dev/null +++ b/src/provider/semanticTokensProvider.ts @@ -0,0 +1,46 @@ +import type { CancellationToken, DocumentSemanticTokensProvider, TextDocument } from 'vscode'; +import { EventEmitter, SemanticTokensLegend, SemanticTokensBuilder } from 'vscode'; +import { getActiveCacheFile, getAllMatches } from '../cache/activeFileCache'; +import { SemanticTokenType } from '../enum/semanticTokens'; + +const tokenTypes = Object.values(SemanticTokenType); +const tokenTypeIndex = new Map(tokenTypes.map((t, i) => [t, i])); +const tokenModifiers: string[] = []; +const tokensChanged = new EventEmitter(); +let enabled = false; +let cachedTokens: ReturnType | undefined; + +export const semanticTokensLegend = new SemanticTokensLegend(tokenTypes, tokenModifiers); + +export const semanticTokensProvider: DocumentSemanticTokensProvider = { + onDidChangeSemanticTokens: tokensChanged.event, + provideDocumentSemanticTokens(document: TextDocument, _token: CancellationToken) { + // Only provide tokens for the document the active cache was built for. + if (document.uri.fsPath !== getActiveCacheFile()) { + return new SemanticTokensBuilder(semanticTokensLegend).build(); + } + if (!enabled) { + return cachedTokens ?? new SemanticTokensBuilder(semanticTokensLegend).build(); + } + enabled = false; + const builder = new SemanticTokensBuilder(semanticTokensLegend); + const typesWithSemanticTokenConfig = getAllMatches().filter(match => match.context.matchType.semanticTokenConfig !== undefined) + for (const wordMatch of typesWithSemanticTokenConfig) { + const tokenConfig = wordMatch.context.matchType.semanticTokenConfig; + const token = wordMatch.context.declaration ? tokenConfig?.declaration : tokenConfig?.reference; + if (!token) continue; + const lineNum = wordMatch.context.line.number; + const start = wordMatch.context.word.start; + const length = wordMatch.context.word.end - wordMatch.context.word.start + 1; + builder.push(lineNum, start, length, tokenTypeIndex.get(token)!, 0); + } + const tokens = builder.build(); + cachedTokens = tokens; + return tokens; + } +}; + +export function rebuildSemanticTokens(): void { + enabled = true; + tokensChanged.fire(); +} diff --git a/src/provider/signatureHelp/configSignatureHelpProvider.ts b/src/provider/signatureHelp/configSignatureHelpProvider.ts new file mode 100644 index 0000000..8fd8321 --- /dev/null +++ b/src/provider/signatureHelp/configSignatureHelpProvider.ts @@ -0,0 +1,45 @@ +import type { Position, TextDocument} from 'vscode'; +import { ParameterInformation, SignatureHelp, SignatureInformation } from 'vscode'; +import { buildMatchContext } from '../../utils/matchUtils'; +import { waitForActiveFileRebuild } from '../../core/eventHandlers'; +import { parseLineWithStateSnapshot } from '../../parsing/lineParser'; +import { getFileInfo } from '../../utils/fileUtils'; +import { getConfigLineMatch } from '../../matching/matchers/configMatcher'; + +let lastRequestId = 0; + +export const configMetadata = { + triggerCharacters: ['=', ','], + retriggerCharacters: [','] +} + +export const configHelpProvider = { + async provideSignatureHelp(document: TextDocument, position: Position) { + // Try to find a config line match for current cursor position to display signature help for + const requestId = ++lastRequestId; + await waitForActiveFileRebuild(document); + if (requestId !== lastRequestId) return undefined; // guard debounce, only continue with 1 result + let str = document.lineAt(position.line).text; + str = str.substring(0, position.character) + 'temp' + str.substring(position.character); + const parsedWords = parseLineWithStateSnapshot(str, position.line, document.uri); + const wordIndex = parsedWords.findIndex(w => w.start <= position.character && w.end >= position.character); + if (wordIndex < 0) return undefined; + const config = getConfigLineMatch(buildMatchContext(document.uri, parsedWords, document.lineAt(position.line).text, position.line, wordIndex, getFileInfo(document.uri))) + if (!config) return undefined; + + //Build the signature info + const signatureInfo = new SignatureInformation(`${config.key}=${config.params.join(',')}`); + let index = config.key.length + 1; // starting line character index of params (+1 for the '=') + config.params.forEach(paramName => { + signatureInfo.parameters.push(new ParameterInformation([index, index + paramName.length])); + index += paramName.length + 1; // increment index by the length of the param name (+1 for the ',') + }); + signatureInfo.activeParameter = config.index - 1; + + // Build the signature help + const signatureHelp = new SignatureHelp(); + signatureHelp.signatures.push(signatureInfo); + signatureHelp.activeSignature = 0; + return signatureHelp; + } +} diff --git a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts new file mode 100644 index 0000000..8f75ff7 --- /dev/null +++ b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts @@ -0,0 +1,122 @@ +import type { Position, SignatureHelpProvider, SignatureHelpProviderMetadata, TextDocument } from 'vscode'; +import { ParameterInformation, SignatureHelp, SignatureInformation } from 'vscode'; +import { TRIGGER, UNKNOWN } from '../../matching/matchType'; +import { runescriptTrigger } from '../../resource/triggers'; +import { waitForActiveFileRebuild } from '../../core/eventHandlers'; +import { getCallStateAtPosition } from '../../parsing/lineParser'; +import { getCallIdentifier } from '../../cache/activeFileCache'; + +export const signatureMetadata: SignatureHelpProviderMetadata = { + triggerCharacters: ['(', ',', '['], + retriggerCharacters: [','] +} + +export const signatureHelpProvider: SignatureHelpProvider = { + provideSignatureHelp(document: TextDocument, position: Position) { + const triggerHelp = getScriptTriggerHelp(document, position); + if (triggerHelp) { + return triggerHelp; + } + return getParametersHelp(document, position); + } +} + +/** + * Returns a "signature" help for showing what type type when writting script trigger lines + * @param document document to find build the signature help for + * @param position position within the document to build the corresponding signature help for + * @returns the signature help built, if any + */ +function getScriptTriggerHelp(document: TextDocument, position: Position): SignatureHelp | undefined { + let matchTypeId = UNKNOWN.id; + let signatureInfo: SignatureInformation | undefined; + const str = document.lineAt(position.line).text; + if (str.charAt(0) === '[') { + if (position.character > str.indexOf(']')) { + return undefined; + } + const split = str.split(','); + if (split.length > 1) { + const triggerName = split[0].substring(1); + const trigger = runescriptTrigger[triggerName]; + if (trigger) { + matchTypeId = trigger.declaration ? UNKNOWN.id : trigger.match.id; + const matchLabel = matchTypeId === UNKNOWN.id ? `script_name` : matchTypeId.toLowerCase(); + signatureInfo = new SignatureInformation(`script [${triggerName},${matchLabel}]`); + signatureInfo.parameters.push(new ParameterInformation(triggerName)); + signatureInfo.parameters.push(new ParameterInformation(matchLabel)); + signatureInfo.activeParameter = 1; + } + } else { + matchTypeId = TRIGGER.id; + signatureInfo = new SignatureInformation('script [trigger,value]'); + signatureInfo.parameters.push(new ParameterInformation('trigger')); + signatureInfo.parameters.push(new ParameterInformation('value')); + signatureInfo.activeParameter = 0; + } + } + if (signatureInfo) { + const signatureHelp = new SignatureHelp(); + signatureHelp.signatures.push(signatureInfo); + signatureHelp.activeSignature = 0; + return signatureHelp; + } +} + +let lastRequestId = 0; + +/** + * Returns a signature help for the signature of the call function user is typing in (if any) + * @param document document to find signature for + * @param position position within the document to find the corresponding signature help for + * @returns the signature help object, if any + */ +async function getParametersHelp(document: TextDocument, position: Position): Promise { + // We need the parser and active file cache states up to date + const requestId = ++lastRequestId; + await waitForActiveFileRebuild(document); + if (requestId !== lastRequestId) return undefined; // guard debounce, only continue with 1 result + + // Get the callState at the position in the line of text to get the call info and param index + const callState = getCallStateAtPosition(document.lineAt(position.line).text, position.line, document.uri, position.character); + if (!callState?.callName || callState.callNameIndex === undefined || callState.callNameIndex < 0 || callState.paramIndex === undefined) { + return undefined; + } + + // Retrieve the call identifier from the active file cache to access its signature + const identifier = getCallIdentifier(document.uri, position.line, callState.callName, callState.callNameIndex); + if (!identifier?.signature) { + return undefined; + } + + // Build useful flags and display warning for commands that don't take params if attempted + const hasParams = (identifier.signature.params ?? []).length > 0; + const hasReturnTypes = (identifier.signature.returns ?? []).length > 0; + if (!hasParams) { + return displayMessage(`${identifier.matchId} ${identifier.name} has no parameters, remove the parenthesis`); + } + + // Build the signature info and label + const label = `${identifier.name}(${identifier.signature.paramsText})${hasReturnTypes ? `: ${identifier.signature.returnsText}` : ''}`; + const signatureInfo = new SignatureInformation(label); + identifier.signature.paramsText.split(',').forEach(param => signatureInfo.parameters.push(new ParameterInformation(param.trim()))); + signatureInfo.activeParameter = callState.paramIndex; + + // Build the signature help + const signatureHelp = new SignatureHelp(); + signatureHelp.signatures.push(signatureInfo); + signatureHelp.activeSignature = 0; + return signatureHelp; +} + +/** + * Returns a signature help which only displays a simple message, useful for displaying warnings or errors + * @param message Message to display + */ +function displayMessage(message: string): SignatureHelp { + const signatureInfo = new SignatureInformation(message); + const signatureHelp = new SignatureHelp(); + signatureHelp.signatures.push(signatureInfo); + signatureHelp.activeSignature = 0; + return signatureHelp; +} diff --git a/client/info/configKeyInfo.js b/src/resource/configKeyInfo.ts similarity index 66% rename from client/info/configKeyInfo.js rename to src/resource/configKeyInfo.ts index ec589ac..2901518 100644 --- a/client/info/configKeyInfo.js +++ b/src/resource/configKeyInfo.ts @@ -1,13 +1,20 @@ -const { expandCsvKeyObject } = require("../utils/matchUtils"); +import { expandCsvKeyObject } from "../utils/matchUtils"; + +/** +* Defines any config keys with info that will be displayed when the user hovers over that config key +* Format: { key: { 'any': 'info for any fileType', 'obj': 'obj specific info', 'loc, npc': 'loc and npc specific info' } } +* You can define different info for specific file types, or use 'any' to apply to all file types (unless already defined) +* Tip: you can use the same value for multiple file types using a key as a CSV (i.e. use 'obj, loc, npc' as a key) +* Tip: config key will apply to all tags which end in numbers (for example stock will apply to stock1, stock2, stock100, etc...) +* Tip: you can use $TYPE which will be replaced by the file type of the config (loc, obj, etc...) +*/ +function expandInfo(obj: Record>): Record> { + Object.keys(obj).forEach(key => { + obj[key] = expandCsvKeyObject(obj[key]); + }); + return obj; +} -/** - * Defines any config keys with info that will be displayed when the user hovers over that config key - * Format: { key: { 'any': 'info for any fileType', 'obj': 'obj specific info', 'loc, npc': 'loc and npc specific info' } } - * You can define different info for specific file types, or use 'any' to apply to all file types (unless already defined) - * Tip: you can use the same value for multiple file types using a key as a CSV (i.e. use 'obj, loc, npc' as a key) - * Tip: config key will apply to all tags which end in numbers (for example stock will apply to stock1, stock2, stock100, etc...) - * Tip: you can use $TYPE which will be replaced by the file type of the config (loc, obj, etc...) - */ const configKeyInfo = expandInfo({ type: { 'varp': 'The data type of this player variable' }, param: { 'any': 'A param value in the format "paramName,value"' }, @@ -26,22 +33,16 @@ const configKeyInfo = expandInfo({ startbit: { 'varbit': 'The starting bit range on the basevar to limit this view to.' }, endbit: { 'varbit': 'The ending bit range on the basevar to limit this view to.' }, }); -function expandInfo(obj) { - Object.keys(obj).forEach(key => obj[key] = expandCsvKeyObject(obj[key])); - return obj; -} -// Find info for a given config key. If no fileType, will match config keys for 'any' type. Else, return null. -function matchConfigKeyInfo(key, fileType) { +// Find info for a given config key. If no fileType, will match config keys for 'any' type. +export function matchConfigKeyInfo(key: string, fileType: string): string | undefined { const endingNums = key.match(/\d+$/); if (endingNums) { - key = key.substring(0, key.indexOf(endingNums)); + key = key.substring(0, key.indexOf(endingNums[0])); } const info = configKeyInfo[key]; - if (info[fileType]) { + if (info && info[fileType]) { return info[fileType]; } - return info.any; + return info?.any; } - -module.exports = matchConfigKeyInfo; diff --git a/src/resource/configKeys.ts b/src/resource/configKeys.ts new file mode 100644 index 0000000..f54bd40 --- /dev/null +++ b/src/resource/configKeys.ts @@ -0,0 +1,114 @@ +import { DBCOLUMN, ENUM, PARAM } from "../matching/matchType"; +import type { ConfigKeyData } from "../types"; + +/** + * The source of the identifier for config keys which have dynamic varargs + * Used by the matcher to retrieve the identifier which contains the signature type params + */ +export enum ConfigVarArgSrc { + BlockName = 'blockName', + FirstParam = 'firstParam' +} + +/** + * Extends normal config key data for regex matching + */ +interface RegexConfigData extends ConfigKeyData { + /** The regular expression which will be tested against the config keys to find if it matches */ + regex: RegExp; + /** The file types that this will be testing the keys on */ + fileTypes?: string[]; +} + +/** + * Defines static config keys (direct match) + */ +const configKeys: Record = { + walkanim: { params: ['seq', 'seq', 'seq', 'seq'] }, + multivar: { params: ['var'] }, + multiloc: { params: ['int', 'loc'] }, + multinpc: { params: ['int', 'npc'] }, + basevar: { params: ['var'] }, + + category: { params: ['category'] }, + huntmode: { params: ['hunt'] }, + table: { params: ['dbtable'] }, + check_category: { params: ['category'] }, + check_inv: { params: ['inv', 'namedobj'] }, + + param: { params: ['param'], varArgs: {startIndex: 1, idenSrc: ConfigVarArgSrc.FirstParam, idenType: PARAM.id}}, + val: { params: [], varArgs: {startIndex: 0, idenSrc: ConfigVarArgSrc.BlockName, idenType: ENUM.id}}, + data: { params: ['dbcolumn'], varArgs: {startIndex: 1, idenSrc: ConfigVarArgSrc.FirstParam, idenType: DBCOLUMN.id}}, +}; + +/** + * Defines regex config keys (check key against regex to find match) + */ +const regexConfigKeys: Map = groupByFileType([ + { regex: /stock\d+/, params: ['obj', 'int', 'int'], fileTypes: ["inv"] }, + { regex: /count\d+/, params: ['obj', 'int'], fileTypes: ["obj"] }, + { regex: /frame\d+/, params: ['frame'], fileTypes: ["seq"] }, + { regex: /(model|head|womanwear|manwear|womanhead|manhead|activemodel)\d*/, params: ['ob2'], fileTypes:['npc', 'loc', 'obj', 'spotanim', 'if', 'idk'] }, + { regex: /\w*anim\w*/, params: ['seq'], fileTypes: ["loc", "npc", "if", "spotanim"] }, + { regex: /replaceheldleft|replaceheldright/, params: ['obj'], fileTypes: ["seq"], ignoreValues: ["hide"] }, +]); + +/** + * Get the defined config key data, if any + * @param configKey the name of the config key to find a match for + * @param fileType the file type the config key is in + * @returns the config key data, if any + */ +export function getConfigData(configKey: string, fileType: string): ConfigKeyData | undefined { + return configKeys[configKey] ?? checkRegexConfigKeys(configKey, fileType); +} + +/** + * Caches config keys found during matching, used by completion provider to suggest values + */ +const observedConfigKeys = new Set(); + +/** + * Learn a new config key name (save to the cache) + * @param key config key name + */ +export function learnConfigKey(key: string): void { + observedConfigKeys.add(key); +} + +/** + * Returns all of the learned config keys so far + */ +export function getObservedConfigKeys(): Set { + return observedConfigKeys; +} + +/** + * Check a config key against all of the config key regexes applicable to that file type + * @param configKey config key to check against regex + * @param fileType file type where the config key is in + * @returns the matched config key data, if any + */ +function checkRegexConfigKeys(configKey: string, fileType: string): ConfigKeyData | undefined { + for (const regexConfig of (regexConfigKeys.get(fileType) || [])) { + if (regexConfig.regex.test(configKey)) return regexConfig; + } +} + +/** + * Groups the array of regex config data into a quick lookup of valid regex to check by file type + * @param config the regex config data + */ +function groupByFileType(config: RegexConfigData[]): Map { + const result = new Map(); + for (const { regex, params, fileTypes, ignoreValues } of config) { + const safeFileTypes = fileTypes ?? []; + for (const fileType of safeFileTypes) { + if (!result.has(fileType)) { + result.set(fileType, []); + } + result.get(fileType)!.push({ regex: regex, params: params, ignoreValues: ignoreValues }); + } + } + return result; +} diff --git a/src/resource/dataTypeToMatchId.ts b/src/resource/dataTypeToMatchId.ts new file mode 100644 index 0000000..c697d58 --- /dev/null +++ b/src/resource/dataTypeToMatchId.ts @@ -0,0 +1,18 @@ +import { getAllMatchTypes, getMatchTypeById, SKIP } from "../matching/matchType"; +import type { MatchType } from "../types"; + +const keywordToId: Record = {}; + +getAllMatchTypes().forEach(match => { + for (const keyword of (match.types || [])) { + keywordToId[keyword] = match.id; + } +}); + +export function dataTypeToMatchId(keyword: string): string { + return keywordToId[keyword] || SKIP.id; +} + +export function dataTypeToMatchType(keyword: string): MatchType { + return getMatchTypeById(dataTypeToMatchId(keyword))!; +} diff --git a/src/resource/hoverConfigResolver.ts b/src/resource/hoverConfigResolver.ts new file mode 100644 index 0000000..9df4e1e --- /dev/null +++ b/src/resource/hoverConfigResolver.ts @@ -0,0 +1,29 @@ +import type { MatchType } from '../types'; +import type { HoverDisplayItem } from "../enum/hoverDisplayItems"; + +export function getDeclarationHoverItems(match: MatchType): HoverDisplayItem[] { + return match.hoverConfig?.declarationItems ?? []; +} + +export function getReferenceHoverItems(match: MatchType): HoverDisplayItem[] { + return match.hoverConfig?.referenceItems ?? []; +} + +export function getHoverLanguage(match: MatchType): string { + return match.hoverConfig?.language ?? 'runescript'; +} + +export function getBlockSkipLines(match: MatchType): number { + return match.hoverConfig?.blockSkipLines ?? 1; +} + +export function getConfigInclusions(match: MatchType): string[] | undefined { + return match.hoverConfig?.configInclusions ?? undefined; +} + +export function resolveAllHoverItems(match: MatchType): Set { + const displayItems = new Set(); + getDeclarationHoverItems(match).forEach(item => displayItems.add(String(item))); + getReferenceHoverItems(match).forEach(item => displayItems.add(String(item))); + return displayItems; +} diff --git a/src/resource/identifierFactory.ts b/src/resource/identifierFactory.ts new file mode 100644 index 0000000..6502c8a --- /dev/null +++ b/src/resource/identifierFactory.ts @@ -0,0 +1,167 @@ +import type { Identifier, IdentifierText, MatchContext, MatchResult, MatchType } from '../types'; +import type { Uri } from 'vscode'; +import { dataTypeToMatchId } from './dataTypeToMatchId'; +import { getBlockSkipLines, getConfigInclusions, getHoverLanguage, resolveAllHoverItems } from './hoverConfigResolver'; +import { SIGNATURE, CODEBLOCK } from '../enum/hoverDisplayItems'; +import { END_OF_BLOCK_LINE_REGEX, INFO_MATCHER_REGEX } from '../enum/regex'; +import { encodeReference, resolveIdentifierKey } from '../utils/cacheUtils'; +import { put as putIdentifier, putReference } from '../cache/identifierCache'; +import { add as addToIdCache } from '../cache/idCache'; + +export function buildAndCacheIdentifier(match: MatchResult, uri: Uri, lineNum: number, lines: string[]): void { + if (!match.context.matchType.cache) return; + if (match.context.declaration) { + const startIndex = Math.max(lineNum - 1, 0); + putIdentifier(match.word, match.context, { lines: lines.slice(startIndex), start: lineNum - startIndex }); + } else { + let index = match.context.word.start; + if (!match.context.originalWord && match.word.indexOf(':') > 0) index += match.word.indexOf(':') + 1; + putReference(match.word, match.context, uri, lineNum, index, match.context.word.end); + } +} + +export function buildFromDeclaration(name: string, context: MatchContext, text?: IdentifierText): Identifier { + const identifier: Identifier = { + name: name, + matchId: context.matchType.id, + cacheKey: resolveIdentifierKey(context.word.value, context.matchType), + declaration: { uri: context.uri, ref: encodeReference(context.line.number, context.word.start, context.word.end) }, + references: {}, + fileType: context.uri.fsPath.split(/[#?]/)[0].split('.').pop()!.trim(), + language: getHoverLanguage(context.matchType), + }; + process(identifier, context, text); + return identifier; +} + +export function buildFromReference(name: string, context: MatchContext): Identifier { + const identifier: Identifier = { + name: name, + matchId: context.matchType.id, + cacheKey: resolveIdentifierKey(context.word.value, context.matchType), + references: {}, + fileType: (context.matchType.fileTypes || [])[0] || 'rs2', + language: getHoverLanguage(context.matchType), + }; + if (context.matchType.referenceOnly) { + process(identifier, context); + } + return identifier; +} + +export function addReference(identifier: Identifier, fileKey: string, lineNum: number, startIndex: number, endIndex: number, context?: MatchContext): Set { + const fileReferences = identifier.references[fileKey] || new Set(); + fileReferences.add(encodeReference(lineNum, startIndex, endIndex)); + if (context && context.packId) { + identifier.id = context.packId; + addToIdCache(dataTypeToMatchId(context.file.name), context.packId, context.word.value); + } + return fileReferences; +} + +function process(identifier: Identifier, context: MatchContext, text?: IdentifierText): void { + // Set the comparisonType for explicit comparison types from the match type + if (context.matchType.comparisonType !== undefined) identifier.comparisonType = context.matchType.comparisonType; + + // Add extra data if any + const extraData = context.extraData; + if (extraData) { + if (!identifier.extraData) identifier.extraData = {}; + Object.keys(extraData).forEach(key => { + if (identifier.extraData) { + identifier.extraData[key] = extraData[key]; + } + }); + } + + // Process hover display texts + if (text) { + if (identifier.declaration) processInfoText(identifier, text); + const hoverDisplayItems = resolveAllHoverItems(context.matchType); + for (const hoverDisplayItem of hoverDisplayItems) { + switch(hoverDisplayItem) { + case SIGNATURE: processSignature(identifier, text); break; + case CODEBLOCK: processCodeBlock(identifier, context.matchType, text); break; + } + } + } + + // Execute custom post processing for the identifier's matchType (if defined) + if (context.matchType.postProcessor) { + context.matchType.postProcessor(identifier); + } +} + +function processSignature(identifier: Identifier, text: IdentifierText): void { + // Get first line of text, which should contain the data for parsing the signature + let line = text.lines[text.start]; + if (!line) return; + + // Parse input params + const params: Array<{ type: string; name: string; matchTypeId: string }> = []; + let openingIndex = line.indexOf('('); + let closingIndex = line.indexOf(')'); + if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { + line.substring(openingIndex, closingIndex).split(',').forEach(param => { + if (param.startsWith(' ')) param = param.substring(1); + const split = param.split(' '); + if (split.length === 2) { + params.push({ type: split[0], name: split[1], matchTypeId: dataTypeToMatchId(split[0]) }); + } + }); + } + + // Parse response type + let returns: string[] = []; + let returnsText = ''; + line = line.substring(closingIndex + 1); + openingIndex = line.indexOf('('); + closingIndex = line.indexOf(')'); + if (openingIndex >= 0 && closingIndex >= 0 && ++openingIndex !== closingIndex) { + returnsText = line.substring(openingIndex, closingIndex); + returns = line.substring(openingIndex, closingIndex).split(',').map(item => dataTypeToMatchId(item.trim())); + } + + // Add signature to identifier + const paramsText = (params.length > 0) ? params.map(param => `${param.type} ${param.name}`).join(', ') : ''; + identifier.signature = { params, returns, paramsText, returnsText }; +} + +function processCodeBlock(identifier: Identifier, match: MatchType, text: IdentifierText): void { + const lines = text.lines; + const startIndex = text.start + Number(getBlockSkipLines(match)); + const configInclusionTags = getConfigInclusions(match); + let blockInclusionLines: string[] = []; + const matchType = match; + + if (matchType.id === 'CONSTANT' && lines[startIndex]) blockInclusionLines.push(lines[startIndex]); + let i = startIndex + for (; i < lines.length; i++) { + let currentLine = lines[i]; + if (END_OF_BLOCK_LINE_REGEX.test(currentLine)) break; + if (currentLine.startsWith('//')) continue; + if (configInclusionTags && !configInclusionTags.some((inclusionTag: string) => currentLine.startsWith(inclusionTag))) continue; + blockInclusionLines.push(currentLine); + } + identifier.block = { code: blockInclusionLines.join('\n') }; +} + +function processInfoText(identifier: Identifier, text: IdentifierText): void { + if (text.start < 1) return; + const infoLine = text.lines[text.start - 1]; + if (!infoLine) return; + const infoMatch = INFO_MATCHER_REGEX.exec(infoLine); + if (infoMatch && infoMatch[2]) { + identifier.info = infoMatch[2].trim(); + } +} + +export function serializeIdentifier(identifier: Identifier): Record { + return { + ...identifier, + declaration: identifier.declaration ? { uri: identifier.declaration.uri.fsPath,ref: identifier.declaration.ref } : undefined, + references: Object.fromEntries( + Object.keys(identifier.references || {}).map(fileKey => [fileKey, Array.from(identifier.references[fileKey])]) + ) + }; +} diff --git a/src/resource/postProcessors.ts b/src/resource/postProcessors.ts new file mode 100644 index 0000000..a7b5183 --- /dev/null +++ b/src/resource/postProcessors.ts @@ -0,0 +1,99 @@ +import type { PostProcessor } from '../types'; +import { END_OF_LINE_REGEX } from '../enum/regex'; +import { matchConfigKeyInfo } from './configKeyInfo'; +import { matchTriggerInfo } from './triggerInfo'; +import { getLineText } from '../utils/stringUtils'; + +// Post processors are used for any additional post modification needed for a matchType, after an identifier has been built +// postProcessors must be a function which takes indentifier as an input, and directly modifies that identifier as necessary + +export const coordPostProcessor: PostProcessor = function(identifier) { + const coordinates = identifier.name.split('_'); + const xCoord = (Number(coordinates[1]) << 6) + Number(coordinates[3]); + const zCoord = (Number(coordinates[2]) << 6) + Number(coordinates[4]); + identifier.value = `Absolute coordinates: (${xCoord}, ${zCoord})`; +}; + +export const enumPostProcessor: PostProcessor = function(identifier) { + const block = identifier.block!; + const inputType = getLineText(block.code.substring(block.code.indexOf("inputtype="))).substring(10); + const outputType = getLineText(block.code.substring(block.code.indexOf("outputtype="))).substring(11); + const params = [{type: inputType, name: '', matchTypeId: ''}, {type: outputType, name: '', matchTypeId: ''}]; + identifier.signature = { params: params, paramsText: '', returns: [], returnsText: ''}; + identifier.comparisonType = outputType; +}; + +export const localVarPostProcessor: PostProcessor = function(identifier) { + identifier.comparisonType = identifier.extraData!.type; +}; + +export const globalVarPostProcessor: PostProcessor = function(identifier) { + const index = identifier.block!.code.indexOf("type="); + const dataType = (index < 0) ? 'int' : getLineText(identifier.block!.code.substring(index)).substring(5); + identifier.extraData = { dataType: dataType }; + identifier.comparisonType = dataType; +}; + +export const paramPostProcessor: PostProcessor = function(identifier) { + const index = identifier.block!.code.indexOf("type="); + const dataType = (index < 0) ? 'int' : getLineText(identifier.block!.code.substring(index)).substring(5); + identifier.signature = { params: [{type: dataType, name: '', matchTypeId: ''}], paramsText: '', returns: [], returnsText: ''}; + identifier.comparisonType = dataType; +}; + +export const configKeyPostProcessor: PostProcessor = function(identifier) { + const info = matchConfigKeyInfo(identifier.name, identifier.fileType); + if (info) { + identifier.info = info.replace(/\$TYPE/g, identifier.fileType); + } else { + identifier.hideDisplay = true; + } +}; + +export const triggerPostProcessor: PostProcessor = function(identifier) { + if (identifier.extraData) { + const info = matchTriggerInfo(identifier.name, identifier.extraData.triggerName); + if (info) identifier.info = info; + } +}; + +export const categoryPostProcessor: PostProcessor = function(identifier) { + const extraData = identifier.extraData; + if (extraData && extraData.matchId && extraData.categoryName) { + identifier.value = `This script applies to all ${extraData.matchId} with \`category=${extraData.categoryName}\``; + } +}; + +export const componentPostProcessor: PostProcessor = function(identifier) { + const split = identifier.name.split(':'); + identifier.info = `A component of the ${split[0]} interface`; + identifier.name = split[1]; +}; + +export const rowPostProcessor: PostProcessor = function(identifier) { + if (identifier.block) { + const tableName = (identifier.block.code.split('=') || ['', ''])[1]; + identifier.info = `A row in the ${tableName} table`; + delete identifier.block; + identifier.extraData = { table: tableName }; + } +}; + +const columnIgnoreTypes = new Set(['LIST','INDEXED','REQUIRED']); +export const columnPostProcessor: PostProcessor = function(identifier) { + const split = identifier.name.split(':'); + identifier.info = `A column of the ${split[0]} table`; + identifier.name = split[1]; + + if (!identifier.block) return; + const exec = END_OF_LINE_REGEX.exec(identifier.block.code); + if (!exec) return; + const types = identifier.block.code.substring(8 + identifier.name.length, exec.index).split(',').map(t => t.trim()).filter(t => !columnIgnoreTypes.has(t)); + const params = types.map(type => ({type: type, name: '', matchTypeId: ''})); + identifier.signature = { params: params, paramsText: '', returns: [], returnsText: ''}; + identifier.block.code = `Field types: ${types.join(', ')}`; +}; + +export const fileNamePostProcessor: PostProcessor = function(identifier) { + identifier.info = `Refers to the file ${identifier.name}.${identifier.fileType}`; +}; diff --git a/src/resource/triggerInfo.ts b/src/resource/triggerInfo.ts new file mode 100644 index 0000000..771c0b7 --- /dev/null +++ b/src/resource/triggerInfo.ts @@ -0,0 +1,16 @@ +import { expandCsvKeyObject } from "../utils/matchUtils"; + +/** +* Defines trigger information which will be displayed on hover if a user hovers over a trigger keyword +* Tip: You can use CSV keys such as 'oploc1, oploc2, oploc3' to apply the same info message for all of those triggers +* Tip: The string 'NAME' will be replaced with the actual triggers defined name [trigger,triggerName] +*/ +const triggerInfo = expandCsvKeyObject({ + logout: 'The script that executes when the user logs out', + debugproc: 'Proc that only runs for users with cheats enabled, run with ::NAME' +}); + +export function matchTriggerInfo(key: string, triggerName: string): string { + const info = triggerInfo[key]; + return (info || '').replace(/NAME/g, triggerName); +} diff --git a/src/resource/triggers.ts b/src/resource/triggers.ts new file mode 100644 index 0000000..cd3ef24 --- /dev/null +++ b/src/resource/triggers.ts @@ -0,0 +1,85 @@ +import type { MatchType } from '../types'; +import { COMMAND, COMPONENT, INTERFACE, LABEL, LOC, NPC, OBJ, PROC, QUEUE, SOFTTIMER, STAT, TIMER, UNKNOWN, WALKTRIGGER } from "../matching/matchType"; + +interface TriggerMatchType { + match: MatchType; + declaration: boolean; +} + +interface incrementingTriggerDefinition { + triggerName: string; + increments: number; + includeU: boolean; + includeT: boolean; + includeD: boolean; + defaultMatch: MatchType; +} + +function buildMatchForTrigger(match: MatchType, declaration: boolean): TriggerMatchType { + return { match, declaration }; +} + +const runescriptTrigger: Record = { + proc: buildMatchForTrigger(PROC, true), + label: buildMatchForTrigger(LABEL, true), + queue: buildMatchForTrigger(QUEUE, true), + weakqueue: buildMatchForTrigger(QUEUE, true), + longqueue: buildMatchForTrigger(QUEUE, true), + strongqueue: buildMatchForTrigger(QUEUE, true), + softtimer: buildMatchForTrigger(SOFTTIMER, true), + timer: buildMatchForTrigger(TIMER, true), + ai_timer: buildMatchForTrigger(NPC, false), + if_button: buildMatchForTrigger(COMPONENT, false), + if_close: buildMatchForTrigger(INTERFACE, false), + walktrigger: buildMatchForTrigger(WALKTRIGGER, true), + ai_walktrigger: buildMatchForTrigger(NPC, false), + ai_spawn: buildMatchForTrigger(NPC, false), + ai_despawn: buildMatchForTrigger(NPC, false), + debugproc: buildMatchForTrigger(PROC, true), + login: buildMatchForTrigger(UNKNOWN, true), + logout: buildMatchForTrigger(UNKNOWN, true), + tutorial: buildMatchForTrigger(UNKNOWN, true), + advancestat: buildMatchForTrigger(STAT, false), + changestat: buildMatchForTrigger(STAT, false), + mapzone: buildMatchForTrigger(UNKNOWN, true), + mapzoneexit: buildMatchForTrigger(UNKNOWN, true), + zone: buildMatchForTrigger(UNKNOWN, true), + zoneexit: buildMatchForTrigger(UNKNOWN, true), + command: buildMatchForTrigger(COMMAND, true) +}; + +// Builds a map value for each of the incrementing triggers (i.e. opncp1, opnpc2, ..., opnpc5) +// Also can specify which triggers have a U/T/D value (i.e. opnpcU) +const incrementingTriggers: incrementingTriggerDefinition[] = [ + { triggerName: 'opnpc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'apnpc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'ai_apnpc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'ai_opnpc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'opobj', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: OBJ }, + { triggerName: 'apobj', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: OBJ }, + { triggerName: 'ai_apobj', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: OBJ }, + { triggerName: 'ai_opobj', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: OBJ }, + { triggerName: 'oploc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: LOC }, + { triggerName: 'aploc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: LOC }, + { triggerName: 'ai_aploc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: LOC }, + { triggerName: 'ai_oploc', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: LOC }, + { triggerName: 'opplayer', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: UNKNOWN }, + { triggerName: 'applayer', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: UNKNOWN }, + { triggerName: 'ai_applayer', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'ai_opplayer', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: NPC }, + { triggerName: 'ai_queue', increments: 20, includeU: false, includeT: false, includeD: false, defaultMatch: NPC }, + { triggerName: 'opheld', increments: 5, includeU: true, includeT: true, includeD: false, defaultMatch: OBJ }, + { triggerName: 'inv_button', increments: 5, includeU: false, includeT: false, includeD: true, defaultMatch: COMPONENT }, +]; + +// Build the triggers with increments and U/T/D +incrementingTriggers.forEach(incTrigDef => { + for (let i = 1; i <= incTrigDef.increments; i++) { + runescriptTrigger[`${incTrigDef.triggerName}${i}`] = buildMatchForTrigger(incTrigDef.defaultMatch, false); + } + if (incTrigDef.includeU) runescriptTrigger[`${incTrigDef.triggerName}u`] = buildMatchForTrigger(incTrigDef.defaultMatch, false); + if (incTrigDef.includeT) runescriptTrigger[`${incTrigDef.triggerName}t`] = buildMatchForTrigger(COMPONENT, false); + if (incTrigDef.includeD) runescriptTrigger[`${incTrigDef.triggerName}d`] = buildMatchForTrigger(incTrigDef.defaultMatch, false); +}); + +export { runescriptTrigger }; diff --git a/src/runescriptExtension.ts b/src/runescriptExtension.ts new file mode 100644 index 0000000..d31cc07 --- /dev/null +++ b/src/runescriptExtension.ts @@ -0,0 +1,28 @@ +import type { ExtensionContext } from 'vscode'; +import { initializeExtension } from './core/manager'; + +/** + * Lanugages this extension is interested in + */ +export const languageIds = new Set([ + 'runescript','locconfig','objconfig','npcconfig','dbtableconfig','dbrowconfig','paramconfig','structconfig', + 'enumconfig','varpconfig','varbitconfig','varnconfig','varsconfig','invconfig','seqconfig','spotanimconfig', + 'mesanimconfig','idkconfig','huntconfig','constants','interface','pack','floconfig' +]); + +/** + * Runescript type keywords + */ +export const typeKeywords = new Set([ + 'int','string','boolean','seq','locshape','component','idk','midi','npc_mode','namedobj','synth','stat', + 'npc_stat','fontmetrics','enum','loc','model','npc','obj','player_uid','spotanim','npc_uid','inv','category', + 'struct','dbrow','interface','dbtable','coord','mesanim','param','queue','weakqueue','timer','softtimer', + 'char','dbcolumn','proc','label' +]) + +/** + * Runs when the extension is activated + */ +export function activate(context: ExtensionContext) { + initializeExtension(context); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a79604b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,320 @@ +import type { Uri } from 'vscode'; +import type { HoverDisplayItem } from './enum/hoverDisplayItems'; +import type { SemanticTokenType } from './enum/semanticTokens'; +import type { ConfigVarArgSrc } from './resource/configKeys'; + +/** + * Represents all of the parsed data from a file + */ +export interface ParsedFile { + /** All of the parsed words in the file, per line */ + parsedWords: Map; + /** All of the operator tokens in the file, per line */ + operatorTokens: Map; + /** All of the string ranges in the file, per line */ + stringRanges: Map; + /** All of the interpolated code ranges in the file, per line */ + interpolationRanges: Map; +} + +export interface TextRange { + start: number; + end: number; +} + +/** + * Definition of a parsed word + */ +export interface ParsedWord { + /** The actual string value of the word */ + value: string; + /** The start index of the word (int) */ + start: number; + /** The end index of the word (int) */ + end: number; + /** The index of this word on its line (int), as in of all the words on a line which index is this word */ + index: number; + /** Whether or not the word is inside a string */ + inString: boolean; + /** Whether or not the word is inside interpolated code (code inside a string) */ + inInterpolation: boolean; + /** The depth of parenthesis the word is in */ + parenDepth: number; + /** The depth of braces (curly brackets) the word is in */ + braceDepth: number; + /** The call name of the function this word is in, if any */ + callName?: string; + /** The word index of the call name word */ + callNameIndex?: number; + /** The param index this word is in inside a call function */ + paramIndex?: number; + /** The config key name for config value words */ + configKey?: string; +} + +/** Definition of a parsed operator token */ +export interface OperatorToken { + /** The operator text */ + token: string; + /** The character index where the operator starts */ + index: number; + /** The parenthesis depth at this operator */ + parenDepth: number; +} + +export interface FileInfo { name: string, type: string } + +/** + * The full match context for a matching a word + */ +export interface MatchContext { + /** The specific word that the match is for */ + word: ParsedWord; + /** An array of all of the words in the line of the found match */ + words: ParsedWord[]; + /** The file uri that the found match is in */ + uri: Uri; + /** The matchType of the matched word */ + matchType: MatchType; + /** Whether or not the match is a declaration (if false, its a reference) */ + declaration: boolean; + /** The line text and number (line number in the file) that the match is on */ + line: { text: string; number: number }; + /** The name and type of the file that the match is in */ + file: FileInfo; + /** The character index of the match within the line it is in */ + lineIndex: number; + /** The word that comes before the matched word (undefined if no previous words) */ + prevWord: ParsedWord | undefined; + /** The character that is right before the matched word */ + prevChar: string; + /** The character that is right after the matched word */ + nextChar: string; + /** The original word before modification */ + originalWord?: string; + /** The original prefix text if this is a modified word */ + originalPrefix?: string; + /** The original suffic text if this is a modified word */ + originalSuffix?: string; + /** Extra data that exists for this match, if any */ + extraData?: Record; + /** A boolean indicating if this match is a cert obj */ + cert?: boolean; + /** The pack ID for this match if it has one (ex: Obj ID 1234, only populated when matching *.pack files) */ + packId?: string; +} + +/** + * The data used to represent a signature of a proc or other type + */ +export interface Signature { + /** The parameters for the signature */ + params: Array<{ type: string; name: string; matchTypeId: string }>; + /** The return types for the signature */ + returns: string[]; + /** The precomputed single line text of the parameters, for display purposes */ + paramsText: string; + /** The precomputed single line text of the return types, for display purposes */ + returnsText: string; +} + +/** + * The definition of an identifier, identifiers are actual found matches of any matchType in the project files + * This stores all of the data necessary for the core functions of the extension + * (finding references, going to definitions, showing hover display info, etc...) + */ +export interface Identifier { + /** The name of an identifier */ + name: string; + /** The matchType ID of the identifier */ + matchId: string; + /** This is the pack id (such as Obj ID 1234), if it has one */ + id?: string; + /** The cache key for this identifier */ + cacheKey: string; + /** The location of the declaration/definition of the identifier, if it has one */ + declaration?: { uri: Uri; ref: string }; + /** The locations (encoded as string) of the references of the identifier */ + references: Record>; + /** The file type where the identifier exists/defined in */ + fileType: string; + /** The code language the identifier should use for syntax highlighting display purposes */ + language: string; + /** For displaying the identifiers info text on hover (italicized body text, always on first line of body text) */ + info?: string; + /** For referencing and displaying on hover the identifiers params and return types. */ + signature?: Signature; + /** For displaying the identifiers code line on hover */ + block?: { code: string }; + /** For displaying the identifiers value text on hover (plain body text, positioned below info text but above signature or code blocks) */ + value?: string; + /** Any extra data tied to this identifier */ + extraData?: Record; + /** Boolean indicating if hover text should not display for this identifier */ + hideDisplay?: boolean; + /** The type of value this identifier is or resolves to during comparison operations */ + comparisonType?: string; +} + +/** + * The item returned by the active file cache, contains MatchResult plus the actual identifer (if it exists) + */ +export interface Item extends MatchResult { + identifier?: Identifier +} + +/** + * Function format for post processors which run when an identifier is created + */ +export type PostProcessor = (identifier: Identifier) => void; + +/** + * Text info necessary for creating identifiers + */ +export interface IdentifierText { + /** The file text lines this identifier is in */ + lines: string[]; + /** The line number where this identifiers relevant code starts at */ + start: number; +} + +/** + * Tracks the keys of identifier declarations and references within a file + */ +export interface FileIdentifiers { + declarations: Set; + references: Set; +} + +/** + * The MatchType is the config that controls how identifiers are built, cached, and displayed + */ +export interface MatchType { + /** Unique identifier for the match type */ + id: string; + /** The types which can correspond to a matchtype (ex: [namedobj, obj] are types for the OBJ matchType) */ + types: string[]; + /** The file types where a matchType can be defined/declared */ + fileTypes?: string[]; + /** Override the color this type will show up as by assigning it to a semantic token type */ + semanticTokenConfig?: { declaration?: SemanticTokenType, reference?: SemanticTokenType } + /** Whether or not identifiers of this match type should be cached */ + cache: boolean; + /** Whether or not this match type can be a callable or have parameters (like PROC, LOC, COMMAND...) */ + callable?: boolean; + /** Whether or not identifiers of this type have only references (no definition/declaration). Used mainly for identifiers which refer to a file, like synths. */ + referenceOnly?: boolean; + /** Whether or not identifiers of this type should be allowed to be renamed (code change) */ + allowRename: boolean; + /** Whether or not identifiers declaration file name can be renamed (actual file rename) */ + renameFile?: boolean; + /** Whether or not identifiers of this type is no operation (used for finding matches and terminating matching early, but not ever cached or displayed) */ + noop?: boolean; + /** The config settings for the hover display of identifiers of this type */ + hoverConfig?: HoverConfig; + /** The comparison type that is *always* used for this matchType, if it has multiple possible comparison types such as constants, handle that in the identifier instead */ + comparisonType?: string; + /** Function that is executed after identifiers of this type have been created (allows for more dynamic runtime info with full context to be tied to an identifier) */ + postProcessor?: PostProcessor; +} + +/** + * Config which controls how the hover display is built + */ +export interface HoverConfig { + /** Hover items shown for declarations of a matchType */ + declarationItems?: HoverDisplayItem[]; + /** Hover items shown for references of a matchType */ + referenceItems?: HoverDisplayItem[]; + /** Language used for displaying code blocks of this matchType (for proper syntax highlighting) */ + language?: string; + /** Number of lines to skip for displaying a code block (defaults to 1 to skip the declaration line that most types have) */ + blockSkipLines?: number; + /** Config line items to include in code block. Undefined shows all config items (default). */ + configInclusions?: string[]; +} + +/** + * The data returned when a match is found + */ +export interface MatchResult { + /** The word that was matched */ + word: string; + /** Additional context for the match found */ + context: MatchContext; +} + +/** + * The definition of matchers. Lower priority runs first. Faster processing matchers should be given priority. + */ +export interface Matcher { + /** Lower priority runs first; Quick matchers should have high priority */ + priority: number; + /** Matcher function for a given match context. + * Matchers should mutate the context to set the MatchType and declaration boolean of the found match. + * Matchers should set SKIP matchType if you want to early terminate matching. + * Matchers should keep the default (UNKNOWN) matchType if there might be a match with a different matcher. */ + fn: (context: MatchContext) => void; +} + +/** + * Response type returned when params are matched + */ +export interface ParamsMatchResponse { + /** The parent identifier for the parameter match */ + identifier: Identifier; + /** The parameter index in the signature */ + index: number; + /** The resolved match type for the parameter */ + match: MatchType; + /** Whether this match refers to return parameters */ + isReturns?: boolean; + /** Dynamic command name when inferred from callsite */ + dynamicCommand?: string; +} + +/** + * The identifier key is the identifier name + matchTypeId. (ex: a proc called do_something -> do_somethingPROC) + * This supports identifiers with the same name but different match type. + */ +export type IdentifierKey = string; + +/** + * The file key is simply the URI fsPath, a full file path is always unique within a specific workspace + */ +export type FileKey = string; + +/** + * A wrapper interface that holds data and the start and end positions that data is contained in + */ +export interface DataRange { + start: number, + end: number, + data: T +} + +/** + * Data about a config line returned from the config matcher + */ +export interface ConfigLineData { + key: string; + params: string[]; + index: number; +} + +/** Data which defines info about the values a config key expects (key=value(s)) */ +export interface ConfigKeyData { + /** The types of the params for this config key, in order */ + params: string[], + /** Words to be ignored as params if they belong to this config key */ + ignoreValues?: string[] + /** If this config key has var args, this data is used by the matcher to figure out the arg match types */ + varArgs?: { + /** The param index that the varags start on */ + startIndex: number, + /** The source of the identifier where the vararg param types are defined */ + idenSrc: ConfigVarArgSrc, + /** The match type id of the identifier where teh varag param types are defined */ + idenType: string + } +} diff --git a/src/utils/cacheUtils.ts b/src/utils/cacheUtils.ts new file mode 100644 index 0000000..70c0262 --- /dev/null +++ b/src/utils/cacheUtils.ts @@ -0,0 +1,38 @@ +import type { Uri } from 'vscode'; +import type { FileKey, Identifier, IdentifierKey, MatchType } from '../types'; +import { Location, Position, Range } from 'vscode'; + +export function resolveIdentifierKey(name: string, match: MatchType): IdentifierKey { + return name + match.id; +} + +export function resolveKeyFromIdentifier(iden: Identifier): string { + return iden.name + iden.matchId; +} + +export function resolveFileKey(uri: Uri): FileKey | undefined { + return uri.fsPath; +} + +export function encodeReference(line: number, startIndex: number, endIndex: number): string { + return `${line}|${startIndex}|${endIndex}`; +} + +export function decodeReferenceToLocation(uri: Uri, encodedValue: string): Location | undefined { + const split = encodedValue.split('|'); + return (split.length !== 3) ? undefined : new Location(uri, new Position(Number(split[0]), Number(split[1]))); +} + +export function decodeReferenceToRange(encodedValue: string): Range | undefined { + const split = encodedValue.split('|'); + if (split.length !== 3) { + return undefined; + } + const startPosition = new Position(Number(split[0]), Number(split[1])); + const wordLength = Number(split[2]) - Number(split[1]); + return new Range(startPosition, startPosition.translate(0, wordLength)); +} + +export function getFullName(iden: Identifier): string { + return iden.cacheKey.slice(0, -iden.matchId.length); +} diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts new file mode 100644 index 0000000..17ddf48 --- /dev/null +++ b/src/utils/fileUtils.ts @@ -0,0 +1,41 @@ +import { window, type Uri } from "vscode"; +import { monitoredFileTypes } from "../core/manager"; +import { getLines } from "./stringUtils"; +import { readFile } from 'fs/promises'; +import type { FileInfo } from "../types"; + +/** +* Checks if the file extension of the uri is in the list of monitored file types +*/ +export function isValidFile(uri: Uri): boolean { + const ext = uri.fsPath.split(/[#?]/)[0].split('.').pop()?.trim(); + return ext !== undefined && monitoredFileTypes.has(ext); +} + +/** + * Checks if the file uri is the active opened editor + */ +export function isActiveFile(uri: Uri): boolean { + return getActiveFile() === uri; +} + +/** + * Get the active file being viewed + */ +export function getActiveFile(): Uri | undefined { + return window.activeTextEditor?.document.uri; +} + +export async function getFileText(uri: Uri): Promise { + return getLines(await readFile(uri.fsPath, "utf8")) +} + +export function getFileInfo(uri: Uri): FileInfo { + const fileSplit = uri.fsPath.split('\\').pop()!.split('/').pop()!.split('.'); + return { name: fileSplit[0]!, type: fileSplit[1]! }; +} + +export function getFileName(uri: Uri): string { + const fileSplit = uri.fsPath.split('\\').pop()!.split('/').pop()!.split('.'); + return `${fileSplit[0]!}.${fileSplit[1]!}`; +} diff --git a/src/utils/markdownUtils.ts b/src/utils/markdownUtils.ts new file mode 100644 index 0000000..eb38d9d --- /dev/null +++ b/src/utils/markdownUtils.ts @@ -0,0 +1,140 @@ +import type { ExtensionContext } from 'vscode'; +import type { Identifier, MatchContext, OperatorToken, ParsedWord, TextRange } from '../types'; +import { MarkdownString, Uri } from 'vscode'; +import { join, sep } from 'path'; +import { INFO, VALUE, SIGNATURE, CODEBLOCK } from '../enum/hoverDisplayItems'; +import { GLOBAL_VAR } from '../matching/matchType'; +import { getFileName } from './fileUtils'; +import { decodeReferenceToLocation } from './cacheUtils'; + +export function markdownBase(extensionContext: ExtensionContext): MarkdownString { + const markdown = new MarkdownString(); + markdown.supportHtml = true; + markdown.isTrusted = true; + markdown.supportThemeIcons = true; + markdown.baseUri = Uri.file(join(extensionContext.extensionPath, 'icons', sep)); + return markdown; +} + +export function appendTitle(name: string, type: string, matchId: string | undefined, markdown: MarkdownString, id?: string, isCert?: boolean): void { + if (isCert) name = `${name} (cert)`; + if (id) name = `${name} [${id}]`; + markdown.appendMarkdown(`${matchId === GLOBAL_VAR.id ? type.toUpperCase() : matchId} ${name}`); +} + +export function appendInfo(identifier: Identifier, displayItems: string[], markdown: MarkdownString): void { + if (displayItems.includes(INFO) && identifier.info) { + appendBody(`${identifier.info}`, markdown); + } +} + +export function appendValue(identifier: Identifier, displayItems: string[], markdown: MarkdownString): void { + if (displayItems.includes(VALUE) && identifier.value) { + appendBody(`${identifier.value}`, markdown); + } +} + +export function appendSignature(identifier: Identifier, displayItems: string[], markdown: MarkdownString): void { + if (displayItems.includes(SIGNATURE) && identifier.signature) { + if (identifier.signature.paramsText.length > 0) markdown.appendCodeblock(`params: ${identifier.signature.paramsText}`, identifier.language ?? 'runescript'); + if (identifier.signature.returnsText.length > 0) markdown.appendCodeblock(`returns: ${identifier.signature.returnsText}`, identifier.language ?? 'runescript'); + } +} + +export function appendCodeBlock(identifier: Identifier, displayItems: string[], markdown: MarkdownString): void { + if (displayItems.includes(CODEBLOCK) && identifier.block) { + markdown.appendCodeblock(identifier.block.code, identifier.language ?? 'runescript'); + } +} + +export function appendBody(text: string, markdown: MarkdownString): void { + if (!markdown.value.includes('---')) { + markdown.appendMarkdown('\n\n---'); + } + markdown.appendMarkdown(`\n\n${text}`); +} + +export function appendDebugHover(markdown: MarkdownString, word: ParsedWord, context?: MatchContext, identifier?: Identifier): void { + if (markdown.value) markdown.appendMarkdown('\n\n---\n\n'); + if (!markdown.value) { + if (context) { + markdown.appendMarkdown(`**${context.matchType.id}** ${word.value}`); + } else { + markdown.appendMarkdown(`**UNMATCHED_TOKEN** ${word.value}`); + } + } + + const parsingInfoLines: string[] = []; + parsingInfoLines.push(`word=${word.value}`); + parsingInfoLines.push(`wordIndex=${word.index}`); + parsingInfoLines.push(`wordRange=${word.start}-${word.end}`); + parsingInfoLines.push(`inInterpolation=${word.inInterpolation}`); + parsingInfoLines.push(`parenthesisDepth=${word.parenDepth}`); + parsingInfoLines.push(`braceDepth=${word.braceDepth}`); + if (context?.extraData && Object.keys(context.extraData).length > 0) { + parsingInfoLines.push(`extraData=${JSON.stringify(context.extraData)}`); + } + markdown.appendMarkdown(`\n\n---\n\n**Parsing Info**`); + markdown.appendCodeblock(parsingInfoLines.join('\n'), 'properties'); + + if (word.callName || word.configKey) { + const callInfoLines: string[] = []; + if (word.callName) { + callInfoLines.push(`callName=${word.callName}`); + callInfoLines.push(`callNameWordIndex=${word.callNameIndex}`); + } + if (word.configKey) { + callInfoLines.push(`configKey=${word.configKey}`); + callInfoLines.push(`configKeyWordIndex=${word.callNameIndex}`); + } + callInfoLines.push(`paramIndex=${word.paramIndex}`); + markdown.appendMarkdown(`\n\n---\n\n**Parent Function**`); + markdown.appendCodeblock(callInfoLines.join('\n'), 'properties'); + } + + if (context?.originalWord) { + const modifiedWordLines: string[] = []; + modifiedWordLines.push(`modifiedWord=true`); + modifiedWordLines.push(`originalWord=${context.originalWord}`); + if (context.originalPrefix) modifiedWordLines.push(`originalPrefix=${context.originalPrefix}`); + if (context.originalSuffix) modifiedWordLines.push(`originalSuffix=${context.originalSuffix}`); + markdown.appendMarkdown(`\n\n---\n\n**Modified Word**`); + markdown.appendCodeblock(modifiedWordLines.join('\n'), 'properties'); + } + + if (identifier) { + const identifierLines: string[] = []; + if (identifier.id) identifierLines.push(`packId=${identifier.id}`); + identifierLines.push(context?.matchType.cache ? `cacheId=${word.value}${identifier.matchId}` : 'cacheId=Not cached'); + if (identifier.declaration) { + const location = decodeReferenceToLocation(identifier.declaration.uri, identifier.declaration.ref); + const line = location ? location.range.start.line + 1 : 'n/a'; + identifierLines.push(`declaration=${getFileName(identifier.declaration.uri)}, line ${line}`); + } + const refCount = Object.values(identifier.references).reduce((count, set) => count + set.size, 0); + identifierLines.push(`references=${refCount}`); + identifierLines.push(`language=${identifier.language}`); + if (identifier.comparisonType) identifierLines.push(`comparisonType=${identifier.comparisonType}`); + if (identifier.hideDisplay) identifierLines.push(`hideDisplay=true`); + markdown.appendMarkdown(`\n\n---\n\n**Identifier**`); + markdown.appendCodeblock(identifierLines.join('\n'), 'properties'); + } +} + +export function appendOperatorHover(markdown: MarkdownString, operator: OperatorToken): void { + const operatorLines: string[] = []; + operatorLines.push(`index=${operator.index}`); + operatorLines.push(`parenDepth=${operator.parenDepth}`); + if (markdown.value) markdown.appendMarkdown('\n\n---\n\n'); + markdown.appendMarkdown(`**OPERATOR** [ ${operator.token} ]`); + markdown.appendCodeblock(operatorLines.join('\n'), 'properties'); +} + +export function appendStringHover(markdown: MarkdownString, range: TextRange): void { + const stringLines: string[] = []; + stringLines.push(`range=${range.start}-${range.end}`); + stringLines.push(`length=${Math.max(0, range.end - range.start + 1)}`); + if (markdown.value) markdown.appendMarkdown('\n\n---\n\n'); + markdown.appendMarkdown(`**STRING**`); + markdown.appendCodeblock(stringLines.join('\n'), 'properties'); +} diff --git a/src/utils/matchUtils.ts b/src/utils/matchUtils.ts new file mode 100644 index 0000000..4159c66 --- /dev/null +++ b/src/utils/matchUtils.ts @@ -0,0 +1,109 @@ +import type { Uri } from 'vscode'; +import type { MatchType, ParsedWord, MatchContext, DataRange, FileInfo } from '../types'; +import { WORD_REGEX } from '../enum/regex'; +import { getAllMatchTypes, UNKNOWN } from '../matching/matchType'; +import { getFileInfo } from './fileUtils'; + +export function getWords(lineText: string, wordPattern: RegExp = WORD_REGEX): ParsedWord[] { + return [...lineText.matchAll(wordPattern)].map((wordMatch, index) => { + return { + value: wordMatch[0]!, + start: wordMatch.index!, + end: wordMatch.index! + wordMatch[0]!.length - 1, + index, + inString: false, + inInterpolation: false, + parenDepth: -1, + braceDepth: -1, + callName: undefined, + callNameIndex: undefined, + paramIndex: undefined + }; + }); +} + +export function getWordAtIndex(words: ParsedWord[], index: number): ParsedWord | undefined { + if (words.length < 1) return undefined; + let prev: ParsedWord | undefined; + for (let i = words.length - 1; i >= 0; i--) { + if (index <= words[i].end) prev = words[i]; + else break; + } + return (prev && prev.start <= index && prev.end >= index) ? prev : undefined; +} + +export function expandCsvKeyObject(obj: Record): Record { + let keys = Object.keys(obj); + for (let i = 0; i < keys.length; ++i) { + let key = keys[i]; + let subkeys = key.split(/,\s?/); + let target = obj[key]; + delete obj[key]; + subkeys.forEach(k => obj[k] = target); + } + return obj; +} + +/** + * Builds the match context needed for processing a word thru the matching engine + * @param uri The file uri for the word being matched + * @param fileInfo The file info for the word being matched (use getFileInfo() helper) + * @param words The parsed words on the line of the word being matched + * @param lineText The text of the line that the word is on + * @param lineNum The line number of the line that the word is on + * @param wordIndex The index of the word we are matching, that is the index of the parsed word on that line + * @returns The constructed matchContext + */ +export function buildMatchContext(uri: Uri, words: ParsedWord[], lineText: string, lineNum: number, wordIndex: number, fileInfo?: FileInfo): MatchContext { + return { + words: words, + uri: uri, + line: { text: lineText, number: lineNum }, + file: fileInfo ?? getFileInfo(uri), + matchType: UNKNOWN, + declaration: false, + word: words[wordIndex], + lineIndex: words[wordIndex].start, + prevWord: (wordIndex === 0) ? undefined : words[wordIndex - 1], + prevChar: lineText.charAt(words[wordIndex].start - 1), + nextChar: lineText.charAt(words[wordIndex].end + 1), + }; +} + +export function reference(type: MatchType, context: MatchContext): void { + context.matchType = type; + context.declaration = false; +} + +export function declaration(type: MatchType, context: MatchContext): void { + context.matchType = type; + context.declaration = true; +} + +export function addExtraData(context: MatchContext, extraData: Record): void { + context.extraData = extraData; +} + +/** + * Binary search to find the match of a data range list at the index provided, if there is one + * @param index Index of the item you are looking for + * @param items List of the DataRanges which hold the data being retrieved + * @returns The data of the DataRange, if a match is found + */ +export function findMatchInRange(index: number, items?: DataRange[]): DataRange | undefined { + if (!items) return undefined; + let lo = 0; + let hi = items.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + const item = items[mid]; + if (index < item.start) hi = mid - 1; + else if (index > item.end) lo = mid + 1; + else return item; + } + return undefined; +} + +export function resolveCallableMatchTypes(): string[] { + return getAllMatchTypes().filter(matchType => matchType.callable).map(matchType => matchType.id); +} diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts new file mode 100644 index 0000000..99dada1 --- /dev/null +++ b/src/utils/stringUtils.ts @@ -0,0 +1,45 @@ +import { Uri } from 'vscode'; +import { END_OF_LINE_REGEX, END_OF_BLOCK_REGEX } from '../enum/regex'; + +export function getLineText(input: string): string { + const endOfLine = END_OF_LINE_REGEX.exec(input); + return !endOfLine ? input : input.substring(0, endOfLine.index); +} + +export function getLines(input: string): string[] { + return input.split(END_OF_LINE_REGEX); +} + +export function skipFirstLine(input: string): string { + const endOfLine = END_OF_LINE_REGEX.exec(input); + return !endOfLine ? input : input.substring(endOfLine.index + 1); +} + +export function getBlockText(input: string): string { + const endOfBlock = END_OF_BLOCK_REGEX.exec(input); + return !endOfBlock ? input : input.substring(0, endOfBlock.index); +} + +export function nthIndexOf(input: string, pattern: string, n: number): number { + let i = -1; + while (n-- > 0 && i++ < input.length) { + i = input.indexOf(pattern, i); + if (i < 0) break; + } + return i; +} + +export function truncateMatchingParenthesis(str: string): string { + let truncateIndex = 0; + let count = 0; + for (let i = 0; i < str.length; i++) { + if (str.charAt(i) === '(') count++; + if (str.charAt(i) === ')' && --count === 0) truncateIndex = i; + } + return (truncateIndex > 0) ? str.substring(truncateIndex + 1) : str; +} + +export function createSearchableString(linkableText: string, query: string, filesToInclude: string, isRegex = false): string { + const searchOptions = JSON.stringify({ query: query, filesToInclude: filesToInclude, isRegex: isRegex }); + return `[${linkableText}](${Uri.parse(`command:workbench.action.findInFiles?${encodeURIComponent(searchOptions)}`)})`; +} diff --git a/src/webview/identifierLookupView.ts b/src/webview/identifierLookupView.ts new file mode 100644 index 0000000..d99ba1d --- /dev/null +++ b/src/webview/identifierLookupView.ts @@ -0,0 +1,180 @@ +import { ViewColumn, window } from 'vscode'; +import { get } from '../cache/identifierCache'; +import { getAllWithPrefix } from '../cache/completionCache'; +import { getAllMatchTypes, getMatchTypeById } from '../matching/matchType'; +import { serializeIdentifier } from '../resource/identifierFactory'; + +export function showIdentifierLookupView(): void { + const panel = window.createWebviewPanel( + 'runescriptIdentifierLookup', + 'Runescript: Identifier Lookup', + ViewColumn.One, + { enableScripts: true } + ); + panel.webview.html = getIdentifierLookupHtml(); + const matchTypeIds = getAllMatchTypes().map(matchType => matchType.id).sort(); + void panel.webview.postMessage({ type: 'init', matchTypeIds }); + panel.webview.onDidReceiveMessage((message) => { + if (!message) return; + if (message.type === 'suggest') { + const matchTypeId = (message.matchTypeId ?? '').toString().trim().toUpperCase(); + const prefix = (message.prefix ?? '').toString(); + if (!matchTypeId || !prefix) { + void panel.webview.postMessage({ type: 'suggestions', results: [] }); + return; + } + const matchType = getMatchTypeById(matchTypeId); + if (!matchType) { + void panel.webview.postMessage({ type: 'suggestions', results: [] }); + return; + } + const results = getAllWithPrefix(prefix, matchType.id)?.slice(0, 200) ?? []; + void panel.webview.postMessage({ type: 'suggestions', results }); + return; + } + if (message.type !== 'lookup') return; + const name = (message.name ?? '').toString().trim(); + const matchTypeId = (message.matchTypeId ?? '').toString().trim(); + if (!name || !matchTypeId) { + void panel.webview.postMessage({ type: 'result', result: 'Name and match type id are required.' }); + return; + } + const normalizedMatchTypeId = matchTypeId.toUpperCase(); + const matchType = getMatchTypeById(normalizedMatchTypeId); + if (!matchType) { + void panel.webview.postMessage({ type: 'result', result: `Unknown match type id: ${matchTypeId}` }); + return; + } + const identifier = get(name, matchType); + if (!identifier) { + void panel.webview.postMessage({ type: 'result', result: `No identifier found for "${name}" (${matchType.id})` }); + return; + } + const serialized = serializeIdentifier(identifier); + void panel.webview.postMessage({ type: 'result', result: JSON.stringify(serialized, undefined, 2) }); + }); +} + +function getIdentifierLookupHtml(): string { + return ` + + + + + Runescript Identifier Lookup + + + +
+ + + + + +
+
Enter a name and match type id, then click Find.
+ + +`; +} diff --git a/syntaxes/enumconfig.tmLanguage.json b/syntaxes/enumconfig.tmLanguage.json index 44eaa2f..1b30d95 100644 --- a/syntaxes/enumconfig.tmLanguage.json +++ b/syntaxes/enumconfig.tmLanguage.json @@ -88,7 +88,7 @@ }, { "comment": "Enum values", - "match": "^(val)=(.+),(.+)", + "match": "^(val)=([^,]+)(?:,(.+))?", "captures": { "1": { "name": "entity.name.type.enumconfig.key" @@ -105,4 +105,4 @@ } }, "scopeName": "source.enumconfig" -} \ No newline at end of file +} diff --git a/syntaxes/huntconfig.tmLanguage.json b/syntaxes/huntconfig.tmLanguage.json index 5390f85..fdc14e9 100644 --- a/syntaxes/huntconfig.tmLanguage.json +++ b/syntaxes/huntconfig.tmLanguage.json @@ -174,6 +174,18 @@ "name": "variable.language.huntconfig" } } + }, + { + "comment": "Hunt check_category values", + "match": "^(check_category)=(.+)", + "captures": { + "1": { + "name": "entity.name.type.huntconfig.key" + }, + "2": { + "name": "variable.language.huntconfig" + } + } } ] } diff --git a/syntaxes/interface.tmLanguage.json b/syntaxes/interface.tmLanguage.json index 10c83f4..e58b804 100644 --- a/syntaxes/interface.tmLanguage.json +++ b/syntaxes/interface.tmLanguage.json @@ -81,7 +81,7 @@ }, { "comment": "Interface type values", - "match": "^(type)=(layer|inv|rect|text|graphic|model|invtext)?", + "match": "^(type)=(invtext|layer|inv|rect|text|graphic|model)?", "captures": { "1": { "name": "entity.name.type.interface.key" @@ -117,7 +117,7 @@ }, { "comment": "Interface font values", - "match": "^(font)=(p11_full|p12_full|b12_full|q8_full)?", + "match": "^(font)=(p11_full|p11|p12_full|p12|b12_full|b12|q8_full|q8)?", "captures": { "1": { "name": "entity.name.type.interface.key" diff --git a/syntaxes/runescript.tmLanguage.json b/syntaxes/runescript.tmLanguage.json index cc9f7ec..2c7917d 100644 --- a/syntaxes/runescript.tmLanguage.json +++ b/syntaxes/runescript.tmLanguage.json @@ -103,25 +103,10 @@ "name": "entity.name.function.runescript", "match": "(?<=,)(\\.)?\\w+(:\\w+)?\\]" }, - { - "comment": "Engine commands that are also function parameters", - "name": "entity.name.function.runescript", - "match": "\\b(queue|walktrigger|enum|softtimer|stat)\\(" - }, { "comment": "Function parameters", "name": "variable.language.runescript", - "match": "\\b(coord\\)|queue|walktrigger|enum|softtimer|stat|int|string|boolean|seq|locshape|component|idk|midi|npc_mode|namedobj|synth|npc_stat|fontmetrics|loc|model|npc|obj|player_uid|spotanim|npc_uid|inv|category|struct|dbrow|interface|dbtable|mesanim|param|char|dbcolumn|proc|label|timer|idkit|hunt)\\b" - }, - { - "comment": "Function parameters (special case)", - "name": "variable.language.runescript", - "match": "\\bcoord " - }, - { - "comment": "Engine commands", - "name": "entity.name.function.runescript", - "match": "\\b(gosub|gettimer|gettimespent|getqueue|getwalktrigger|getbit_range|gender|jump|map_clock|map_members|map_multiway|map_playercount|map_production|map_blocked|map_indoors|map_locaddunsafe|map_lastclock|map_lastclientin|map_lastclientout|map_lastcleanup|map_lastworld|map_lastnpc|map_lastplayer|map_lastlogin|map_lastlogout|map_lastzone|map_lastbandwidthin|map_lastbandwidthout|map_findsquare|max|movecoord|modulo|mes|midi_song|midi_jingle|min|multiply|huntall|huntnext|healenergy|headicons_get|headicons_set|hint_coord|hint_stop|hint_npc|hint_player|npc_huntall|npc_huntnext|npc_heropoints|npc_hasop|npc_find|npc_finduid|npc_findall|npc_findallany|npc_findallzone|npc_findexact|npc_findhero|npc_findnext|npc_facesquare|npc_add|npc_anim|npc_attackrange|npc_arrivedelay|npc_category|npc_coord|npc_changetype|npc_del|npc_delay|npc_damage|npc_param|npc_queue|npc_range|npc_say|npc_sethunt|npc_sethuntmode|npc_setmode|npc_settimer|npc_stat|npc_statadd|npc_statheal|npc_statsub|npc_basestat|npc_type|npc_tele|npc_name|npc_uid|npc_getmode|npc_walk|npc_walktrigger|npccount|name|nc_name|nc_param|nc_category|nc_desc|nc_debugname|nc_op|inzone|inv_allstock|inv_add|inv_size|inv_stockbase|inv_stoptransmit|inv_setslot|inv_changeslot|inv_clear|inv_del|inv_delslot|inv_debugname|inv_dropitem|inv_dropslot|inv_dropall|inv_freespace|inv_getnum|inv_getobj|inv_itemspace|inv_itemspace2|inv_movefromslot|inv_movetoslot|inv_moveitem|inv_moveitem_cert|inv_moveitem_uncert|inv_total|inv_totalcat|inv_transmit|invother_transmit|invpow|interpolate|if_close|if_setcolour|if_sethide|if_setobject|if_setmodel|if_setrecol|if_setresumebuttons|if_setanim|if_settab|if_settabactive|if_settext|if_setplayerhead|if_setposition|if_setnpchead|if_openchat|if_openmain|if_openmain_side|if_openside|lineofwalk|lineofsight|loccount|loc_add|loc_angle|loc_anim|loc_category|loc_change|loc_coord|loc_del|loc_find|loc_findallzone|loc_findnext|loc_param|loc_type|loc_name|loc_shape|loggedout|longqueue|lowmemory|lowercase|last_com|last_int|last_item|last_slot|last_useitem|last_useslot|last_login_info|last_targetslot|lc_name|lc_param|lc_category|lc_desc|lc_debugname|lc_width|lc_length|stat|stat_random|stat_base|stat_add|stat_advance|stat_sub|stat_heal|staffmodlevel|struct_param|strongqueue|string_length|string_indexof_char|string_indexof_string|spotanim_map|spotanim_pl|spotanim_npc|split_init|split_pagecount|split_get|split_getanim|split_linecount|seqlength|settimer|setidkit|setgender|setskincolour|setbit|setbit_range|setbit_range_toint|session_log|say|sound_synth|softtimer|sub|substring|scale|sin_deg|distance|displayname|divide|damage|db_find|db_find_with_count|db_find_refine|db_find_refine_with_count|db_findnext|db_findbyindex|db_getfield|db_getfieldcount|db_getrowtable|db_listall|db_listall_with_count|coord|coordx|coordy|coordz|compare|cos_deg|console|cam_moveto|cam_lookat|cam_shake|cam_reset|clearsofttimer|cleartimer|clearqueue|clearbit|clearbit_range|playercount|player_findallzone|player_findnext|projanim_pl|projanim_npc|projanim_map|p_finduid|p_aprange|p_arrivedelay|p_animprotect|p_countdialog|p_clearpendingaction|p_delay|p_opheld|p_oploc|p_opnpc|p_opnpct|p_opobj|p_opplayer|p_opplayert|p_pausebutton|p_stopaction|p_telejump|p_teleport|p_walk|p_logout|p_locmerge|p_exactmove|p_run|pow|world_delay|weakqueue|wealth_log|weight|walktrigger|zonecount|objcount|obj_add|obj_addall|obj_param|obj_name|obj_del|obj_count|obj_coord|obj_type|obj_takeitem|obj_find|oc_name|oc_param|oc_category|oc_cost|oc_cert|oc_desc|oc_debugname|oc_members|oc_weight|oc_wearpos|oc_wearpos2|oc_wearpos3|oc_tradeable|oc_uncert|oc_stackable|or|finduid|findhero|facesquare|anim|and|allowdesign|afk_event|append|append_num|append_signnum|append_char|add|addpercent|atan2_deg|abs|buffer_full|buildappearance|busy|busy2|bas_readyanim|bas_running|bas_turnonspot|bas_walk_f|bas_walk_b|bas_walk_l|bas_walk_r|both_heropoints|both_moveinv|both_dropslot|bitcount|uid|tut_open|tut_close|tut_flash|text_gender|testbit|tostring|togglebit|timespent|queue|runenergy|random|randominc|enum|enum_getoutputcount|error|calc)\\b" + "match": "\\b(coord|queue|walktrigger|enum|softtimer|stat|int|string|boolean|seq|locshape|component|idk|midi|npc_mode|namedobj|synth|npc_stat|fontmetrics|loc|model|npc|obj|player_uid|spotanim|npc_uid|inv|category|struct|dbrow|interface|dbtable|mesanim|param|char|dbcolumn|proc|label|timer|idkit|hunt)\\b" }, { "comment": "Any other properties", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8cefd30 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "./out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "out", + "syntaxes", + "snippets", + "language-configuration", + "icons" + ] +}