| 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..8fdaa1c 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"
diff --git a/syntaxes/runescript.tmLanguage.json b/syntaxes/runescript.tmLanguage.json
index cc9f7ec..9cf35c6 100644
--- a/syntaxes/runescript.tmLanguage.json
+++ b/syntaxes/runescript.tmLanguage.json
@@ -103,11 +103,6 @@
"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",
@@ -118,11 +113,6 @@
"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"
- },
{
"comment": "Any other properties",
"name": "variable.other.property.runescript",
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"
+ ]
+}
From f193b4ebe48deae6aabd18c9b15148740d1d093f Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Tue, 27 Jan 2026 09:40:16 +0000
Subject: [PATCH 2/8] fix: multiple switch stmt cases matching
---
src/matching/matchers/switchCaseMatcher.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/matching/matchers/switchCaseMatcher.ts b/src/matching/matchers/switchCaseMatcher.ts
index edc5ef7..1d01b34 100644
--- a/src/matching/matchers/switchCaseMatcher.ts
+++ b/src/matching/matchers/switchCaseMatcher.ts
@@ -2,13 +2,14 @@ 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.file.type === 'rs2' && context.word.index > 0 &&
- context.words[context.word.index - 1].value === 'case' && context.word.value !== 'default') {
+ 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);
}
From b0dc2b2d1f5a21a7f18cd6ed1b4de4b0c8bafb67 Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Tue, 27 Jan 2026 10:54:00 +0000
Subject: [PATCH 3/8] fix: debounce forceReload
---
src/core/eventHandlers.ts | 38 +++++++-
.../matchers/columnDeclarationMatcher.ts | 2 +-
src/matching/matchers/commandMatcher.ts | 2 +-
.../matchers/configDeclarationMatcher.ts | 2 +-
src/matching/matchers/configMatcher.ts | 26 ++----
src/matching/matchers/constMatcher.ts | 2 +-
src/matching/matchers/localVarMatcher.ts | 2 +-
src/matching/matchers/parametersMatcher.ts | 2 +-
src/matching/matchers/prevCharMatcher.ts | 2 +-
src/matching/matchers/regexWordMatcher.ts | 2 +-
src/matching/matchers/switchCaseMatcher.ts | 2 +-
src/matching/matchers/triggerMatcher.ts | 2 +-
src/provider/completion/completetionCommon.ts | 5 +-
.../configSignatureHelpProvider.ts | 19 ++--
.../runescriptSignatureHelpProvider.ts | 4 +
src/resource/configKeys.ts | 92 ++++++++++++-------
src/resource/postProcessors.ts | 3 +-
src/types.ts | 18 ++++
18 files changed, 152 insertions(+), 73 deletions(-)
diff --git a/src/core/eventHandlers.ts b/src/core/eventHandlers.ts
index a6d7bd3..a8d0912 100644
--- a/src/core/eventHandlers.ts
+++ b/src/core/eventHandlers.ts
@@ -9,8 +9,9 @@ import { getLines } from "../utils/stringUtils";
import { clearFile, processAllFiles, queueFileRebuild, rebuildFileChanges } from "./manager";
import { monitoredFileTypes } from "../runescriptExtension";
-// Debounce time only applies to active file text change events, everything else is done ASAP
-const debounceTimeMs = 200;
+
+const debounceTimeMs = 150; // debounce time for normal active file text changes
+const forceDebounceTimeMs = 75; // debounce time for force rebuilds (used by signature help and completion providers)
export function registerEventHandlers(context: ExtensionContext): void {
const patterns = Array.from(monitoredFileTypes, ext => `**/*.${ext}`);
@@ -36,6 +37,10 @@ export function registerEventHandlers(context: ExtensionContext): void {
let pendingChanges: TextDocumentContentChangeEvent[] = [];
let pendingDocument: TextDocument | undefined;
let pendingTimer: NodeJS.Timeout | undefined;
+let forcePendingDocument: TextDocument | undefined;
+let forceTimer: NodeJS.Timeout | undefined;
+let forcePromise: Promise | undefined;
+let forceResolve: (() => void) | undefined;
function onActiveFileTextChange(textChangeEvent: TextDocumentChangeEvent): void {
if (!isActiveFile(textChangeEvent.document.uri)) return;
if (!isValidFile(textChangeEvent.document.uri)) return;
@@ -54,12 +59,39 @@ function onActiveFileTextChange(textChangeEvent: TextDocumentChangeEvent): void
}, debounceTimeMs);
}
+/**
+ * Force rebuild a file and cancel any pending (partial) text doc change event debounce
+ * @param document document to force the rebuild on
+ * @returns a promise which resolves only when the document has finished being rebuilt
+ */
export function forceRebuild(document: TextDocument): Promise {
if (pendingTimer) clearTimeout(pendingTimer);
pendingChanges = [];
pendingTimer = undefined;
pendingDocument = undefined;
- return queueFileRebuild(document.uri, getLines(document.getText()));
+ forcePendingDocument = document;
+ if (forceTimer) clearTimeout(forceTimer);
+ if (!forcePromise) {
+ forcePromise = new Promise((resolve) => {
+ forceResolve = resolve;
+ });
+ }
+ forceTimer = setTimeout(() => {
+ const doc = forcePendingDocument;
+ const resolve = forceResolve;
+ forcePendingDocument = undefined;
+ forceTimer = undefined;
+ forcePromise = undefined;
+ forceResolve = undefined;
+ if (!doc) {
+ resolve?.();
+ return;
+ }
+ void queueFileRebuild(doc.uri, getLines(doc.getText())).finally(() => {
+ resolve?.();
+ });
+ }, forceDebounceTimeMs);
+ return forcePromise;
}
async function onActiveDocumentChange(editor: TextEditor | undefined): Promise {
diff --git a/src/matching/matchers/columnDeclarationMatcher.ts b/src/matching/matchers/columnDeclarationMatcher.ts
index dc0ccf7..3196e1c 100644
--- a/src/matching/matchers/columnDeclarationMatcher.ts
+++ b/src/matching/matchers/columnDeclarationMatcher.ts
@@ -9,4 +9,4 @@ function columnDeclarationMatcherFn(context: MatchContext): void {
}
}
-export const columnDeclarationMatcher: Matcher = { priority: 1250, fn: columnDeclarationMatcherFn};
+export const columnDeclarationMatcher: Matcher = { priority: 2000, fn: columnDeclarationMatcherFn};
diff --git a/src/matching/matchers/commandMatcher.ts b/src/matching/matchers/commandMatcher.ts
index ea9d995..295f7fa 100644
--- a/src/matching/matchers/commandMatcher.ts
+++ b/src/matching/matchers/commandMatcher.ts
@@ -21,4 +21,4 @@ const commandMatcherFn = (context: MatchContext): void => {
}
}
-export const commandMatcher: Matcher = { priority: 5000, fn: commandMatcherFn };
+export const commandMatcher: Matcher = { priority: 8000, fn: commandMatcherFn };
diff --git a/src/matching/matchers/configDeclarationMatcher.ts b/src/matching/matchers/configDeclarationMatcher.ts
index 119df6e..f411054 100644
--- a/src/matching/matchers/configDeclarationMatcher.ts
+++ b/src/matching/matchers/configDeclarationMatcher.ts
@@ -27,4 +27,4 @@ function configDeclarationMatcherFn(context: MatchContext): void {
}
}
-export const configDeclarationMatcher: Matcher = { priority: 1500, fn: configDeclarationMatcherFn};
+export const configDeclarationMatcher: Matcher = { priority: 3000, fn: configDeclarationMatcherFn};
diff --git a/src/matching/matchers/configMatcher.ts b/src/matching/matchers/configMatcher.ts
index 0097529..03f1057 100644
--- a/src/matching/matchers/configMatcher.ts
+++ b/src/matching/matchers/configMatcher.ts
@@ -1,4 +1,4 @@
-import { type ConfigData, configKeys, ConfigVarArgSrc, getRegexKey, learnConfigKey } from "../../resource/configKeys";
+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";
@@ -27,36 +27,26 @@ export function getConfigLineMatch(context: MatchContext): ConfigLineData | unde
// Get the configData from the configKeys static object [defined in configKeys.ts]
const configKey = context.word.configKey;
const paramIndex = context.word.paramIndex;
- const configData: ConfigData = configKeys[configKey] ?? getRegexKey(configKey, context.file.type);
- if (!configData) {
+ const configData = getConfigData(configKey, context.file.type);
+ if (!configData || (configData.ignoreValues ?? []).includes(context.word.value)) {
reference(SKIP, context);
return undefined;
}
- // Exit early if explicity defined to ignore the word for this configData
- if ((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;
- let paramIndexOffset = 0;
if (configData.varArgs.idenSrc === ConfigVarArgSrc.BlockName) {
iden = getBlockScopeIdentifier(context.line.number);
}
else if (configData.varArgs.idenSrc === ConfigVarArgSrc.FirstParam) {
- // todo I dont think this will work, as we need the resolved matchResult word for word modifications
- const item = getByLineIndex(context.line.number, context.words[1].start);
- iden = item?.identifier;
- paramIndexOffset = -1;
+ iden = getByLineIndex(context.line.number, context.words[1].start)?.identifier;
}
// get the param match types from the identifier signature
- const varArgIndex = paramIndex + paramIndexOffset;
+ 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 - 1 };
+ 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;
@@ -69,8 +59,8 @@ export function getConfigLineMatch(context: MatchContext): ConfigLineData | unde
const paramType = configData.params[paramIndex];
const resolvedMatchType = getMatchTypeById(dataTypeToMatchId(paramType)) ?? SKIP;
reference(resolvedMatchType, context);
- return { key: configKey, params: configData.params, index: context.word.index - 1 };
+ return { key: configKey, params: configData.params, index: context.word.index };
}
}
-export const configMatcher: Matcher = { priority: 7000, fn: configMatcherFn };
+export const configMatcher: Matcher = { priority: 10000, fn: configMatcherFn };
diff --git a/src/matching/matchers/constMatcher.ts b/src/matching/matchers/constMatcher.ts
index 7d75b20..552f1d9 100644
--- a/src/matching/matchers/constMatcher.ts
+++ b/src/matching/matchers/constMatcher.ts
@@ -6,4 +6,4 @@ function constDeclarationMatcherFn(context: MatchContext): void {
if (context.prevChar === '^' && context.file.type === "constant") declaration(CONSTANT, context);
}
-export const constDeclarationMatcher: Matcher = { priority: 1750, fn: constDeclarationMatcherFn};
+export const constDeclarationMatcher: Matcher = { priority: 4000, fn: constDeclarationMatcherFn};
diff --git a/src/matching/matchers/localVarMatcher.ts b/src/matching/matchers/localVarMatcher.ts
index e8cff3f..c0cc2bb 100644
--- a/src/matching/matchers/localVarMatcher.ts
+++ b/src/matching/matchers/localVarMatcher.ts
@@ -22,4 +22,4 @@ function matchLocalVarFn(context: MatchContext): void {
}
}
-export const matchLocalVar: Matcher = { priority: 2500, fn: matchLocalVarFn };
+export const matchLocalVar: Matcher = { priority: 6000, fn: matchLocalVarFn };
diff --git a/src/matching/matchers/parametersMatcher.ts b/src/matching/matchers/parametersMatcher.ts
index cd88a76..e331715 100644
--- a/src/matching/matchers/parametersMatcher.ts
+++ b/src/matching/matchers/parametersMatcher.ts
@@ -31,4 +31,4 @@ function parametersMatcherFn(context: MatchContext): void {
}
}
-export const parametersMatcher: Matcher = { priority: 9000, fn: parametersMatcherFn };
+export const parametersMatcher: Matcher = { priority: 12000, fn: parametersMatcherFn };
diff --git a/src/matching/matchers/prevCharMatcher.ts b/src/matching/matchers/prevCharMatcher.ts
index d50ac18..d3dd775 100644
--- a/src/matching/matchers/prevCharMatcher.ts
+++ b/src/matching/matchers/prevCharMatcher.ts
@@ -31,4 +31,4 @@ function labelMatcher(context: MatchContext): void {
reference(LABEL, context);
}
-export const prevCharMatcher: Matcher = { priority: 3000, fn: prevCharMatcherFn };
+export const prevCharMatcher: Matcher = { priority: 7000, fn: prevCharMatcherFn };
diff --git a/src/matching/matchers/regexWordMatcher.ts b/src/matching/matchers/regexWordMatcher.ts
index 5e6d842..801cd8a 100644
--- a/src/matching/matchers/regexWordMatcher.ts
+++ b/src/matching/matchers/regexWordMatcher.ts
@@ -22,4 +22,4 @@ function regexWordMatcherFn(context: MatchContext): void {
}
}
-export const regexWordMatcher: Matcher = { priority: 2000, fn: regexWordMatcherFn };
+export const regexWordMatcher: Matcher = { priority: 5000, fn: regexWordMatcherFn };
diff --git a/src/matching/matchers/switchCaseMatcher.ts b/src/matching/matchers/switchCaseMatcher.ts
index 1d01b34..00f4f20 100644
--- a/src/matching/matchers/switchCaseMatcher.ts
+++ b/src/matching/matchers/switchCaseMatcher.ts
@@ -15,4 +15,4 @@ function switchCaseMatcherFn(context: MatchContext): void {
}
}
-export const switchCaseMatcher: Matcher = { priority: 8000, fn: switchCaseMatcherFn };
+export const switchCaseMatcher: Matcher = { priority: 11000, fn: switchCaseMatcherFn };
diff --git a/src/matching/matchers/triggerMatcher.ts b/src/matching/matchers/triggerMatcher.ts
index 3567e32..1b7d88c 100644
--- a/src/matching/matchers/triggerMatcher.ts
+++ b/src/matching/matchers/triggerMatcher.ts
@@ -28,4 +28,4 @@ function triggerMatcherFn(context: MatchContext): void {
}
}
-export const triggerMatcher: Matcher = { priority: 6000, fn: triggerMatcherFn };
+export const triggerMatcher: Matcher = { priority: 9000, fn: triggerMatcherFn };
diff --git a/src/provider/completion/completetionCommon.ts b/src/provider/completion/completetionCommon.ts
index 9d25ba1..3ae7115 100644
--- a/src/provider/completion/completetionCommon.ts
+++ b/src/provider/completion/completetionCommon.ts
@@ -50,11 +50,14 @@ export function completionTypeSelector(position: Position): CompletionItem[] {
});
}
+let lastRequestId = 0;
export async function searchForMatchType(document: TextDocument, position: Position, defaultMatchId = SKIP.id, fromTrigger = false): Promise {
+ const requestId = ++lastRequestId;
+ await forceRebuild(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);
- await forceRebuild(document);
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);
diff --git a/src/provider/signatureHelp/configSignatureHelpProvider.ts b/src/provider/signatureHelp/configSignatureHelpProvider.ts
index 5eb6754..026eb7f 100644
--- a/src/provider/signatureHelp/configSignatureHelpProvider.ts
+++ b/src/provider/signatureHelp/configSignatureHelpProvider.ts
@@ -6,6 +6,8 @@ import { parseLineWithStateSnapshot } from '../../parsing/lineParser';
import { getFileInfo } from '../../utils/fileUtils';
import { getConfigLineMatch } from '../../matching/matchers/configMatcher';
+let lastRequestId = 0;
+
export const configMetadata = {
triggerCharacters: ['=', ','],
retriggerCharacters: [',']
@@ -13,23 +15,26 @@ export const configMetadata = {
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 forceRebuild(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);
- await forceRebuild(document);
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 index of params
- config.params.forEach(param => {
- // use range instead of param name due to possible duplicates
- signatureInfo.parameters.push(new ParameterInformation([index, index + param.length]));
- index += param.length + 1;
+ 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;
+ signatureInfo.activeParameter = config.index - 1;
// Build the signature help
const signatureHelp = new SignatureHelp();
diff --git a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
index 216b323..c7e50ab 100644
--- a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
+++ b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
@@ -63,6 +63,8 @@ function getScriptTriggerHelp(document: TextDocument, position: Position): Signa
}
}
+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
@@ -71,7 +73,9 @@ function getScriptTriggerHelp(document: TextDocument, position: Position): Signa
*/
async function getParametersHelp(document: TextDocument, position: Position): Promise {
// We need the parser and active file cache states up to date
+ const requestId = ++lastRequestId;
await forceRebuild(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);
diff --git a/src/resource/configKeys.ts b/src/resource/configKeys.ts
index a588e68..f54bd40 100644
--- a/src/resource/configKeys.ts
+++ b/src/resource/configKeys.ts
@@ -1,38 +1,29 @@
import { DBCOLUMN, ENUM, PARAM } from "../matching/matchType";
+import type { ConfigKeyData } from "../types";
-export interface ConfigData {
- /** 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?: { startIndex: number, idenSrc: ConfigVarArgSrc, idenType: string }
-}
-
+/**
+ * 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'
}
-interface RegexConfigData extends ConfigData {
+/**
+ * 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[];
}
-const observedConfigKeys = new Set();
-
-export function learnConfigKey(key: string): void {
- observedConfigKeys.add(key);
-}
-
-export function getObservedConfigKeys(): Set {
- return observedConfigKeys;
-}
-
-// === STATIC CONFIG KEY MATCHES ===
-export const configKeys: Record = {
+/**
+ * Defines static config keys (direct match)
+ */
+const configKeys: Record = {
walkanim: { params: ['seq', 'seq', 'seq', 'seq'] },
multivar: { params: ['var'] },
multiloc: { params: ['int', 'loc'] },
@@ -50,8 +41,10 @@ export const configKeys: Record = {
data: { params: ['dbcolumn'], varArgs: {startIndex: 1, idenSrc: ConfigVarArgSrc.FirstParam, idenType: DBCOLUMN.id}},
};
-// === REGEX CONFIG KEY MATCHES ===
-export const regexConfigKeys: Map = groupByFileType([
+/**
+ * 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"] },
@@ -60,19 +53,52 @@ export const regexConfigKeys: Map = groupByFileType([
{ regex: /replaceheldleft|replaceheldright/, params: ['obj'], fileTypes: ["seq"], ignoreValues: ["hide"] },
]);
-// === CONFIG KEYS THAT ARE HANDLED MANUALLY IN CONFIG_MATCHER ===
-export const specialCaseKeys = ['val', 'param', 'data'];
+/**
+ * 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);
+}
-export function getRegexKey(configKey: string, fileType: string): RegexConfigData | undefined {
- const fileTypeRegexMatchers = regexConfigKeys.get(fileType) || [];
- for (let regexKey of fileTypeRegexMatchers) {
- if (regexKey.regex.test(configKey)) {
- return regexKey;
- }
+/**
+ * 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;
}
- return undefined;
}
+/**
+ * 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) {
diff --git a/src/resource/postProcessors.ts b/src/resource/postProcessors.ts
index 8224278..4a006cf 100644
--- a/src/resource/postProcessors.ts
+++ b/src/resource/postProcessors.ts
@@ -72,6 +72,7 @@ export const rowPostProcessor: PostProcessor = function(identifier) {
}
};
+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`;
@@ -80,7 +81,7 @@ export const columnPostProcessor: PostProcessor = function(identifier) {
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(',');
+ 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(', ')}`;
diff --git a/src/types.ts b/src/types.ts
index d6a6f29..4ba5fe4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,7 @@
import type { Uri } from 'vscode';
import type { HoverDisplayItem } from './enum/hoverDisplayItems';
import type { SemanticTokenType } from './enum/semanticTokens';
+import type { ConfigVarArgSrc } from './resource/configKeys';
/**
* Definition of a parsed word
@@ -259,3 +260,20 @@ export interface ConfigLineData {
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
+ }
+}
\ No newline at end of file
From 899b08d2f74103fdc25fac8a88601d946e9c298b Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Tue, 27 Jan 2026 20:41:39 +0000
Subject: [PATCH 4/8] fix: better event handling
---
src/core/devMode.ts | 50 ++++++++-
src/core/diagnostics.ts | 2 +-
src/core/eventHandlers.ts | 106 ++++++++----------
src/core/manager.ts | 43 +++----
src/parsing/fileParser.ts | 34 +++++-
src/provider/completion/completetionCommon.ts | 4 +-
.../configSignatureHelpProvider.ts | 4 +-
.../runescriptSignatureHelpProvider.ts | 4 +-
8 files changed, 139 insertions(+), 108 deletions(-)
diff --git a/src/core/devMode.ts b/src/core/devMode.ts
index 1aead77..da05518 100644
--- a/src/core/devMode.ts
+++ b/src/core/devMode.ts
@@ -1,5 +1,5 @@
import type { OutputChannel, Uri } from "vscode";
-import { EventType } from "./eventHandlers";
+import type { MatchResult, ParsedWord } from "../types";
import { window } from "vscode";
import { version as packageVersion } from '../../package.json';
import { processAllFiles } from "./manager";
@@ -135,7 +135,29 @@ function formatMs(ms: number) {
return `${Math.round(ms)} ms`;
}
-export function logFileEvent(uri: Uri, event: EventType, extra?: string) {
+/**
+ * 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 enum LogType {
+ 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',
+}
+
+export function logFileEvent(uri: Uri, event: LogType, extra?: string) {
if (!isDevMode()) return;
const fileInfo = getFileInfo(uri);
logEvent(event, `on file ${fileInfo.name}.${fileInfo.type}${extra ? ` [${extra}]` : ''}`);
@@ -143,9 +165,27 @@ export function logFileEvent(uri: Uri, event: EventType, extra?: string) {
export function logSettingsEvent(setting: Settings) {
if (!isDevMode()) return;
- logEvent(EventType.SettingsChanged, `setting ${setting} updated to ${getSettingValue(setting)}`);
+ logEvent(LogType.SettingsChanged, `setting ${setting} updated to ${getSettingValue(setting)}`);
+}
+
+export function logFileParsed(startTime: number, parsedFile: Map, uri: Uri, lines: number, partial = false) {
+ if (!isDevMode()) return;
+ const fileInfo = getFileInfo(uri);
+ const msg = partial ? 'Partial reparse of file' : 'Parsed file';
+ log(`${msg} ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`);
+}
+
+export function logFileRebuild(startTime: number, uri: Uri, matches: MatchResult[]) {
+ if (!isDevMode()) return;
+ const fileInfo = getFileInfo(uri);
+ log(`Rebuilt file ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [matches: ${matches.length}]`);
+}
+
+export function logEvent(event: LogType, msg?: string) {
+ log(`Event [${event}]${msg ? ' ' + msg : ''}`, true);
}
-export function logEvent(event: EventType, msg?: string) {
- appendOutput([`[${new Date().toLocaleTimeString('en-US', { hour12: false })}] Event [${event}]${msg ? ' ' + msg : ''}`]);
+function log(message: string, skipLine = false) {
+ const base = skipLine ? [''] : [];
+ appendOutput([...base, `[${new Date().toLocaleTimeString('en-US', { hour12: false })}] ${message}`]);
}
diff --git a/src/core/diagnostics.ts b/src/core/diagnostics.ts
index 6ca8c8d..5d12306 100644
--- a/src/core/diagnostics.ts
+++ b/src/core/diagnostics.ts
@@ -32,7 +32,7 @@ export async function rebuildFileDiagnostics(uri: Uri, matchResults: MatchResult
const diagnosticsList: Diagnostic[] = [];
for (const result of matchResults) {
// Skip these types as they never have diagnostics
- if ((result.context.matchType.noop || result.context.matchType.hoverOnly)) {
+ if (result.context.matchType.noop || result.context.matchType.hoverOnly) {
continue;
}
diff --git a/src/core/eventHandlers.ts b/src/core/eventHandlers.ts
index a8d0912..cf34dc8 100644
--- a/src/core/eventHandlers.ts
+++ b/src/core/eventHandlers.ts
@@ -4,14 +4,14 @@ import { clearAllDiagnostics } 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 } from "./devMode";
+import { clearDevModeOutput, initDevMode, logEvent, logFileEvent, logSettingsEvent, LogType } from "./devMode";
import { getLines } from "../utils/stringUtils";
-import { clearFile, processAllFiles, queueFileRebuild, rebuildFileChanges } from "./manager";
+import { clearFile, processAllFiles, queueFileRebuild } from "./manager";
import { monitoredFileTypes } from "../runescriptExtension";
+import { reparseFileWithChanges } from "../parsing/fileParser";
const debounceTimeMs = 150; // debounce time for normal active file text changes
-const forceDebounceTimeMs = 75; // debounce time for force rebuilds (used by signature help and completion providers)
export function registerEventHandlers(context: ExtensionContext): void {
const patterns = Array.from(monitoredFileTypes, ext => `**/*.${ext}`);
@@ -37,93 +37,86 @@ export function registerEventHandlers(context: ExtensionContext): void {
let pendingChanges: TextDocumentContentChangeEvent[] = [];
let pendingDocument: TextDocument | undefined;
let pendingTimer: NodeJS.Timeout | undefined;
-let forcePendingDocument: TextDocument | undefined;
-let forceTimer: NodeJS.Timeout | undefined;
-let forcePromise: Promise | undefined;
-let forceResolve: (() => void) | 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 (!isValidFile(textChangeEvent.document.uri)) return;
+ if (!isActiveFile(textChangeEvent.document.uri) || !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(() => {
- if (!pendingDocument) return;
+ const doc = pendingDocument;
+ if (!doc) return;
+ logFileEvent(doc.uri, LogType.ActiveFileTextChanged, `partial reparse`);
const changes = pendingChanges;
pendingChanges = [];
pendingTimer = undefined;
- const linesReparsed = rebuildFileChanges(pendingDocument, changes);
- logFileEvent(pendingDocument.uri, EventType.ActiveFileTextChanged, `${linesReparsed} lines reparsed`);
+ 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);
}
-/**
- * Force rebuild a file and cancel any pending (partial) text doc change event debounce
- * @param document document to force the rebuild on
- * @returns a promise which resolves only when the document has finished being rebuilt
- */
-export function forceRebuild(document: TextDocument): Promise {
- if (pendingTimer) clearTimeout(pendingTimer);
- pendingChanges = [];
- pendingTimer = undefined;
- pendingDocument = undefined;
- forcePendingDocument = document;
- if (forceTimer) clearTimeout(forceTimer);
- if (!forcePromise) {
- forcePromise = new Promise((resolve) => {
- forceResolve = resolve;
- });
- }
- forceTimer = setTimeout(() => {
- const doc = forcePendingDocument;
- const resolve = forceResolve;
- forcePendingDocument = undefined;
- forceTimer = undefined;
- forcePromise = undefined;
- forceResolve = undefined;
- if (!doc) {
- resolve?.();
- return;
- }
- void queueFileRebuild(doc.uri, getLines(doc.getText())).finally(() => {
- resolve?.();
- });
- }, forceDebounceTimeMs);
- return forcePromise;
+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 (!isValidFile(editor.document.uri)) return;
- logFileEvent(editor.document.uri, EventType.ActiveFileChanged, 'full reparse');
+ logFileEvent(editor.document.uri, LogType.ActiveFileChanged, 'full reparse');
updateFileFromDocument(editor.document);
}
function onDeleteFile(uri: Uri) {
if (!isValidFile(uri)) return;
- logFileEvent(uri, EventType.FileDeleted, 'relevant cache entries invalidated');
+ logFileEvent(uri, LogType.FileDeleted, 'relevant cache entries invalidated');
clearFile(uri);
removeUris([uri]);
}
function onCreateFile(uri: Uri) {
if (!isValidFile(uri)) return;
- logFileEvent(uri, EventType.FileCreated, 'full parse');
+ logFileEvent(uri, LogType.FileCreated, 'full parse');
void updateFileFromUri(uri);
addUris([uri]);
}
function onChangeFile(uri: Uri) {
- if (isActiveFile(uri)) return; // let the change document text event handle active file changes
+ if (isActiveFile(uri)) return; // let the active document text change event handle active file changes
if (!isValidFile(uri)) return;
- logFileEvent(uri, EventType.FileChanged, 'full reparse');
+ logFileEvent(uri, LogType.FileChanged, 'full reparse');
void updateFileFromUri(uri);
}
function onGitBranchChange() {
- logEvent(EventType.GitBranchChanged, 'full cache rebuild');
+ logEvent(LogType.GitBranchChanged, 'full cache rebuild');
processAllFiles();
}
@@ -148,14 +141,3 @@ function onSettingsChange(event: ConfigurationChangeEvent) {
getSettingValue(Settings.DevMode) ? initDevMode() : clearDevModeOutput();
}
}
-
-export enum EventType {
- 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'
-}
diff --git a/src/core/manager.ts b/src/core/manager.ts
index 8316200..b68cebc 100644
--- a/src/core/manager.ts
+++ b/src/core/manager.ts
@@ -1,4 +1,4 @@
-import type { TextDocument, TextDocumentContentChangeEvent, Uri, ExtensionContext } from "vscode";
+import type { Uri, ExtensionContext } from "vscode";
import type { MatchResult, ParsedWord } from "../types";
import { ProgressLocation, window, workspace } from "vscode";
import { getActiveFile, getFileText, isActiveFile } from "../utils/fileUtils";
@@ -15,9 +15,7 @@ import { registerProviders } from "./providers";
import { parseFile } from "../parsing/fileParser";
import { monitoredFileTypes } from "../runescriptExtension";
import { findFileExceptionWords } from "../parsing/wordExceptions";
-import { isDevMode, rebuildMetrics, registerDevMode, reportRebuildMetrics } from "./devMode";
-import { applyLineChanges, getAllWords } from "../parsing/lineParser";
-import { getLines } from "../utils/stringUtils";
+import { isDevMode, logFileRebuild, rebuildMetrics, registerDevMode, reportRebuildMetrics } from "./devMode";
export function initializeExtension(context: ExtensionContext) {
registerDiagnostics(context);
@@ -54,7 +52,8 @@ export function processAllFiles() {
*/
let rebuildFileQueue = Promise.resolve();
export function queueFileRebuild(uri: Uri, fileText: string[], parsedFile?: Map): Promise {
- rebuildFileQueue = rebuildFileQueue.then(() => rebuildFile(uri, fileText, parsedFile));
+ const parsed = parsedFile ?? parseFile(uri, fileText);
+ rebuildFileQueue = rebuildFileQueue.then(() => rebuildFile(uri, fileText, parsed));
return rebuildFileQueue;
}
@@ -82,7 +81,7 @@ async function rebuildAllFiles(recordMetrics = isDevMode()): Promise {
// Parse the files into words with deeper parsing context
startTime = performance.now();
- files.forEach(file => file.parsedWords = new Map(parseFile(file.uri, file.lines)));
+ files.forEach(file => file.parsedWords = new Map(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
@@ -110,16 +109,17 @@ async function rebuildAllFiles(recordMetrics = isDevMode()): Promise {
* @param uri Uri of the file getting rebuilt
* @param lines Text of the file getting rebuilt
*/
-async function rebuildFile(uri: Uri, lines: string[], parsedFile?: Map): Promise {
- const parsed = parsedFile ?? parseFile(uri, lines);
+async function rebuildFile(uri: Uri, lines: string[], parsedFile: Map, quiet = false): Promise {
+ const startTime = performance.now();
clearFile(uri);
- initActiveFilecache(uri, parsed);
- const fileMatches: MatchResult[] = matchFile(uri, parsed, lines, false);
+ initActiveFilecache(uri, parsedFile);
+ const fileMatches: MatchResult[] = matchFile(uri, parsedFile, lines, false);
await rebuildFileDiagnostics(uri, fileMatches);
if (isActiveFile(uri)) {
rebuildSemanticTokens();
rebuildHighlights();
}
+ if (!quiet) logFileRebuild(startTime, uri, fileMatches);
}
/**
@@ -127,27 +127,10 @@ async function rebuildFile(uri: Uri, lines: string[], parsedFile?: Map {
const activeFile = getActiveFile();
- if (activeFile) void queueFileRebuild(activeFile, await getFileText(activeFile));
-}
-
-/**
- * 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 rebuildFileChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): number {
- let linesReparsed = 0;
- if (changes.length === 0) return linesReparsed;
- 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;
- linesReparsed = applyLineChanges(document, startLine, endLine, lineDelta);
+ if (activeFile) {
+ const fileText = await getFileText(activeFile);
+ void queueFileRebuild(activeFile, fileText, parseFile(activeFile, fileText, true));
}
- void queueFileRebuild(document.uri, getLines(document.getText()), getAllWords());
- return linesReparsed;
}
/**
diff --git a/src/parsing/fileParser.ts b/src/parsing/fileParser.ts
index f3ca4c3..4be04d4 100644
--- a/src/parsing/fileParser.ts
+++ b/src/parsing/fileParser.ts
@@ -1,7 +1,8 @@
-import type { Uri } from "vscode";
+import { type TextDocument, type TextDocumentContentChangeEvent, type Uri } from "vscode";
import type { ParsedWord } from "../types";
-import { getAllWords, parseLine, resetLineParser } from "./lineParser";
+import { applyLineChanges, getAllWords, 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
@@ -10,12 +11,37 @@ import { getFileText } from "../utils/fileUtils";
* @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[]): Map {
+export function parseFile(uri: Uri, fileText: string[], quiet = false): Map {
+ 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));
}
- return getAllWords();
+ const parsedFile = getAllWords();
+ if (!quiet) logFileParsed(startTime, parsedFile, uri, lines.length);
+ return parsedFile;
+}
+
+/**
+ * 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): Map | 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);
+ }
+ const parsedFile = getAllWords();
+ if (!quiet) logFileParsed(startTime, parsedFile, document.uri, linesAffected);
+ return parsedFile;
}
diff --git a/src/provider/completion/completetionCommon.ts b/src/provider/completion/completetionCommon.ts
index 3ae7115..2353307 100644
--- a/src/provider/completion/completetionCommon.ts
+++ b/src/provider/completion/completetionCommon.ts
@@ -2,7 +2,7 @@ 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 { forceRebuild } from "../../core/eventHandlers";
+import { waitForActiveFileRebuild } from "../../core/eventHandlers";
import { parseLineWithStateSnapshot } from "../../parsing/lineParser";
import { singleWordMatch } from "../../matching/matchingEngine";
import { runescriptTrigger } from "../../resource/triggers";
@@ -53,7 +53,7 @@ export function completionTypeSelector(position: Position): CompletionItem[] {
let lastRequestId = 0;
export async function searchForMatchType(document: TextDocument, position: Position, defaultMatchId = SKIP.id, fromTrigger = false): Promise {
const requestId = ++lastRequestId;
- await forceRebuild(document);
+ 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;
diff --git a/src/provider/signatureHelp/configSignatureHelpProvider.ts b/src/provider/signatureHelp/configSignatureHelpProvider.ts
index 026eb7f..8fd8321 100644
--- a/src/provider/signatureHelp/configSignatureHelpProvider.ts
+++ b/src/provider/signatureHelp/configSignatureHelpProvider.ts
@@ -1,7 +1,7 @@
import type { Position, TextDocument} from 'vscode';
import { ParameterInformation, SignatureHelp, SignatureInformation } from 'vscode';
import { buildMatchContext } from '../../utils/matchUtils';
-import { forceRebuild } from '../../core/eventHandlers';
+import { waitForActiveFileRebuild } from '../../core/eventHandlers';
import { parseLineWithStateSnapshot } from '../../parsing/lineParser';
import { getFileInfo } from '../../utils/fileUtils';
import { getConfigLineMatch } from '../../matching/matchers/configMatcher';
@@ -17,7 +17,7 @@ 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 forceRebuild(document);
+ 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);
diff --git a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
index c7e50ab..9c439f2 100644
--- a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
+++ b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
@@ -2,7 +2,7 @@ import type { Position, SignatureHelpProvider, SignatureHelpProviderMetadata, Te
import { ParameterInformation, SignatureHelp, SignatureInformation } from 'vscode';
import { TRIGGER, UNKNOWN } from '../../matching/matchType';
import { runescriptTrigger } from '../../resource/triggers';
-import { forceRebuild } from '../../core/eventHandlers';
+import { waitForActiveFileRebuild } from '../../core/eventHandlers';
import { getCallStateAtPosition } from '../../parsing/lineParser';
import { getCallIdentifier } from '../../cache/activeFileCache';
@@ -74,7 +74,7 @@ let lastRequestId = 0;
async function getParametersHelp(document: TextDocument, position: Position): Promise {
// We need the parser and active file cache states up to date
const requestId = ++lastRequestId;
- await forceRebuild(document);
+ 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
From 96ed088590cc2d8acc537476fd675cdbbfa85ff7 Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Wed, 28 Jan 2026 00:45:24 +0000
Subject: [PATCH 5/8] feat: improved diagnostics
---
src/cache/activeFileCache.ts | 4 +-
src/cache/identifierCache.ts | 35 ++---
src/core/diagnostics.ts | 126 ++++++++++++------
src/core/eventHandlers.ts | 4 +-
src/core/manager.ts | 6 +-
src/diagnostics/RunescriptDiagnostic.ts | 9 ++
src/diagnostics/unknownFileDiagnostic.ts | 20 +++
.../unknownIdentifierDiagnostic.ts | 57 ++++++++
src/matching/matchingEngine.ts | 2 +-
src/provider/renameProvider.ts | 3 +-
src/resource/identifierFactory.ts | 10 +-
src/types.ts | 10 ++
src/utils/cacheUtils.ts | 19 ++-
13 files changed, 217 insertions(+), 88 deletions(-)
create mode 100644 src/diagnostics/RunescriptDiagnostic.ts
create mode 100644 src/diagnostics/unknownFileDiagnostic.ts
create mode 100644 src/diagnostics/unknownIdentifierDiagnostic.ts
diff --git a/src/cache/activeFileCache.ts b/src/cache/activeFileCache.ts
index fd073fd..3bec2a0 100644
--- a/src/cache/activeFileCache.ts
+++ b/src/cache/activeFileCache.ts
@@ -235,7 +235,7 @@ function cacheLocalVariable(matchResult: MatchResult): void {
if (!localVarIden) return;
const fileKey = resolveFileKey(matchResult.context.uri);
if (!fileKey) return;
- const refs = addReference(localVarIden, fileKey, lineNum, matchResult.context.word.start);
+ const refs = addReference(localVarIden, fileKey, lineNum, matchResult.context.word.start, matchResult.context.word.end);
localVarIden.references[fileKey] = refs;
}
}
@@ -252,7 +252,7 @@ function createLocalVariableIdentifier(matchResult: MatchResult): Identifier | u
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);
+ const refs = addReference(localVarIdentifier, fileKey, matchResult.context.line.number, matchResult.context.word.start, matchResult.context.word.end);
localVarIdentifier.references[fileKey] = refs;
return localVarIdentifier;
}
diff --git a/src/cache/identifierCache.ts b/src/cache/identifierCache.ts
index 8569f04..69a6d6e 100644
--- a/src/cache/identifierCache.ts
+++ b/src/cache/identifierCache.ts
@@ -1,6 +1,6 @@
const sizeof = require('object-sizeof');
import type { Uri } from 'vscode';
-import type { FileKey, Identifier, IdentifierKey, IdentifierText, MatchContext, MatchType } from '../types';
+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';
@@ -18,25 +18,6 @@ const identifierCache = new Map();
*/
const fileToIdentifierMap = new Map();
-/**
- * Tracks the keys of identifier declarations and references within a file
- */
-interface FileIdentifiers {
- declarations: Set;
- references: Set;
-}
-
-/**
- * Check if the identifierCache contains an item using the identifier name and match type
- * @param name Name of the identifier
- * @param match MatchType of the identifier
- * @returns boolean: true if found, false otherwise
- */
-function contains(name: string, match: MatchType): boolean {
- const key = resolveIdentifierKey(name, match);
- return key !== undefined && identifierCache.has(key);
-}
-
/**
* Get the cached identifier using the identifier name and match type
* @param name Name of the identifier
@@ -98,7 +79,7 @@ function put(name: string, context: MatchContext, text: IdentifierText): Identif
putCompletionCache(name, context.matchType.id);
// Also insert the declaration as a reference
- putReference(name, context, context.uri, context.line.number, context.word.start);
+ putReference(name, context, context.uri, context.line.number, context.word.start, context.word.end);
// Return the created identifier
return identifier;
@@ -110,10 +91,10 @@ function put(name: string, context: MatchContext, text: IdentifierText): Identif
* @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 index the index within the line where the reference is found
+ * @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, index: number): void {
+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);
@@ -131,7 +112,7 @@ function putReference(name: string, context: MatchContext, uri: Uri, lineNum: nu
// 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, index, context);
+ const fileReferences = addReference(curIdentifier, fileKey, lineNum, startIndex, endIndex, context);
// Add the reference to the file map
addToFileMap(fileKey, key, false);
@@ -257,4 +238,8 @@ function getTotalReferences(): number {
return total;
}
-export { contains, get, getByKey, put, putReference, clear, clearFile, serializeCache, getCacheKeys, getCacheKeyCount, appriximateSize, getTotalReferences };
+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/core/diagnostics.ts b/src/core/diagnostics.ts
index 5d12306..fb212e4 100644
--- a/src/core/diagnostics.ts
+++ b/src/core/diagnostics.ts
@@ -1,14 +1,23 @@
-import type { DiagnosticCollection, ExtensionContext, Uri } from 'vscode';
-import type { Identifier, MatchResult } from '../types';
-import { Diagnostic } from 'vscode';
-import { DiagnosticSeverity, languages, Range } from 'vscode';
-import { get as getIdentifierFromCache } from '../cache/identifierCache';
-import { fileNamePostProcessor } from '../resource/postProcessors';
-import { exists as projectFileExists } from '../cache/projectFilesCache';
+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 { getByKey } from '../cache/identifierCache';
+import { decodeReferenceToRange } from '../utils/cacheUtils';
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() });
@@ -17,14 +26,21 @@ export function registerDiagnostics(context: ExtensionContext): void {
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 async function rebuildFileDiagnostics(uri: Uri, matchResults: MatchResult[]): Promise {
@@ -36,52 +52,74 @@ export async function rebuildFileDiagnostics(uri: Uri, matchResults: MatchResult
continue;
}
- // Build the range for the diagnostic, if needed
+ // Build the range for the diagnostic
const { line: { number: lineNum }, word: { start, end } } = result.context;
- const range = buildRange(lineNum, start, end);
-
- // Check for matches that reference an actual file, and make sure the file exists
- if (result.context.matchType.postProcessor === fileNamePostProcessor) {
- const fileName = `${result.word}.${(result.context.matchType.fileTypes || [])[0] ?? 'rs2'}`;
- if (!projectFileExists(fileName)) {
- diagnosticsList.push(buildFileNotFoundDiagnostic(range, fileName));
+ 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);
}
- }
-
- // Below this point the identifier itself is needed
- const identifier: Identifier | undefined = getIdentifierFromCache(result.word, result.context.matchType);
- if (!identifier) {
- continue;
- }
-
- // Check for unknown identifiers that are trying to be used
- else if (!result.context.matchType.referenceOnly && !result.context.declaration && !identifier.declaration) {
- diagnosticsList.push(buildUnknownItemDiagnostic(range, result.context.matchType.id, result.word));
- }
+ });
}
diagnostics.set(uri, diagnosticsList);
}
-export function getFileDiagnostics(uri: Uri): readonly Diagnostic[] {
- return diagnostics?.get(uri) || [];
-}
+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[] = [];
-function buildUnknownItemDiagnostic(range: Range, matchTypeId: string, word: string): Diagnostic {
- return buildDiagnostic(range, `Unknown ${matchTypeId.toLowerCase()}: ${word}`);
-}
+ for (const key of beforeDecs) {
+ if (!afterDecs.has(key)) {
+ removedDeclarations.push(key);
+ }
+ }
+ for (const key of afterDecs) {
+ if (!beforeDecs.has(key)) {
+ addedDeclarations.push(key);
+ }
+ }
-function buildFileNotFoundDiagnostic(range: Range, fileName: string): Diagnostic {
- return buildDiagnostic(range, `Refers to file ${fileName}, but it doesn't exist`);
-}
+ // 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)
+ }
+ }
-function buildDiagnostic(range: Range, message: string, severity = DiagnosticSeverity.Warning): Diagnostic {
- const diagnostic = new Diagnostic(range, message, severity);
- diagnostic.source = 'runescript';
- return diagnostic;
+ // 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);
+ }
+ }
}
-function buildRange(lineNum: number, start: number, end: number): Range {
- return new Range(lineNum, start, lineNum, end + 1);
+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
index cf34dc8..98be60c 100644
--- a/src/core/eventHandlers.ts
+++ b/src/core/eventHandlers.ts
@@ -1,6 +1,6 @@
import type { ConfigurationChangeEvent, ExtensionContext, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, TextEditor, Uri } from "vscode";
import { window, workspace } from "vscode";
-import { clearAllDiagnostics } from "./diagnostics";
+import { clearAllDiagnostics, handleFileUpdate } from "./diagnostics";
import { getFileText, isActiveFile, isValidFile } from "../utils/fileUtils";
import { addUris, removeUris } from "../cache/projectFilesCache";
import { eventAffectsSetting, getSettingValue, Settings } from "./settings";
@@ -9,6 +9,7 @@ import { getLines } from "../utils/stringUtils";
import { clearFile, processAllFiles, queueFileRebuild } from "./manager";
import { monitoredFileTypes } from "../runescriptExtension";
import { reparseFileWithChanges } from "../parsing/fileParser";
+import { getFileIdentifiers } from "../cache/identifierCache";
const debounceTimeMs = 150; // debounce time for normal active file text changes
@@ -97,6 +98,7 @@ async function onActiveDocumentChange(editor: TextEditor | undefined): Promise {
*/
async function rebuildFile(uri: Uri, lines: string[], parsedFile: Map, quiet = false): Promise {
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();
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..d0a532c
--- /dev/null
+++ b/src/diagnostics/unknownFileDiagnostic.ts
@@ -0,0 +1,20 @@
+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 {
+ fileName: string = '';
+
+ check(result: MatchResult): boolean {
+ if (result.context.matchType.postProcessor !== fileNamePostProcessor) return false;
+ this.fileName = `${result.word}.${(result.context.matchType.fileTypes || [])[0] ?? 'rs2'}`;
+ return !projectFileExists(this.fileName)
+ }
+
+ createDiagnostic(range: Range): Diagnostic {
+ return new Diagnostic(range, `Refers to file ${this.fileName}, but it doesn't exist`, DiagnosticSeverity.Warning);
+ }
+}
diff --git a/src/diagnostics/unknownIdentifierDiagnostic.ts b/src/diagnostics/unknownIdentifierDiagnostic.ts
new file mode 100644
index 0000000..a3c75a0
--- /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 anda. 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/matching/matchingEngine.ts b/src/matching/matchingEngine.ts
index d87c45a..a1a745b 100644
--- a/src/matching/matchingEngine.ts
+++ b/src/matching/matchingEngine.ts
@@ -102,7 +102,7 @@ export function matchFile(uri: Uri, parsedFile: Map, lines
} 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);
+ putReference(match.word, match.context, uri, lineNum, index, match.context.word.end);
}
}
}
diff --git a/src/provider/renameProvider.ts b/src/provider/renameProvider.ts
index d1ebb14..53aa1ad 100644
--- a/src/provider/renameProvider.ts
+++ b/src/provider/renameProvider.ts
@@ -43,11 +43,10 @@ export const renameProvider: RenameProvider = {
function renameReferences(identifier: Identifier | undefined, oldName: string, newName: string): WorkspaceEdit {
const renameWorkspaceEdits = new WorkspaceEdit();
if (identifier?.references) {
- const wordLength = oldName.length - oldName.indexOf(':') - 1;
Object.keys(identifier.references).forEach(fileKey => {
const uri = Uri.file(fileKey);
identifier.references[fileKey].forEach((encodedReference: string) => {
- const range = decodeReferenceToRange(wordLength, encodedReference);
+ const range = decodeReferenceToRange(encodedReference);
if (range) {
renameWorkspaceEdits.replace(uri, range, newName);
}
diff --git a/src/resource/identifierFactory.ts b/src/resource/identifierFactory.ts
index 6ad8034..d21ad76 100644
--- a/src/resource/identifierFactory.ts
+++ b/src/resource/identifierFactory.ts
@@ -3,13 +3,14 @@ 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 } from '../utils/cacheUtils';
+import { encodeReference, resolveIdentifierKey } from '../utils/cacheUtils';
export function buildFromDeclaration(name: string, context: MatchContext, text?: IdentifierText): Identifier {
const identifier: Identifier = {
name: name,
matchId: context.matchType.id,
- declaration: { uri: context.uri, ref: encodeReference(context.line.number, context.word.start) },
+ 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)
@@ -22,6 +23,7 @@ export function buildFromReference(name: string, context: MatchContext): Identif
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),
@@ -32,9 +34,9 @@ export function buildFromReference(name: string, context: MatchContext): Identif
return identifier;
}
-export function addReference(identifier: Identifier, fileKey: string, lineNum: number, index: number, context?: MatchContext): Set {
+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, index));
+ fileReferences.add(encodeReference(lineNum, startIndex, endIndex));
if (context && context.packId) identifier.id = context.packId;
return fileReferences;
}
diff --git a/src/types.ts b/src/types.ts
index 4ba5fe4..21c25f9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -101,6 +101,8 @@ export interface 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 */
@@ -145,6 +147,14 @@ export interface IdentifierText {
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
*/
diff --git a/src/utils/cacheUtils.ts b/src/utils/cacheUtils.ts
index a756a5c..70c0262 100644
--- a/src/utils/cacheUtils.ts
+++ b/src/utils/cacheUtils.ts
@@ -2,8 +2,8 @@ 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 | undefined {
- return (!name || !match) ? undefined : name + match.id;
+export function resolveIdentifierKey(name: string, match: MatchType): IdentifierKey {
+ return name + match.id;
}
export function resolveKeyFromIdentifier(iden: Identifier): string {
@@ -14,20 +14,25 @@ export function resolveFileKey(uri: Uri): FileKey | undefined {
return uri.fsPath;
}
-export function encodeReference(line: number, index: number): string {
- return `${line}|${index}`;
+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 !== 2) ? undefined : new Location(uri, new Position(Number(split[0]), Number(split[1])));
+ return (split.length !== 3) ? undefined : new Location(uri, new Position(Number(split[0]), Number(split[1])));
}
-export function decodeReferenceToRange(wordLength: number, encodedValue: string): Range | undefined {
+export function decodeReferenceToRange(encodedValue: string): Range | undefined {
const split = encodedValue.split('|');
- if (split.length !== 2) {
+ 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);
+}
From 9db658ca8da586bb96b64211dc1d7f36fd6cbc8b Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Thu, 29 Jan 2026 17:50:49 +0000
Subject: [PATCH 6/8] feat: introduce more match types
---
src/cache/activeFileCache.ts | 120 +++++++++++++-----
src/core/devMode.ts | 81 +++++++++---
src/core/diagnostics.ts | 2 +-
src/core/eventHandlers.ts | 12 +-
src/core/highlights.ts | 26 +++-
src/core/manager.ts | 26 ++--
src/enum/regex.ts | 5 +-
src/matching/matchType.ts | 43 ++++---
src/matching/matchers/booleanMatcher.ts | 16 +++
src/matching/matchers/commandMatcher.ts | 3 +-
src/matching/matchers/configMatcher.ts | 2 +-
src/matching/matchers/keyWordTypeMatcher.ts | 19 +++
src/matching/matchers/parametersMatcher.ts | 2 +-
src/matching/matchers/regexWordMatcher.ts | 11 +-
src/matching/matchers/triggerMatcher.ts | 39 ++++--
src/matching/matchingEngine.ts | 48 +++----
src/matching/operatorMatching.ts | 1 +
src/parsing/fileParser.ts | 20 +--
src/parsing/lineParser.ts | 79 +++++++++++-
src/parsing/operators.ts | 10 ++
src/provider/gotoDefinitionProvider.ts | 6 +-
src/provider/hoverProvider.ts | 20 ++-
src/provider/referenceProvider.ts | 6 +-
src/provider/renameProvider.ts | 4 +-
.../runescriptSignatureHelpProvider.ts | 2 +-
src/resource/identifierFactory.ts | 21 ++-
src/resource/postProcessors.ts | 9 +-
src/types.ts | 28 +++-
src/utils/markdownUtils.ts | 75 ++++++-----
syntaxes/interface.tmLanguage.json | 2 +-
syntaxes/runescript.tmLanguage.json | 7 +-
31 files changed, 527 insertions(+), 218 deletions(-)
create mode 100644 src/matching/matchers/booleanMatcher.ts
create mode 100644 src/matching/matchers/keyWordTypeMatcher.ts
create mode 100644 src/matching/operatorMatching.ts
create mode 100644 src/parsing/operators.ts
diff --git a/src/cache/activeFileCache.ts b/src/cache/activeFileCache.ts
index 3bec2a0..ec1e58c 100644
--- a/src/cache/activeFileCache.ts
+++ b/src/cache/activeFileCache.ts
@@ -1,13 +1,14 @@
import type { Position, TextDocument, Uri } from "vscode";
-import type { DataRange, Identifier, IdentifierText, Item, MatchResult, MatchType, ParsedWord } from "../types";
+import type { DataRange, Identifier, IdentifierText, Item, MatchResult, MatchType, OperatorToken, ParsedFile, ParsedWord } from "../types";
import { get as getIdentifier } from "./identifierCache";
-import { LOCAL_VAR, QUEUE, SKIP, SWITCH, TRIGGER, UNKNOWN } from "../matching/matchType";
+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 */
@@ -31,6 +32,12 @@ const fileMatches = new Map[]>();
*/
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();
+
// ===== GET DATA ===== //
/**
@@ -39,7 +46,7 @@ let parsedWords: Map = new Map();
* @param position The position (line num + index) to get the item for
* @returns The item at that positon, if exists
*/
-export async function getByDocPosition(document: TextDocument, position: Position): Promise- {
+export function getByDocPosition(document: TextDocument, position: Position): Item | undefined {
return get(document.uri, position.line, position.character);
}
@@ -50,8 +57,8 @@ export async function getByDocPosition(document: TextDocument, position: Positio
* @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(lineNum: number, lineIndex: number): Item | undefined {
- return getNoAsync(lineNum, lineIndex);
+export function getByLineIndex(uri: Uri, lineNum: number, lineIndex: number): Item | undefined {
+ return get(uri, lineNum, lineIndex);
}
/**
@@ -60,25 +67,38 @@ export function getByLineIndex(lineNum: number, lineIndex: number): Item | undef
* @param position The position (line num + index) to get the word for
* @returns The word at that positon, if exists
*/
-export async function getParsedWordByDocPosition(document: TextDocument, position: Position): Promise {
+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 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(lineNum: number, callName: string, callNameIndex: number): Identifier | undefined {
+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 = getNoAsync(curLine, potentialCallWord.start);
+ 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];
@@ -91,6 +111,14 @@ export function getCallIdentifier(lineNum: number, callName: string, callNameInd
}
}
+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
@@ -98,27 +126,10 @@ export function getCallIdentifier(lineNum: number, callName: string, callNameInd
* @param index Position/Index the item is at within that line
* @returns The item at that position, if it exists
*/
-async function get(uri: Uri, lineNum: number, index: number): Promise
- {
- // In case we try to access cache data with the wrong file, it should be updated soon so poll until it matches (100ms max)
- let tries = 0;
- while (file !== uri.fsPath && tries < 20) {
- await new Promise((resolve) => setTimeout(resolve, 5));
- tries++;
- }
+function get(uri: Uri, lineNum: number, index: number): Item | undefined {
if (file !== uri.fsPath) {
- return undefined; // File never matched even after 100ms, give up (something is wrong!)
+ return undefined;
}
- return getNoAsync(lineNum, index);
-}
-
-/**
- * The core get item, bypassing the file name polling
- * @param lineNum Line number the item is on
- * @param index Position/Index the item is at within that line
- * @returns
- */
-function getNoAsync(lineNum: number, index: number): Item | undefined {
- // Get the item from the cache, if there is one on the line at that index, otherwise return early
const result = findMatchInRange(index, fileMatches.get(lineNum))?.data;
if (result) {
return buildItem(result);
@@ -158,15 +169,23 @@ export function getAllParsedWords(): Map {
return parsedWords;
}
+/**
+ * Returns all of the matches and parsed words for the file
+ */
+export function getAllOperatorTokens(): Map {
+ return operatorTokens;
+}
+
// ==== 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: Map) {
+export function init(uri: Uri, parsedFile: ParsedFile) {
fileMatches.clear();
- parsedWords = parsedFile;
+ parsedWords = parsedFile.parsedWords;
+ operatorTokens = parsedFile.operatorTokens;
localVarCache.clear();
codeBlockCache.clear();
switchStmtCache.clear();
@@ -180,6 +199,7 @@ export function init(uri: Uri, parsedFile: Map) {
export function clear() {
fileMatches.clear();
parsedWords = new Map();
+ operatorTokens = new Map();
localVarCache.clear();
codeBlockCache.clear();
switchStmtCache.clear();
@@ -203,6 +223,46 @@ export function processMatch(result: MatchResult): void {
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
@@ -347,7 +407,7 @@ export function getBlockScopeIdentifier(lineNum: number): Identifier | undefined
const switchStmtCache: Map> = new Map();
export function cacheSwitchStmt(result: MatchResult): void {
- if (result.context.matchType.id === SWITCH.id) {
+ 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));
diff --git a/src/core/devMode.ts b/src/core/devMode.ts
index da05518..6b08aba 100644
--- a/src/core/devMode.ts
+++ b/src/core/devMode.ts
@@ -1,6 +1,6 @@
import type { OutputChannel, Uri } from "vscode";
-import type { MatchResult, ParsedWord } from "../types";
-import { window } 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";
@@ -123,7 +123,7 @@ export function reportRebuildMetrics(): void {
* @returns formatted string
*/
function formatToSec(ms: number) {
- return `${(ms / 1000).toFixed(2)}s`;
+ return `${(ms / 1000).toFixed(2)} s`;
}
/**
@@ -158,34 +158,73 @@ export enum LogType {
}
export function logFileEvent(uri: Uri, event: LogType, extra?: string) {
- if (!isDevMode()) return;
- const fileInfo = getFileInfo(uri);
- logEvent(event, `on file ${fileInfo.name}.${fileInfo.type}${extra ? ` [${extra}]` : ''}`);
+ const resolver = () => {
+ const fileInfo = getFileInfo(uri);
+ return `on file ${fileInfo.name}.${fileInfo.type}${extra ? ` [${extra}]` : ''}`;
+ }
+ logEvent(event, resolver);
}
export function logSettingsEvent(setting: Settings) {
- if (!isDevMode()) return;
- logEvent(LogType.SettingsChanged, `setting ${setting} updated to ${getSettingValue(setting)}`);
+ const resolver = () => `setting ${setting} updated to ${getSettingValue(setting)}`;
+ logEvent(LogType.SettingsChanged, resolver);
}
-export function logFileParsed(startTime: number, parsedFile: Map, uri: Uri, lines: number, partial = false) {
- if (!isDevMode()) return;
- const fileInfo = getFileInfo(uri);
- const msg = partial ? 'Partial reparse of file' : 'Parsed file';
- log(`${msg} ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`);
+export function logEvent(event: LogType, 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 fileInfo = getFileInfo(uri);
+ const msg = partial ? 'Partial reparse of file' : 'Parsed file';
+ return `${msg} ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`;
+ }
+ log(resolver, LogLevel.Debug);
}
export function logFileRebuild(startTime: number, uri: Uri, matches: MatchResult[]) {
- if (!isDevMode()) return;
- const fileInfo = getFileInfo(uri);
- log(`Rebuilt file ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [matches: ${matches.length}]`);
+ const resolver = () => {
+ const fileInfo = getFileInfo(uri);
+ return `Rebuilt file ${fileInfo.name}.${fileInfo.type} 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 logEvent(event: LogType, msg?: string) {
- log(`Event [${event}]${msg ? ' ' + msg : ''}`, true);
+export function logWarning(message: string) {
+ log(() => message, LogLevel.Warning);
}
-function log(message: string, skipLine = false) {
- const base = skipLine ? [''] : [];
- appendOutput([...base, `[${new Date().toLocaleTimeString('en-US', { hour12: false })}] ${message}`]);
+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
index fb212e4..45a15fa 100644
--- a/src/core/diagnostics.ts
+++ b/src/core/diagnostics.ts
@@ -48,7 +48,7 @@ export async function rebuildFileDiagnostics(uri: Uri, matchResults: MatchResult
const diagnosticsList: Diagnostic[] = [];
for (const result of matchResults) {
// Skip these types as they never have diagnostics
- if (result.context.matchType.noop || result.context.matchType.hoverOnly) {
+ if (result.context.matchType.noop || !result.context.matchType.cache) {
continue;
}
diff --git a/src/core/eventHandlers.ts b/src/core/eventHandlers.ts
index 98be60c..46d7c7c 100644
--- a/src/core/eventHandlers.ts
+++ b/src/core/eventHandlers.ts
@@ -8,7 +8,7 @@ import { clearDevModeOutput, initDevMode, logEvent, logFileEvent, logSettingsEve
import { getLines } from "../utils/stringUtils";
import { clearFile, processAllFiles, queueFileRebuild } from "./manager";
import { monitoredFileTypes } from "../runescriptExtension";
-import { reparseFileWithChanges } from "../parsing/fileParser";
+import { parseFile, reparseFileWithChanges } from "../parsing/fileParser";
import { getFileIdentifiers } from "../cache/identifierCache";
@@ -61,7 +61,7 @@ function onActiveFileTextChange(textChangeEvent: TextDocumentChangeEvent): void
const changes = pendingChanges;
pendingChanges = [];
pendingTimer = undefined;
- const parsedFile = reparseFileWithChanges(doc, changes);
+ 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--) {
@@ -118,18 +118,20 @@ function onChangeFile(uri: Uri) {
}
function onGitBranchChange() {
- logEvent(LogType.GitBranchChanged, 'full cache rebuild');
+ logEvent(LogType.GitBranchChanged, () => 'full cache rebuild');
processAllFiles();
}
async function updateFileFromUri(uri: Uri): Promise {
if (!isValidFile(uri)) return;
- void queueFileRebuild(uri, await getFileText(uri));
+ const fileText = await getFileText(uri);
+ void queueFileRebuild(uri, fileText, parseFile(uri, fileText));
}
function updateFileFromDocument(document: TextDocument): void {
if (!isValidFile(document.uri)) return;
- void queueFileRebuild(document.uri, getLines(document.getText()));
+ const fileText = getLines(document.getText());
+ void queueFileRebuild(document.uri, fileText, parseFile(document.uri, fileText));
}
function onSettingsChange(event: ConfigurationChangeEvent) {
diff --git a/src/core/highlights.ts b/src/core/highlights.ts
index cc5e0ea..1826fac 100644
--- a/src/core/highlights.ts
+++ b/src/core/highlights.ts
@@ -1,10 +1,10 @@
import { Position, Range, type TextEditor } from 'vscode';
import { DecorationRangeBehavior, window } from "vscode";
-import { getAllMatches, getAllParsedWords } from '../cache/activeFileCache';
+import { getAllMatches, getAllOperatorTokens, getAllParsedWords } from '../cache/activeFileCache';
import { isDevMode } from './devMode';
const matchDecoration = window.createTextEditorDecorationType({
- backgroundColor: 'rgba(255, 200, 0, 0.25)',
+ backgroundColor: 'rgba(80, 200, 120, 0.20)',
rangeBehavior: DecorationRangeBehavior.ClosedClosed
});
@@ -13,6 +13,11 @@ const wordDecoration = window.createTextEditorDecorationType({
rangeBehavior: DecorationRangeBehavior.ClosedClosed
});
+const operatorDecoration = window.createTextEditorDecorationType({
+ backgroundColor: 'rgba(160, 80, 255, 0.25)',
+ rangeBehavior: DecorationRangeBehavior.ClosedClosed
+});
+
enum HighlightMode {
Disabled = 'disabled',
Matches = 'matches',
@@ -37,6 +42,7 @@ function buildHighlights(editor: TextEditor, mode = HighlightMode.AllWords) {
case HighlightMode.AllWords:
editor.setDecorations(matchDecoration, getMatchRanges());
editor.setDecorations(wordDecoration, getWordRanges());
+ editor.setDecorations(operatorDecoration, getOperatorTokenRanges());
break;
}
}
@@ -46,10 +52,18 @@ function getMatchRanges(): Range[] {
}
function getWordRanges(): Range[] {
- const matches = getMatchRanges();
- const words: Range[] = [];
+ const matchRanges = getMatchRanges();
+ const wordRanges: Range[] = [];
getAllParsedWords().forEach((parsedLineWords, lineNum) => {
- parsedLineWords.forEach(word => words.push(new Range(new Position(lineNum, word.start), new Position(lineNum, word.end + 1))));
+ 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 words.filter(range => !matches.some(match => match.intersection(range)));
+ return operatorRanges;
}
diff --git a/src/core/manager.ts b/src/core/manager.ts
index 0605d50..a1ea4b6 100644
--- a/src/core/manager.ts
+++ b/src/core/manager.ts
@@ -1,5 +1,5 @@
import type { Uri, ExtensionContext } from "vscode";
-import type { MatchResult, ParsedWord } from "../types";
+import type { MatchResult, ParsedFile } from "../types";
import { ProgressLocation, window, workspace } from "vscode";
import { getActiveFile, getFileText, isActiveFile } from "../utils/fileUtils";
import { matchFile } from "../matching/matchingEngine";
@@ -51,9 +51,8 @@ export function processAllFiles() {
* Add a file to get rebuilt into the rebuild file queue
*/
let rebuildFileQueue = Promise.resolve();
-export function queueFileRebuild(uri: Uri, fileText: string[], parsedFile?: Map): Promise {
- const parsed = parsedFile ?? parseFile(uri, fileText);
- rebuildFileQueue = rebuildFileQueue.then(() => rebuildFile(uri, fileText, parsed));
+export function queueFileRebuild(uri: Uri, fileText: string[], parsedFile: ParsedFile, quiet = false): Promise {
+ rebuildFileQueue = rebuildFileQueue.then(() => rebuildFile(uri, fileText, parsedFile, quiet));
return rebuildFileQueue;
}
@@ -70,7 +69,7 @@ async function rebuildAllFiles(recordMetrics = isDevMode()): Promise {
// Read and parse all of the relevant project files
let startTime = performance.now();
- type File = { uri: Uri; lines: string[]; parsedWords?: Map };
+ 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;
@@ -81,23 +80,23 @@ async function rebuildAllFiles(recordMetrics = isDevMode()): Promise {
// Parse the files into words with deeper parsing context
startTime = performance.now();
- files.forEach(file => file.parsedWords = new Map(parseFile(file.uri, file.lines, true)));
+ 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.parsedWords!);
- matchFile(file.uri, file.parsedWords!, file.lines, true);
- if (recordMetrics) rebuildMetrics.wordCount += [...file.parsedWords!.values()].reduce((sum, words) => sum + words.length, 0);
+ 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.parsedWords!);
- const matchResults = matchFile(file.uri, file.parsedWords!, file.lines, false);
+ 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;
@@ -109,7 +108,8 @@ async function rebuildAllFiles(recordMetrics = isDevMode()): Promise {
* @param uri Uri of the file getting rebuilt
* @param lines Text of the file getting rebuilt
*/
-async function rebuildFile(uri: Uri, lines: string[], parsedFile: Map, quiet = false): Promise {
+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);
@@ -131,7 +131,7 @@ async function rebuildActiveFile(): Promise {
const activeFile = getActiveFile();
if (activeFile) {
const fileText = await getFileText(activeFile);
- void queueFileRebuild(activeFile, fileText, parseFile(activeFile, fileText, true));
+ void queueFileRebuild(activeFile, fileText, parseFile(activeFile, fileText));
}
}
diff --git a/src/enum/regex.ts b/src/enum/regex.ts
index 2161a21..3539cb5 100644
--- a/src/enum/regex.ts
+++ b/src/enum/regex.ts
@@ -1,8 +1,11 @@
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 = /^\d+.?\d+$/;
+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]).*/;
diff --git a/src/matching/matchType.ts b/src/matching/matchType.ts
index 537da88..f6c7139 100644
--- a/src/matching/matchType.ts
+++ b/src/matching/matchType.ts
@@ -1,7 +1,8 @@
import type { MatchType } from '../types';
-import { dataTypePostProcessor, enumPostProcessor, columnPostProcessor, rowPostProcessor, componentPostProcessor,
+import { globalVarPostProcessor, enumPostProcessor, columnPostProcessor, rowPostProcessor, componentPostProcessor,
fileNamePostProcessor, coordPostProcessor, configKeyPostProcessor, triggerPostProcessor, categoryPostProcessor,
- paramPostProcessor} from '../resource/postProcessors';
+ paramPostProcessor,
+ localVarPostProcessor} from '../resource/postProcessors';
import { CODEBLOCK, INFO, SIGNATURE, TITLE, VALUE } from "../enum/hoverDisplayItems";
import { SemanticTokenType } from '../enum/semanticTokens';
@@ -15,12 +16,13 @@ function defineMatchType(match: MatchType): MatchType {
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: dataTypePostProcessor
+ postProcessor: globalVarPostProcessor
});
export const CONSTANT: MatchType = defineMatchType({
@@ -175,64 +177,67 @@ export const STRUCT: MatchType = defineMatchType({
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'], hoverOnly: true, cache: false, allowRename: false,
+ id: 'COORDINATES', types: ['coord'], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE, VALUE] },
postProcessor: coordPostProcessor
});
export const CONFIG_KEY: MatchType = defineMatchType({
- id: 'CONFIG_KEY', types: [], hoverOnly: true, cache: false, allowRename: false,
+ id: 'CONFIG_KEY', types: [], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE, INFO] },
postProcessor: configKeyPostProcessor
});
export const TRIGGER: MatchType = defineMatchType({
- id: 'TRIGGER', types: [], hoverOnly: true, cache: false, allowRename: false,
+ id: 'TRIGGER', types: [], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE, INFO] },
postProcessor: triggerPostProcessor
});
export const STAT: MatchType = defineMatchType({
- id: 'STAT', types: ['stat'], hoverOnly: true, cache: false, allowRename: false,
+ id: 'STAT', types: ['stat'], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE] },
});
export const NPC_STAT: MatchType = defineMatchType({
- id: 'NPC_STAT', types: ['npc_stat'], hoverOnly: true, cache: false, allowRename: false,
+ 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'], hoverOnly: true, cache: false, allowRename: false,
+ id: 'NPC_MODE', types: ['npc_mode'], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE] },
});
export const LOCSHAPE: MatchType = defineMatchType({
- id: 'LOCSHAPE', types: ['locshape'], hoverOnly: true, cache: false, allowRename: false,
+ id: 'LOCSHAPE', types: ['locshape'], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE] },
});
export const FONTMETRICS: MatchType = defineMatchType({
- id: 'FONTMETRICS', types: ['fontmetrics'], hoverOnly: true, cache: false, allowRename: false,
+ id: 'FONTMETRICS', types: ['fontmetrics'], cache: false, allowRename: false,
hoverConfig: { referenceItems: [TITLE] },
});
-export const CATEGORY: MatchType = defineMatchType({
- id: 'CATEGORY', types: ['category'], hoverOnly: true, cache: true, allowRename: true, referenceOnly: true,
- hoverConfig: { referenceItems: [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
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 COLOR: MatchType = defineMatchType({ id: 'COLOR', types: [], fileTypes: [], cache: false, allowRename: false, noop: true });
-export const SWITCH: MatchType = defineMatchType({ id: 'SWITCH', 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 });
+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);
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/commandMatcher.ts b/src/matching/matchers/commandMatcher.ts
index 295f7fa..f05f01b 100644
--- a/src/matching/matchers/commandMatcher.ts
+++ b/src/matching/matchers/commandMatcher.ts
@@ -1,6 +1,6 @@
import type { MatchContext, Matcher } from '../../types';
import { get as getIdentifier } from "../../cache/identifierCache";
-import { COMMAND, SKIP } from "../matchType";
+import { COMMAND } from "../matchType";
import { reference, declaration } from "../../utils/matchUtils";
import { TRIGGER_LINE_REGEX } from "../../enum/regex";
@@ -12,7 +12,6 @@ const commandMatcherFn = (context: MatchContext): void => {
if (command) {
if (TRIGGER_LINE_REGEX.test(context.line.text)) {
if (context.word.index === 1) return declaration(COMMAND, context);
- else if (context.word.index > 1) return reference(SKIP, context);
}
if (command.signature && command.signature.params.length > 0 && context.nextChar !== '('){
return undefined;
diff --git a/src/matching/matchers/configMatcher.ts b/src/matching/matchers/configMatcher.ts
index 03f1057..c507e9d 100644
--- a/src/matching/matchers/configMatcher.ts
+++ b/src/matching/matchers/configMatcher.ts
@@ -41,7 +41,7 @@ export function getConfigLineMatch(context: MatchContext): ConfigLineData | unde
iden = getBlockScopeIdentifier(context.line.number);
}
else if (configData.varArgs.idenSrc === ConfigVarArgSrc.FirstParam) {
- iden = getByLineIndex(context.line.number, context.words[1].start)?.identifier;
+ 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;
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/parametersMatcher.ts b/src/matching/matchers/parametersMatcher.ts
index e331715..8ece4d3 100644
--- a/src/matching/matchers/parametersMatcher.ts
+++ b/src/matching/matchers/parametersMatcher.ts
@@ -23,7 +23,7 @@ function parametersMatcherFn(context: MatchContext): void {
return undefined;
}
- const iden = getCallIdentifier(context.line.number, context.word.callName, context.word.callNameIndex);
+ 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;
diff --git a/src/matching/matchers/regexWordMatcher.ts b/src/matching/matchers/regexWordMatcher.ts
index 801cd8a..f0cecc4 100644
--- a/src/matching/matchers/regexWordMatcher.ts
+++ b/src/matching/matchers/regexWordMatcher.ts
@@ -1,6 +1,6 @@
import type { MatchContext, Matcher } from '../../types';
-import { COLOR_REGEX, COORD_REGEX, NUMBER_REGEX, SWITCH_TYPE_REGEX } from "../../enum/regex";
-import { COLOR, COORDINATES, SKIP, SWITCH } from "../matchType";
+import { COORD_REGEX, NUMBER_REGEX, SWITCH_TYPE_REGEX } from "../../enum/regex";
+import { COORDINATES, NUMBER, KEYWORD } from "../matchType";
import { reference } from "../../utils/matchUtils";
/**
@@ -9,16 +9,13 @@ import { reference } from "../../utils/matchUtils";
function regexWordMatcherFn(context: MatchContext): void {
const word = context.word.value;
if (NUMBER_REGEX.test(word)) {
- return reference(SKIP, context); // extension doesnt need to know if word is a number, but we can short circuit the matchers here by returning SKIP
+ 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 (COLOR_REGEX.test(word)) {
- return reference(COLOR, context);
- }
if (SWITCH_TYPE_REGEX.test(word)) {
- return reference(SWITCH, context);
+ return reference(KEYWORD, context);
}
}
diff --git a/src/matching/matchers/triggerMatcher.ts b/src/matching/matchers/triggerMatcher.ts
index 1b7d88c..7271315 100644
--- a/src/matching/matchers/triggerMatcher.ts
+++ b/src/matching/matchers/triggerMatcher.ts
@@ -1,6 +1,6 @@
import type { MatchContext, Matcher } from '../../types';
import { TRIGGER_LINE_REGEX } from "../../enum/regex";
-import { CATEGORY, SKIP, TRIGGER } from "../matchType";
+import { CATEGORY, TRIGGER, TYPE } from "../matchType";
import { runescriptTrigger } from "../../resource/triggers";
import { reference, declaration, addExtraData } from "../../utils/matchUtils";
@@ -12,20 +12,35 @@ function triggerMatcherFn(context: MatchContext): void {
return undefined;
}
if (TRIGGER_LINE_REGEX.test(context.line.text)) {
- if (context.word.index > 1) return reference(SKIP, context);
- 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.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);
}
- if (context.word.value.charAt(0) === '_') {
- addExtraData(context, { matchId: trigger.match.id, categoryName: context.word.value.substring(1) })
- return reference(CATEGORY, 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);
}
- return trigger.declaration ? declaration(trigger.match, context) : reference(trigger.match, context);
}
}
}
-export const triggerMatcher: Matcher = { priority: 9000, fn: triggerMatcherFn };
+export const triggerMatcher: Matcher = { priority: 7500, fn: triggerMatcherFn };
diff --git a/src/matching/matchingEngine.ts b/src/matching/matchingEngine.ts
index a1a745b..bde967c 100644
--- a/src/matching/matchingEngine.ts
+++ b/src/matching/matchingEngine.ts
@@ -1,7 +1,7 @@
import type { Uri } from 'vscode';
-import type { MatchContext, MatchResult, ParsedWord } from '../types';
-import { CATEGORY, COMPONENT, DBCOLUMN, DBROW, DBTABLE, MODEL, OBJ, SKIP, UNKNOWN } from './matchType';
-import { buildMatchContext } from '../utils/matchUtils';
+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';
@@ -14,10 +14,12 @@ import { switchCaseMatcher } from './matchers/switchCaseMatcher';
import { parametersMatcher } from './matchers/parametersMatcher';
import { configDeclarationMatcher } from './matchers/configDeclarationMatcher';
import { getFileInfo } from '../utils/fileUtils';
-import { getBlockScopeIdentifier, processMatch } from '../cache/activeFileCache';
+import { getBlockScopeIdentifier, processMatch as addToActiveFileCache } from '../cache/activeFileCache';
import { columnDeclarationMatcher } from './matchers/columnDeclarationMatcher';
-import { put, putReference } from '../cache/identifierCache';
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',
@@ -54,6 +56,8 @@ const engines = {
triggerMatcher,
switchCaseMatcher,
parametersMatcher,
+ keywordTypeMatcher,
+ booleanMatcher,
].slice().sort((a, b) => a.priority - b.priority),
}
} as const
@@ -67,12 +71,12 @@ const engines = {
* @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: Map, lines: string[], declarationsOnly = false, engineOverride?: Engine): MatchResult[] {
+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, ([lineNum, parsedWords]) => ({ lineNum, parsedWords }));
+ 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) {
@@ -89,22 +93,18 @@ export function matchFile(uri: Uri, parsedFile: Map, lines
// Iterate thru each line
for (const { lineNum, parsedWords } of parsedLines) {
const lineText = lines[lineNum];
- // Iterate thru each word on the line
+ // 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) {
- processMatch(match);
- fileMatches.push(match);
- if (!match.context.matchType.cache) continue;
- if (match.context.declaration) {
- const startIndex = Math.max(lineNum - 1, 0);
- put(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);
- }
- }
+ 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;
@@ -136,9 +136,13 @@ export function singleWordMatch(uri: Uri, parsedLineWords: ParsedWord[], lineTex
* @returns A matchResult if a match is made, undefined otherwise
*/
function matchWord(context: MatchContext, engine: Engine, declarationsOnly = false): MatchResult | undefined {
- if (!context.word || context.word.value === 'null') {
+ 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);
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
index 4be04d4..6c4cc2b 100644
--- a/src/parsing/fileParser.ts
+++ b/src/parsing/fileParser.ts
@@ -1,6 +1,6 @@
import { type TextDocument, type TextDocumentContentChangeEvent, type Uri } from "vscode";
-import type { ParsedWord } from "../types";
-import { applyLineChanges, getAllWords, parseLine, resetLineParser } from "./lineParser";
+import type { ParsedFile } from "../types";
+import { applyLineChanges, getParsedFile, parseLine, resetLineParser } from "./lineParser";
import { getFileText } from "../utils/fileUtils";
import { logFileParsed } from "../core/devMode";
@@ -11,7 +11,7 @@ import { logFileParsed } from "../core/devMode";
* @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): Map {
+export function parseFile(uri: Uri, fileText: string[], quiet = false): ParsedFile {
const startTime = performance.now();
const parsedWords = [];
resetLineParser(uri);
@@ -19,9 +19,9 @@ export function parseFile(uri: Uri, fileText: string[], quiet = false): Map | undefined {
+export function reparseFileWithChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[], quiet = false): ParsedFile | undefined {
if (changes.length === 0) return undefined;
const startTime = performance.now();
let linesAffected = 0;
@@ -41,7 +41,7 @@ export function reparseFileWithChanges(document: TextDocument, changes: TextDocu
const lineDelta = addedLines - removedLines;
linesAffected = applyLineChanges(document, startLine, endLine, lineDelta);
}
- const parsedFile = getAllWords();
- if (!quiet) logFileParsed(startTime, parsedFile, document.uri, linesAffected);
- return parsedFile;
+ const parsedFile = getParsedFile();
+ if (!quiet) logFileParsed(startTime, document.uri, linesAffected);
+ return { parsedWords: new Map(parsedFile.parsedWords), operatorTokens: new Map(parsedFile.operatorTokens) };
}
diff --git a/src/parsing/lineParser.ts b/src/parsing/lineParser.ts
index 2ed5eea..2d703e8 100644
--- a/src/parsing/lineParser.ts
+++ b/src/parsing/lineParser.ts
@@ -1,5 +1,5 @@
import type { TextDocument, Uri } from 'vscode';
-import type { ParsedWord } from '../types';
+import type { OperatorToken, ParsedFile, ParsedWord } from '../types';
import { resolveFileKey } from '../utils/cacheUtils';
import { getFileInfo } from '../utils/fileUtils';
import { matchLongestException } from './wordExceptions';
@@ -23,6 +23,7 @@ type ParserState = {
type LineParseStateResult = {
words: ParsedWord[];
+ operators: OperatorToken[];
stringRanges: Range[];
blockCommentRanges: Range[];
nextState: ParserState;
@@ -31,6 +32,7 @@ type LineParseStateResult = {
const stringRangesByLine = new Map();
const blockCommentRangesByLine = new Map();
const wordsByLine = new Map();
+const operatorsByLine = new Map();
const endStateByLine = new Map();
const state: ParserState = {
fileKey: undefined,
@@ -83,6 +85,7 @@ export function resetLineParser(uri?: Uri): void {
stringRangesByLine.clear();
blockCommentRangesByLine.clear();
wordsByLine.clear();
+ operatorsByLine.clear();
endStateByLine.clear();
}
@@ -104,6 +107,18 @@ export function getAllWords(): Map {
return wordsByLine;
}
+export function getParsedFile(): ParsedFile {
+ return { parsedWords: getAllWords(), operatorTokens: getAllOperators() };
+}
+
+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);
}
@@ -118,11 +133,13 @@ export function applyLineChanges(document: TextDocument, startLine: number, endL
shiftLineMap(wordsByLine, startLine, endLine, lineDelta);
shiftLineMap(stringRangesByLine, 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);
blockCommentRangesByLine.delete(startLine);
+ operatorsByLine.delete(startLine);
endStateByLine.delete(startLine);
}
@@ -177,6 +194,8 @@ export function parseLine(lineText: string, lineNum: number, uri: Uri): ParsedWo
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;
}
@@ -200,6 +219,8 @@ export function parseLineFromCache(lineText: string, lineNum: number, uri: Uri):
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;
}
@@ -263,9 +284,10 @@ export function getCallStateAtPosition(lineText: string, lineNum: number, uri: U
function parseLineWithState(lineText: string, _lineNum: number, startState: ParserState): LineParseStateResult {
if (lineText.startsWith("text=") || lineText.startsWith("activetext=")) {
- return { words: [], stringRanges: [], blockCommentRanges: [], nextState: cloneParserState(startState) };
+ return { words: [], operators: [], stringRanges: [], blockCommentRanges: [], nextState: cloneParserState(startState) };
}
const words: ParsedWord[] = [];
+ const operators: OperatorToken[] = [];
const stringRanges: Range[] = [];
const blockCommentRanges: Range[] = [];
const nextState = cloneParserState(startState);
@@ -333,6 +355,9 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
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]!;
@@ -418,6 +443,54 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
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) {
@@ -532,7 +605,7 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
nextState.parenDepth = parenDepth;
nextState.braceDepth = braceDepth;
- return { words, stringRanges, blockCommentRanges, nextState };
+ return { words, operators, stringRanges, blockCommentRanges, nextState };
}
function statesEqual(a: ParserState, b: ParserState): boolean {
diff --git a/src/parsing/operators.ts b/src/parsing/operators.ts
new file mode 100644
index 0000000..a5e9099
--- /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/provider/gotoDefinitionProvider.ts b/src/provider/gotoDefinitionProvider.ts
index 36dc587..ecc681d 100644
--- a/src/provider/gotoDefinitionProvider.ts
+++ b/src/provider/gotoDefinitionProvider.ts
@@ -5,9 +5,9 @@ import { decodeReferenceToLocation } from '../utils/cacheUtils';
export const gotoDefinitionProvider: DefinitionProvider = {
async provideDefinition(document: TextDocument, position: Position): Promise {
- // Get the item from the active document cache, exit early if noop or hoverOnly type
- const item = await getByDocPosition(document, position);
- if (!item || item.context.matchType.noop || item.context.matchType.hoverOnly) {
+ // 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;
}
diff --git a/src/provider/hoverProvider.ts b/src/provider/hoverProvider.ts
index 1f68e2d..d159aec 100644
--- a/src/provider/hoverProvider.ts
+++ b/src/provider/hoverProvider.ts
@@ -3,9 +3,9 @@ import type { Item } from '../types';
import { Hover } from 'vscode';
import { buildFromDeclaration } from '../resource/identifierFactory';
import { getDeclarationHoverItems, getReferenceHoverItems } from '../resource/hoverConfigResolver';
-import { markdownBase, appendTitle, appendInfo, appendValue, appendSignature, appendCodeBlock, appendDebugHover } from '../utils/markdownUtils';
+import { markdownBase, appendTitle, appendInfo, appendValue, appendSignature, appendCodeBlock, appendDebugHover, appendOperatorHover } from '../utils/markdownUtils';
import { getFileDiagnostics } from '../core/diagnostics';
-import { getByDocPosition, getParsedWordByDocPosition } from '../cache/activeFileCache';
+import { getByDocPosition, getOperatorByDocPosition, getParsedWordByDocPosition } from '../cache/activeFileCache';
import { isDevMode } from '../core/devMode';
import { getSettingValue, Settings } from '../core/settings';
@@ -14,7 +14,7 @@ export const hoverProvider = function(context: ExtensionContext): HoverProvider
async provideHover(document: TextDocument, position: Position): Promise {
if (!getSettingValue(Settings.ShowHover)) return undefined; // Exit early if hover disabled
const markdown = markdownBase(context);
- const item = await getByDocPosition(document, position);
+ const item = getByDocPosition(document, position);
appendHover(markdown, document, position, item)
await appendDebug(markdown, document, position, item);
return new Hover(markdown);
@@ -23,7 +23,7 @@ export const hoverProvider = function(context: ExtensionContext): HoverProvider
}
function getIdentifier(item: Item) {
- return item.identifier ?? (item.context.matchType.hoverOnly ? buildFromDeclaration(item.word, item.context) : undefined);
+ return item.identifier ?? (!item.context.matchType.cache ? buildFromDeclaration(item.word, item.context) : undefined);
}
function appendHover(markdown: MarkdownString, document: TextDocument, position: Position, item: Item | undefined): void {
@@ -60,12 +60,10 @@ function appendHover(markdown: MarkdownString, document: TextDocument, position:
async function appendDebug(markdown: MarkdownString, document: TextDocument, position: Position, item: Item | undefined): Promise {
if (isDevMode()) {
- if (!item) {
- const parsedWord = await getParsedWordByDocPosition(document, position);
- if (!parsedWord) return;
- appendDebugHover(markdown, parsedWord);
- } else {
- appendDebugHover(markdown, item.context.word, item.context, getIdentifier(item));
- }
+ 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);
}
}
diff --git a/src/provider/referenceProvider.ts b/src/provider/referenceProvider.ts
index 8a9e004..54eb0e9 100644
--- a/src/provider/referenceProvider.ts
+++ b/src/provider/referenceProvider.ts
@@ -5,9 +5,9 @@ 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 hoverOnly type
- const item = await getByDocPosition(document, position);
- if (!item || item.context.matchType.noop || item.context.matchType.hoverOnly) {
+ // 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 [];
}
diff --git a/src/provider/renameProvider.ts b/src/provider/renameProvider.ts
index 53aa1ad..3418036 100644
--- a/src/provider/renameProvider.ts
+++ b/src/provider/renameProvider.ts
@@ -8,7 +8,7 @@ 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 = await getByDocPosition(document, position);
+ const item = getByDocPosition(document, position);
if (!item) {
throw new Error("Cannot rename");
}
@@ -27,7 +27,7 @@ export const renameProvider: RenameProvider = {
async provideRenameEdits(document: TextDocument, position: Position, newName: string): Promise {
// Get the item from the active document cache
- const item = await getByDocPosition(document, position);
+ const item = getByDocPosition(document, position);
if (!item) {
return undefined;
}
diff --git a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
index 9c439f2..8f75ff7 100644
--- a/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
+++ b/src/provider/signatureHelp/runescriptSignatureHelpProvider.ts
@@ -84,7 +84,7 @@ async function getParametersHelp(document: TextDocument, position: Position): Pr
}
// Retrieve the call identifier from the active file cache to access its signature
- const identifier = getCallIdentifier(position.line, callState.callName, callState.callNameIndex);
+ const identifier = getCallIdentifier(document.uri, position.line, callState.callName, callState.callNameIndex);
if (!identifier?.signature) {
return undefined;
}
diff --git a/src/resource/identifierFactory.ts b/src/resource/identifierFactory.ts
index d21ad76..635be8b 100644
--- a/src/resource/identifierFactory.ts
+++ b/src/resource/identifierFactory.ts
@@ -1,9 +1,23 @@
-import type { Identifier, IdentifierText, MatchContext, MatchType } from '../types';
+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';
+
+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 = {
@@ -13,7 +27,7 @@ export function buildFromDeclaration(name: string, context: MatchContext, text?:
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)
+ language: getHoverLanguage(context.matchType),
};
process(identifier, context, text);
return identifier;
@@ -42,6 +56,9 @@ export function addReference(identifier: Identifier, fileKey: string, lineNum: n
}
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) {
diff --git a/src/resource/postProcessors.ts b/src/resource/postProcessors.ts
index 4a006cf..a7b5183 100644
--- a/src/resource/postProcessors.ts
+++ b/src/resource/postProcessors.ts
@@ -20,18 +20,25 @@ export const enumPostProcessor: PostProcessor = function(identifier) {
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 dataTypePostProcessor: PostProcessor = function(identifier) {
+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) {
diff --git a/src/types.ts b/src/types.ts
index 21c25f9..07baf6b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,6 +3,16 @@ 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;
+}
+
/**
* Definition of a parsed word
*/
@@ -33,6 +43,16 @@ export interface ParsedWord {
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 }
/**
@@ -123,6 +143,8 @@ export interface 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;
}
/**
@@ -177,12 +199,12 @@ export interface MatchType {
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 for hover display only (not cached) */
- hoverOnly?: 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;
}
@@ -286,4 +308,4 @@ export interface ConfigKeyData {
/** The match type id of the identifier where teh varag param types are defined */
idenType: string
}
-}
\ No newline at end of file
+}
diff --git a/src/utils/markdownUtils.ts b/src/utils/markdownUtils.ts
index a4a795f..1b923f5 100644
--- a/src/utils/markdownUtils.ts
+++ b/src/utils/markdownUtils.ts
@@ -1,5 +1,5 @@
import type { ExtensionContext } from 'vscode';
-import type { Identifier, MatchContext, ParsedWord } from '../types';
+import type { Identifier, MatchContext, OperatorToken, ParsedWord } from '../types';
import { MarkdownString, Uri } from 'vscode';
import { join, sep } from 'path';
import { INFO, VALUE, SIGNATURE, CODEBLOCK } from '../enum/hoverDisplayItems';
@@ -55,65 +55,78 @@ export function appendBody(text: string, markdown: MarkdownString): void {
}
export function appendDebugHover(markdown: MarkdownString, word: ParsedWord, context?: MatchContext, identifier?: Identifier): void {
- const debugLines: string[] = [];
- debugLines.push(`matchType: ${context ? context.matchType.id : 'n/a'}`);
if (markdown.value) markdown.appendMarkdown('\n\n---\n\n');
- markdown.appendMarkdown(`**DEBUG ${word.value}**`);
- markdown.appendCodeblock(debugLines.join('\n'), 'text');
+ 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}`);
+ 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)}`);
+ parsingInfoLines.push(`extraData=${JSON.stringify(context.extraData)}`);
}
markdown.appendMarkdown(`\n\n---\n\n**Parsing Info**`);
- markdown.appendCodeblock(parsingInfoLines.join('\n'), 'text');
+ 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}`);
+ 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(`configKey=${word.configKey}`);
+ callInfoLines.push(`configKeyWordIndex=${word.callNameIndex}`);
}
- callInfoLines.push(`paramIndex: ${word.paramIndex}`);
+ callInfoLines.push(`paramIndex=${word.paramIndex}`);
markdown.appendMarkdown(`\n\n---\n\n**Parent Function**`);
- markdown.appendCodeblock(callInfoLines.join('\n'), 'text');
+ 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}`);
+ 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'), 'text');
+ markdown.appendCodeblock(modifiedWordLines.join('\n'), 'properties');
}
if (identifier) {
const identifierLines: string[] = [];
- if (identifier.id) identifierLines.push(`packId: ${identifier.id}`);
- identifierLines.push(`cacheId: ${word.value}${identifier.matchId}`);
+ 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 fileInfo = getFileInfo(identifier.declaration.uri);
const location = decodeReferenceToLocation(identifier.declaration.uri, identifier.declaration.ref);
const line = location ? location.range.start.line + 1 : 'n/a';
- identifierLines.push(`declaration: ${fileInfo.name}.${fileInfo.type}, line ${line}`);
+ identifierLines.push(`declaration=${fileInfo.name}.${fileInfo.type}, 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.hideDisplay) identifierLines.push(`hideDisplay: true`);
+ 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'), 'text');
+ 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');
+}
diff --git a/syntaxes/interface.tmLanguage.json b/syntaxes/interface.tmLanguage.json
index 8fdaa1c..e58b804 100644
--- a/syntaxes/interface.tmLanguage.json
+++ b/syntaxes/interface.tmLanguage.json
@@ -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 9cf35c6..2c7917d 100644
--- a/syntaxes/runescript.tmLanguage.json
+++ b/syntaxes/runescript.tmLanguage.json
@@ -106,12 +106,7 @@
{
"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 "
+ "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",
From 48464effd54c23c062dd3e637faeecc626ac695b Mon Sep 17 00:00:00 2001
From: klymp <14829626+kylmp@users.noreply.github.com>
Date: Fri, 30 Jan 2026 10:41:39 +0000
Subject: [PATCH 7/8] feat: add map helper info
---
src/cache/activeFileCache.ts | 56 +++-
src/cache/idCache.ts | 28 ++
src/core/devMode.ts | 53 +--
src/core/diagnostics.ts | 43 ++-
src/core/eventHandlers.ts | 39 ++-
src/core/highlights.ts | 48 ++-
src/core/manager.ts | 40 ++-
src/core/mapManager.ts | 316 ++++++++++++++++++
src/core/providers.ts | 6 +
src/diagnostics/unknownFileDiagnostic.ts | 41 ++-
.../unknownIdentifierDiagnostic.ts | 2 +-
src/matching/matchType.ts | 2 +-
src/parsing/fileParser.ts | 16 +-
src/parsing/lineParser.ts | 33 +-
src/parsing/mapParser.ts | 116 +++++++
src/parsing/operators.ts | 4 +-
src/provider/gotoDefinitionProvider.ts | 14 +-
src/provider/hoverProvider.ts | 58 +++-
src/resource/identifierFactory.ts | 6 +-
src/runescriptExtension.ts | 16 -
src/types.ts | 9 +
src/utils/fileUtils.ts | 7 +-
src/utils/markdownUtils.ts | 16 +-
23 files changed, 876 insertions(+), 93 deletions(-)
create mode 100644 src/cache/idCache.ts
create mode 100644 src/core/mapManager.ts
create mode 100644 src/parsing/mapParser.ts
diff --git a/src/cache/activeFileCache.ts b/src/cache/activeFileCache.ts
index ec1e58c..cbcb3e2 100644
--- a/src/cache/activeFileCache.ts
+++ b/src/cache/activeFileCache.ts
@@ -1,5 +1,5 @@
import type { Position, TextDocument, Uri } from "vscode";
-import type { DataRange, Identifier, IdentifierText, Item, MatchResult, MatchType, OperatorToken, ParsedFile, ParsedWord } from "../types";
+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";
@@ -38,6 +38,18 @@ let parsedWords: Map = new Map();
*/
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 ===== //
/**
@@ -87,6 +99,30 @@ export function getOperatorByDocPosition(position: Position): OperatorToken | un
}
}
+/**
+ * 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)
@@ -176,6 +212,20 @@ 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 ==== //
/**
@@ -186,6 +236,8 @@ 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();
@@ -200,6 +252,8 @@ export function clear() {
fileMatches.clear();
parsedWords = new Map();
operatorTokens = new Map();
+ stringRanges = new Map();
+ interpolationRanges = new Map();
localVarCache.clear();
codeBlockCache.clear();
switchStmtCache.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/core/devMode.ts b/src/core/devMode.ts
index 6b08aba..599889c 100644
--- a/src/core/devMode.ts
+++ b/src/core/devMode.ts
@@ -7,7 +7,22 @@ import { getSettingValue, Settings } from "./settings";
import { appriximateSize, getCacheKeyCount, getTotalReferences } from "../cache/identifierCache";
import { getTypesCount } from "../cache/completionCache";
import { getExceptionWords } from "../parsing/wordExceptions";
-import { getFileInfo } from "../utils/fileUtils";
+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,
@@ -144,33 +159,19 @@ function formatMs2(ms: number) {
return `${ms.toFixed(2)} ms`;
}
-export enum LogType {
- 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',
-}
-
-export function logFileEvent(uri: Uri, event: LogType, extra?: string) {
+export function logFileEvent(uri: Uri, event: Events, extra?: string) {
const resolver = () => {
- const fileInfo = getFileInfo(uri);
- return `on file ${fileInfo.name}.${fileInfo.type}${extra ? ` [${extra}]` : ''}`;
+ return `on file ${getFileName(uri)}${extra ? ` [${extra}]` : ''}`;
}
logEvent(event, resolver);
}
export function logSettingsEvent(setting: Settings) {
const resolver = () => `setting ${setting} updated to ${getSettingValue(setting)}`;
- logEvent(LogType.SettingsChanged, resolver);
+ logEvent(Events.SettingsChanged, resolver);
}
-export function logEvent(event: LogType, msgResolver: () => string) {
+export function logEvent(event: Events, msgResolver: () => string) {
const resolver = () => {
const msg = msgResolver();
return `Event [${event}]${msg ? ' ' + msg : ''}`
@@ -180,17 +181,23 @@ export function logEvent(event: LogType, msgResolver: () => string) {
export function logFileParsed(startTime: number, uri: Uri, lines: number, partial = false) {
const resolver = () => {
- const fileInfo = getFileInfo(uri);
const msg = partial ? 'Partial reparse of file' : 'Parsed file';
- return `${msg} ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [lines parsed: ${lines}]`;
+ 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 = () => {
- const fileInfo = getFileInfo(uri);
- return `Rebuilt file ${fileInfo.name}.${fileInfo.type} in ${formatMs2(performance.now() - startTime)} [matches: ${matches.length}]`;
+ return `Rebuilt file ${getFileName(uri)} in ${formatMs2(performance.now() - startTime)} [matches: ${matches.length}]`;
}
log(resolver, LogLevel.Debug);
}
diff --git a/src/core/diagnostics.ts b/src/core/diagnostics.ts
index 45a15fa..c82460b 100644
--- a/src/core/diagnostics.ts
+++ b/src/core/diagnostics.ts
@@ -5,8 +5,10 @@ import { languages, Range, Uri } from 'vscode';
import { getSettingValue, Settings } from './settings';
import { UnknownIdentifierDiagnostic } from '../diagnostics/unknownIdentifierDiagnostic';
import { UnknownFileDiagnostic } from '../diagnostics/unknownFileDiagnostic';
-import { getByKey } from '../cache/identifierCache';
+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;
@@ -43,6 +45,11 @@ 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[] = [];
@@ -115,6 +122,40 @@ export function handleFileUpdate(before?: FileIdentifiers, after?: FileIdentifie
}
}
+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) ?? [];
diff --git a/src/core/eventHandlers.ts b/src/core/eventHandlers.ts
index 46d7c7c..45fdfcc 100644
--- a/src/core/eventHandlers.ts
+++ b/src/core/eventHandlers.ts
@@ -1,21 +1,21 @@
import type { ConfigurationChangeEvent, ExtensionContext, TextDocument, TextDocumentChangeEvent, TextDocumentContentChangeEvent, TextEditor, Uri } from "vscode";
import { window, workspace } from "vscode";
-import { clearAllDiagnostics, handleFileUpdate } from "./diagnostics";
+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, LogType } from "./devMode";
+import { clearDevModeOutput, initDevMode, logEvent, logFileEvent, logSettingsEvent, Events } from "./devMode";
import { getLines } from "../utils/stringUtils";
-import { clearFile, processAllFiles, queueFileRebuild } from "./manager";
-import { monitoredFileTypes } from "../runescriptExtension";
+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(monitoredFileTypes, ext => `**/*.${ext}`);
+ const patterns = Array.from(allFileTypes, ext => `**/*.${ext}`);
const fileWatcher = workspace.createFileSystemWatcher(`{${patterns.join(',')}}`);
const gitBranchWatcher = workspace.createFileSystemWatcher('**/.git/HEAD');
gitBranchWatcher.onDidCreate(onGitBranchChange);
@@ -43,7 +43,9 @@ 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) || !isValidFile(textChangeEvent.document.uri)) return;
+ 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);
@@ -57,7 +59,7 @@ function onActiveFileTextChange(textChangeEvent: TextDocumentChangeEvent): void
pendingTimer = setTimeout(() => {
const doc = pendingDocument;
if (!doc) return;
- logFileEvent(doc.uri, LogType.ActiveFileTextChanged, `partial reparse`);
+ logFileEvent(doc.uri, Events.ActiveFileTextChanged, `partial reparse`);
const changes = pendingChanges;
pendingChanges = [];
pendingTimer = undefined;
@@ -90,35 +92,42 @@ export function waitForActiveFileRebuild(document: TextDocument, version = docum
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, LogType.ActiveFileChanged, 'full reparse');
+ 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;
- logFileEvent(uri, LogType.FileDeleted, 'relevant cache entries invalidated');
- handleFileUpdate(getFileIdentifiers(uri), undefined);
+ handleFileUpdateDiagnostics(getFileIdentifiers(uri), undefined);
clearFile(uri);
- removeUris([uri]);
}
function onCreateFile(uri: Uri) {
+ logFileEvent(uri, Events.FileCreated, 'full parse');
+ handleFileCreatedDiagnostics(uri);
+ addUris([uri]);
if (!isValidFile(uri)) return;
- logFileEvent(uri, LogType.FileCreated, 'full parse');
void updateFileFromUri(uri);
- addUris([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, LogType.FileChanged, 'full reparse');
+ logFileEvent(uri, Events.FileChanged, 'full reparse');
void updateFileFromUri(uri);
}
function onGitBranchChange() {
- logEvent(LogType.GitBranchChanged, () => 'full cache rebuild');
+ logEvent(Events.GitBranchChanged, () => 'full cache rebuild');
processAllFiles();
}
diff --git a/src/core/highlights.ts b/src/core/highlights.ts
index 1826fac..3f6a199 100644
--- a/src/core/highlights.ts
+++ b/src/core/highlights.ts
@@ -1,6 +1,6 @@
import { Position, Range, type TextEditor } from 'vscode';
import { DecorationRangeBehavior, window } from "vscode";
-import { getAllMatches, getAllOperatorTokens, getAllParsedWords } from '../cache/activeFileCache';
+import { getAllInterpolationRanges, getAllMatches, getAllOperatorTokens, getAllParsedWords, getAllStringRanges } from '../cache/activeFileCache';
import { isDevMode } from './devMode';
const matchDecoration = window.createTextEditorDecorationType({
@@ -18,6 +18,11 @@ const operatorDecoration = window.createTextEditorDecorationType({
rangeBehavior: DecorationRangeBehavior.ClosedClosed
});
+const stringDecoration = window.createTextEditorDecorationType({
+ backgroundColor: 'rgba(255, 170, 60, 0.20)',
+ rangeBehavior: DecorationRangeBehavior.ClosedClosed
+});
+
enum HighlightMode {
Disabled = 'disabled',
Matches = 'matches',
@@ -35,6 +40,8 @@ export function rebuildHighlights(): void {
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());
@@ -43,6 +50,7 @@ function buildHighlights(editor: TextEditor, mode = HighlightMode.AllWords) {
editor.setDecorations(matchDecoration, getMatchRanges());
editor.setDecorations(wordDecoration, getWordRanges());
editor.setDecorations(operatorDecoration, getOperatorTokenRanges());
+ editor.setDecorations(stringDecoration, getStringRanges());
break;
}
}
@@ -67,3 +75,41 @@ function getOperatorTokenRanges(): Range[] {
});
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
index a1ea4b6..3bbeda8 100644
--- a/src/core/manager.ts
+++ b/src/core/manager.ts
@@ -13,11 +13,16 @@ import { registerCommands } from "./commands";
import { registerEventHandlers } from "./eventHandlers";
import { registerProviders } from "./providers";
import { parseFile } from "../parsing/fileParser";
-import { monitoredFileTypes } from "../runescriptExtension";
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);
@@ -41,6 +46,7 @@ export function processAllFiles() {
cancellable: false
}, async () => {
clearAll();
+ await rebuildProjectFilesCache();
await rebuildAllFiles();
void rebuildActiveFile();
reportRebuildMetrics();
@@ -130,6 +136,7 @@ async function rebuildFile(uri: Uri, lines: string[], parsedFile: ParsedFile, qu
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));
}
@@ -142,6 +149,8 @@ export function clearAll() {
clearIdentifierCache();
clearAllDiagnostics();
clearActiveFileCache();
+ clearAllIds();
+ clearMap();
}
/**
@@ -159,4 +168,33 @@ function dispose() {
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..ee43e17
--- /dev/null
+++ b/src/core/mapManager.ts
@@ -0,0 +1,316 @@
+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 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 || !entry.idRange.contains(position)) return undefined;
+ const type = entry.kind.toUpperCase();
+ const name = getIdName(type, String(entry.id));
+ if (!name) return undefined;
+ return getIdentifierByKey(name + type);
+}
+
+
+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();
+ 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));
+ }
+}
+
+function replaceRange(startLine: number, endLine: number, result: MapParseResult) {
+ for (let line = startLine; line <= endLine; line++) {
+ entriesByLine.delete(line);
+ errorsByLine.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
index e64bef0..b10df55 100644
--- a/src/core/providers.ts
+++ b/src/core/providers.ts
@@ -20,6 +20,12 @@ export function registerProviders(context: ExtensionContext) {
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));
}
function registerUniversalProviders(language: string, context: ExtensionContext): void {
diff --git a/src/diagnostics/unknownFileDiagnostic.ts b/src/diagnostics/unknownFileDiagnostic.ts
index d0a532c..9e7c863 100644
--- a/src/diagnostics/unknownFileDiagnostic.ts
+++ b/src/diagnostics/unknownFileDiagnostic.ts
@@ -6,15 +6,50 @@ 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 = `${result.word}.${(result.context.matchType.fileTypes || [])[0] ?? 'rs2'}`;
+ this.fileName = resultToFileKey(result);
return !projectFileExists(this.fileName)
}
- createDiagnostic(range: Range): Diagnostic {
- return new Diagnostic(range, `Refers to file ${this.fileName}, but it doesn't exist`, DiagnosticSeverity.Warning);
+ 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
index a3c75a0..cab26f6 100644
--- a/src/diagnostics/unknownIdentifierDiagnostic.ts
+++ b/src/diagnostics/unknownIdentifierDiagnostic.ts
@@ -6,7 +6,7 @@ import { getFullName, resolveIdentifierKey } from "../utils/cacheUtils";
export class UnknownIdentifierDiagnostic extends RunescriptDiagnostic {
/**
- * Cache the diagnostics by identifierKey, value is a map keyed by URI anda. range of references in that URI
+ * Cache the diagnostics by identifierKey, value is a map keyed by URI and range of references in that URI
*/
cache: Map> = new Map();
diff --git a/src/matching/matchType.ts b/src/matching/matchType.ts
index f6c7139..c797a44 100644
--- a/src/matching/matchType.ts
+++ b/src/matching/matchType.ts
@@ -236,7 +236,7 @@ export const SKIP: MatchType = defineMatchType({ id: 'SKIP', types: [], fileType
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 });
+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 {
diff --git a/src/parsing/fileParser.ts b/src/parsing/fileParser.ts
index 6c4cc2b..3ca9059 100644
--- a/src/parsing/fileParser.ts
+++ b/src/parsing/fileParser.ts
@@ -19,9 +19,8 @@ export function parseFile(uri: Uri, fileText: string[], quiet = false): ParsedFi
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
parsedWords.push(...parseLine(lines[lineNum], lineNum, uri));
}
- const parsedFile = getParsedFile();
if (!quiet) logFileParsed(startTime, uri, lines.length);
- return { parsedWords: new Map(parsedFile.parsedWords), operatorTokens: new Map(parsedFile.operatorTokens) };
+ return cloneParsedFile();
}
/**
@@ -41,7 +40,16 @@ export function reparseFileWithChanges(document: TextDocument, changes: TextDocu
const lineDelta = addedLines - removedLines;
linesAffected = applyLineChanges(document, startLine, endLine, lineDelta);
}
- const parsedFile = getParsedFile();
if (!quiet) logFileParsed(startTime, document.uri, linesAffected);
- return { parsedWords: new Map(parsedFile.parsedWords), operatorTokens: new Map(parsedFile.operatorTokens) };
+ 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
index 2d703e8..e2f5d2c 100644
--- a/src/parsing/lineParser.ts
+++ b/src/parsing/lineParser.ts
@@ -25,11 +25,13 @@ 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();
@@ -83,6 +85,7 @@ export function resetLineParser(uri?: Uri): void {
state.paramIndexStack = resetState.paramIndexStack;
state.interpParenDepthStack = resetState.interpParenDepthStack;
stringRangesByLine.clear();
+ interpRangesByLine.clear();
blockCommentRangesByLine.clear();
wordsByLine.clear();
operatorsByLine.clear();
@@ -94,6 +97,14 @@ export function getStringRanges(lineNum?: number): Range[] | 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) ?? [];
@@ -108,7 +119,12 @@ export function getAllWords(): Map {
}
export function getParsedFile(): ParsedFile {
- return { parsedWords: getAllWords(), operatorTokens: getAllOperators() };
+ return {
+ parsedWords: getAllWords(),
+ operatorTokens: getAllOperators(),
+ stringRanges: getAllStringRanges(),
+ interpolationRanges: getAllInterpolationRanges()
+ };
}
export function getLineOperators(lineNum: number): OperatorToken[] {
@@ -132,12 +148,14 @@ export function applyLineChanges(document: TextDocument, startLine: number, endL
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);
@@ -190,6 +208,8 @@ export function parseLine(lineText: string, lineNum: number, uri: Uri): ParsedWo
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);
@@ -215,6 +235,8 @@ export function parseLineFromCache(lineText: string, lineNum: number, uri: Uri):
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);
@@ -284,11 +306,12 @@ export function getCallStateAtPosition(lineText: string, lineNum: number, uri: U
function parseLineWithState(lineText: string, _lineNum: number, startState: ParserState): LineParseStateResult {
if (lineText.startsWith("text=") || lineText.startsWith("activetext=")) {
- return { words: [], operators: [], stringRanges: [], blockCommentRanges: [], nextState: cloneParserState(startState) };
+ 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);
@@ -315,6 +338,7 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
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) => {
@@ -415,12 +439,15 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
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;
@@ -605,7 +632,7 @@ function parseLineWithState(lineText: string, _lineNum: number, startState: Pars
nextState.parenDepth = parenDepth;
nextState.braceDepth = braceDepth;
- return { words, operators, stringRanges, blockCommentRanges, nextState };
+ return { words, operators, stringRanges, interpRanges, blockCommentRanges, nextState };
}
function statesEqual(a: ParserState, b: ParserState): boolean {
diff --git a/src/parsing/mapParser.ts b/src/parsing/mapParser.ts
new file mode 100644
index 0000000..ad28510
--- /dev/null
+++ b/src/parsing/mapParser.ts
@@ -0,0 +1,116 @@
+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[];
+};
+
+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[] = [];
+ 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();
+ 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 };
+}
+
+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
index a5e9099..532e835 100644
--- a/src/parsing/operators.ts
+++ b/src/parsing/operators.ts
@@ -6,5 +6,5 @@ export const operators = new Set([
'>',
'!',
'&',
- '|']
-);
\ No newline at end of file
+ '|'
+]);
\ No newline at end of file
diff --git a/src/provider/gotoDefinitionProvider.ts b/src/provider/gotoDefinitionProvider.ts
index ecc681d..33543d0 100644
--- a/src/provider/gotoDefinitionProvider.ts
+++ b/src/provider/gotoDefinitionProvider.ts
@@ -2,9 +2,15 @@ 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) {
@@ -18,7 +24,11 @@ export const gotoDefinitionProvider: DefinitionProvider = {
}
// Goto the declaration if the identifier exists
- if (!item.identifier?.declaration) return undefined;
- return decodeReferenceToLocation(item.identifier.declaration.uri, item.identifier.declaration.ref);
+ 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
index d159aec..a264da4 100644
--- a/src/provider/hoverProvider.ts
+++ b/src/provider/hoverProvider.ts
@@ -1,22 +1,29 @@
import type { ExtensionContext, HoverProvider, MarkdownString, Position, TextDocument } from 'vscode';
-import type { Item } from '../types';
+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 } from '../utils/markdownUtils';
+import { markdownBase, appendTitle, appendInfo, appendValue, appendSignature, appendCodeBlock, appendDebugHover, appendOperatorHover, appendStringHover } from '../utils/markdownUtils';
import { getFileDiagnostics } from '../core/diagnostics';
-import { getByDocPosition, getOperatorByDocPosition, getParsedWordByDocPosition } from '../cache/activeFileCache';
+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);
- const item = getByDocPosition(document, position);
- appendHover(markdown, document, position, item)
- await appendDebug(markdown, document, position, item);
+ 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);
}
};
@@ -26,6 +33,16 @@ 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);
@@ -39,8 +56,8 @@ function appendHover(markdown: MarkdownString, document: TextDocument, position:
}
// If no config found, or no items to display then exit early
- const hoverDisplayItems = item.context.declaration ? getDeclarationHoverItems(item.context.matchType) : getReferenceHoverItems(item.context.matchType);
- if (!Array.isArray(hoverDisplayItems) || hoverDisplayItems.length === 0) {
+ const hoverDisplayItems = getHoverItems(item.context.declaration, item.context.matchType);
+ if (hoverDisplayItems.length === 0) {
return undefined;
}
@@ -50,12 +67,24 @@ function appendHover(markdown: MarkdownString, document: TextDocument, position:
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, item.context.cert);
- appendInfo(identifier, hoverDisplayItems, markdown);
- appendValue(identifier, hoverDisplayItems, markdown);
- appendSignature(identifier, hoverDisplayItems, markdown);
- appendCodeBlock(identifier, hoverDisplayItems, markdown);
+ 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 {
@@ -65,5 +94,8 @@ async function appendDebug(markdown: MarkdownString, document: TextDocument, pos
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/resource/identifierFactory.ts b/src/resource/identifierFactory.ts
index 635be8b..6502c8a 100644
--- a/src/resource/identifierFactory.ts
+++ b/src/resource/identifierFactory.ts
@@ -6,6 +6,7 @@ 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;
@@ -51,7 +52,10 @@ export function buildFromReference(name: string, context: MatchContext): Identif
export function addReference(identifier: Identifier, fileKey: string, lineNum: number, startIndex: number, endIndex: number, context?: MatchContext): Set {
const fileReferences = identifier.references[fileKey] || new Set