Enhance refactor commands with controller-aware Route() updates and fix code quality violations

Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 06:21:56 +00:00
parent e678b987c2
commit f6ac36c632
5683 changed files with 5854736 additions and 22329 deletions

View File

@@ -81,7 +81,6 @@ export class RspadeClassRefactorProvider {
if (!new_class_name) {
this.output_channel.appendLine('Refactor cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
@@ -96,7 +95,6 @@ export class RspadeClassRefactorProvider {
if (confirmation !== 'Rename') {
this.output_channel.appendLine('Global rename cancelled by user');
await vscode.commands.executeCommand('workbench.action.closePanel');
return;
}
@@ -144,27 +142,23 @@ export class RspadeClassRefactorProvider {
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files and auto-rename
// Wait for filesystem changes to propagate then reload files and auto-rename
// Note: Panel is kept open so developer can see results and warnings
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
await this.reload_all_open_files();
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
// Wait another 500ms then check if current file needs renaming
setTimeout(async () => {
await this.reload_all_open_files();
// Wait another 500ms then check if current file needs renaming
setTimeout(async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
const file_path = editor.document.uri.fsPath;
// Only auto-rename if file is in ./rsx
if (file_path.includes('/rsx/') || file_path.includes('\\rsx\\')) {
await this.auto_rename_provider.check_and_rename(editor.document);
}
const editor = vscode.window.activeTextEditor;
if (editor) {
const file_path = editor.document.uri.fsPath;
// Only auto-rename if file is in ./rsx
if (file_path.includes('/rsx/') || file_path.includes('\\rsx\\')) {
await this.auto_rename_provider.check_and_rename(editor.document);
}
}, 500);
}
}, 500);
}, 3500);
}, 500);
vscode.window.showInformationMessage(
`Successfully refactored ${class_info.class_name} to ${new_class_name}`

View File

@@ -0,0 +1,118 @@
import * as vscode from 'vscode';
import { JqhtmlLifecycleSemanticTokensProvider } from './jqhtml_lifecycle_provider';
import { CommentFileReferenceSemanticTokensProvider } from './comment_file_reference_provider';
import { ThatVariableSemanticTokensProvider } from './that_variable_provider';
interface DecodedToken {
line: number;
char: number;
length: number;
type: number;
modifiers: number;
}
/**
* Combined semantic tokens provider that merges tokens from multiple providers
*
* VS Code only allows one SemanticTokensLegend per language, so we need to
* combine all our providers into one to avoid conflicts.
*/
export class CombinedSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
private jqhtml_provider: JqhtmlLifecycleSemanticTokensProvider;
private file_ref_provider: CommentFileReferenceSemanticTokensProvider;
private that_provider: ThatVariableSemanticTokensProvider;
constructor() {
this.jqhtml_provider = new JqhtmlLifecycleSemanticTokensProvider();
this.file_ref_provider = new CommentFileReferenceSemanticTokensProvider();
this.that_provider = new ThatVariableSemanticTokensProvider();
}
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
// Get tokens from all providers
const jqhtml_tokens = await this.jqhtml_provider.provideDocumentSemanticTokens(document);
const file_ref_tokens = await this.file_ref_provider.provideDocumentSemanticTokens(document);
const that_tokens = await this.that_provider.provideDocumentSemanticTokens(document);
// Decode all tokens to absolute positions
const decoded_tokens: DecodedToken[] = [];
// Decode JQHTML tokens (type 0 = conventionMethod, orange)
this.decode_tokens(jqhtml_tokens.data, 0, decoded_tokens);
// Decode file reference tokens (type 1 = class, teal)
this.decode_tokens(file_ref_tokens.data, 1, decoded_tokens);
// Decode 'that' tokens (type 2 = macro, dark blue #569CD6 like 'this')
this.decode_tokens(that_tokens.data, 2, decoded_tokens);
// Sort tokens by line, then by character
decoded_tokens.sort((a, b) => {
if (a.line !== b.line) {
return a.line - b.line;
}
return a.char - b.char;
});
// Re-encode tokens with delta encoding
const builder = new vscode.SemanticTokensBuilder();
for (const token of decoded_tokens) {
builder.push(token.line, token.char, token.length, token.type, token.modifiers);
}
return builder.build();
}
private decode_tokens(data: Uint32Array, new_type: number, output: DecodedToken[]): void {
let current_line = 0;
let current_char = 0;
for (let i = 0; i < data.length; i += 5) {
const delta_line = data[i];
const delta_char = data[i + 1];
const length = data[i + 2];
const modifiers = data[i + 4];
if (delta_line > 0) {
current_line += delta_line;
current_char = delta_char;
} else {
current_char += delta_char;
}
output.push({
line: current_line,
char: current_char,
length: length,
type: new_type,
modifiers: modifiers
});
}
}
private decode_tokens_with_modifier(data: Uint32Array, new_type: number, new_modifier: number, output: DecodedToken[]): void {
let current_line = 0;
let current_char = 0;
for (let i = 0; i < data.length; i += 5) {
const delta_line = data[i];
const delta_char = data[i + 1];
const length = data[i + 2];
if (delta_line > 0) {
current_line += delta_line;
current_char = delta_char;
} else {
current_char += delta_char;
}
output.push({
line: current_line,
char: current_char,
length: length,
type: new_type,
modifiers: new_modifier
});
}
}
}

View File

@@ -0,0 +1,175 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
/**
* Check if position is inside a comment
*/
function is_in_comment(document: vscode.TextDocument, position: vscode.Position): boolean {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for block comment markers
const doc_comment_idx = line_text.indexOf('/*');
const doc_comment_end_idx = line_text.indexOf('*/');
const asterisk_comment = line_text.trim().startsWith('*');
if (asterisk_comment || doc_comment_idx !== -1 || doc_comment_end_idx !== -1) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
} else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
} else {
i++;
}
}
return in_block_comment;
}
/**
* Get the word at position, including periods
*/
function get_word_with_period(document: vscode.TextDocument, position: vscode.Position): { word: string, range: vscode.Range } | undefined {
const line = document.lineAt(position.line);
const line_text = line.text;
const char = position.character;
// Find start of word (alphanumeric, underscore, period, hyphen)
let start = char;
while (start > 0 && /[a-zA-Z0-9_.-]/.test(line_text[start - 1])) {
start--;
}
// Find end of word
let end = char;
while (end < line_text.length && /[a-zA-Z0-9_.-]/.test(line_text[end])) {
end++;
}
const word = line_text.substring(start, end);
// Must contain a period and not be just a period
if (!word.includes('.') || word === '.') {
return undefined;
}
const range = new vscode.Range(
new vscode.Position(position.line, start),
new vscode.Position(position.line, end)
);
return { word, range };
}
/**
* Check if a file exists in the same directory as the document
*/
function find_file_in_same_directory(document: vscode.TextDocument, filename: string): string | undefined {
const doc_dir = path.dirname(document.uri.fsPath);
const file_path = path.join(doc_dir, filename);
if (fs.existsSync(file_path)) {
return file_path;
}
return undefined;
}
/**
* Provides semantic tokens for file references in comments (light blue like class properties)
*/
export class CommentFileReferenceSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
// Only for JavaScript/TypeScript files
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return tokens_builder.build();
}
const text = document.getText();
// Find all words with periods
const regex = /\b[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g;
let match;
while ((match = regex.exec(text)) !== null) {
const start_pos = document.positionAt(match.index);
// Skip if not in a comment
if (!is_in_comment(document, start_pos)) {
continue;
}
const word = match[0];
// Check if file exists in same directory
if (find_file_in_same_directory(document, word)) {
tokens_builder.push(
start_pos.line,
start_pos.character,
word.length,
0, // token type index for 'class' (teal)
0 // token modifiers
);
}
}
return tokens_builder.build();
}
}
/**
* Provides "Go to Definition" for file references in comments
*/
export class CommentFileReferenceDefinitionProvider implements vscode.DefinitionProvider {
async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position,
_token: vscode.CancellationToken
): Promise<vscode.Definition | undefined> {
// Only for JavaScript/TypeScript files
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return undefined;
}
// Must be in a comment
if (!is_in_comment(document, position)) {
return undefined;
}
// Get word with period
const word_info = get_word_with_period(document, position);
if (!word_info) {
return undefined;
}
// Check if file exists in same directory
const file_path = find_file_in_same_directory(document, word_info.word);
if (!file_path) {
return undefined;
}
// Return file location
const file_uri = vscode.Uri.file(file_path);
return new vscode.Location(file_uri, new vscode.Position(0, 0));
}
}

View File

@@ -9,8 +9,10 @@ import { get_config } from './config';
import { LaravelCompletionProvider } from './laravel_completion_provider';
import { blade_spacer } from './blade_spacer';
import { init_blade_language_config } from './blade_client';
import { ConventionMethodSemanticTokensProvider, ConventionMethodHoverProvider, ConventionMethodDiagnosticProvider, ConventionMethodDefinitionProvider } from './convention_method_provider';
import { JqhtmlLifecycleSemanticTokensProvider, JqhtmlLifecycleHoverProvider, JqhtmlLifecycleDiagnosticProvider } from './jqhtml_lifecycle_provider';
import { ConventionMethodHoverProvider, ConventionMethodDiagnosticProvider, ConventionMethodDefinitionProvider } from './convention_method_provider';
import { CommentFileReferenceDefinitionProvider } from './comment_file_reference_provider';
import { JqhtmlLifecycleHoverProvider, JqhtmlLifecycleDiagnosticProvider } from './jqhtml_lifecycle_provider';
import { CombinedSemanticTokensProvider } from './combined_semantic_provider';
import { PhpAttributeSemanticTokensProvider } from './php_attribute_provider';
import { BladeComponentSemanticTokensProvider } from './blade_component_provider';
import { AutoRenameProvider } from './auto_rename_provider';
@@ -22,6 +24,7 @@ import { RspadeRefactorCodeActionsProvider } from './refactor_code_actions';
import { RspadeClassRefactorProvider } from './class_refactor_provider';
import { RspadeClassRefactorCodeActionsProvider } from './class_refactor_code_actions';
import { RspadeSortClassMethodsProvider } from './sort_class_methods_provider';
import { SymlinkRedirectProvider } from './symlink_redirect_provider';
import * as fs from 'fs';
import * as path from 'path';
@@ -176,6 +179,11 @@ export async function activate(context: vscode.ExtensionContext) {
const git_diff_provider = new GitDiffProvider(rspade_root);
git_diff_provider.activate(context);
// Register symlink redirect provider
const symlink_redirect_provider = new SymlinkRedirectProvider();
symlink_redirect_provider.activate(context);
console.log('Symlink redirect provider registered - system/rsx/ files will redirect to rsx/');
// Register refactor provider
const refactor_provider = new RspadeRefactorProvider(formatting_provider);
refactor_provider.register(context);
@@ -288,7 +296,6 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('Laravel completion provider registered for PHP files');
// Register convention method providers for JavaScript/TypeScript
// Note: Semantic tokens are handled by JqhtmlLifecycleSemanticTokensProvider to avoid duplicate registration
const convention_hover_provider = new ConventionMethodHoverProvider();
const convention_diagnostic_provider = new ConventionMethodDiagnosticProvider();
const convention_definition_provider = new ConventionMethodDefinitionProvider();
@@ -312,18 +319,9 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('Convention method providers registered for JavaScript/TypeScript');
// Register JQHTML lifecycle method providers for JavaScript/TypeScript
const jqhtml_semantic_provider = new JqhtmlLifecycleSemanticTokensProvider();
const jqhtml_hover_provider = new JqhtmlLifecycleHoverProvider();
const jqhtml_diagnostic_provider = new JqhtmlLifecycleDiagnosticProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
jqhtml_semantic_provider,
new vscode.SemanticTokensLegend(['conventionMethod'])
)
);
context.subscriptions.push(
vscode.languages.registerHoverProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
@@ -335,6 +333,32 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('JQHTML lifecycle providers registered for JavaScript/TypeScript');
// Register combined semantic tokens provider for JavaScript/TypeScript
// This includes: JQHTML lifecycle methods (orange), file references (teal), 'that' variable (blue)
const combined_semantic_provider = new CombinedSemanticTokensProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
combined_semantic_provider,
new vscode.SemanticTokensLegend(['conventionMethod', 'class', 'macro'])
)
);
console.log('Combined semantic tokens provider registered (JQHTML lifecycle, file references, that variable)');
// Register comment file reference definition provider for JavaScript/TypeScript
const comment_file_reference_definition_provider = new CommentFileReferenceDefinitionProvider();
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(
[{ language: 'javascript' }, { language: 'typescript' }],
comment_file_reference_definition_provider
)
);
console.log('Comment file reference definition provider registered for JavaScript/TypeScript');
// Register PHP attribute provider
const php_attribute_provider = new PhpAttributeSemanticTokensProvider();
@@ -376,15 +400,6 @@ export async function activate(context: vscode.ExtensionContext) {
);
// Register commands
context.subscriptions.push(
vscode.commands.registerCommand('rspade.toggleFolding', () => {
const config = get_config();
const current = config.get<boolean>('enableCodeFolding', true);
config.update('enableCodeFolding', !current, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`RSpade code folding ${!current ? 'enabled' : 'disabled'}`);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('rspade.formatPhpFile', async () => {
const editor = vscode.window.activeTextEditor;

View File

@@ -477,18 +477,26 @@ export class JqhtmlLifecycleDiagnosticProvider {
// Check for violations in method body
if (method_name === 'on_create') {
// Check for this.data or that.data access
const data_access_regex = /(this|that)\.data/g;
// Check for this.data or that.data access (reading, not assignment)
// Note: on_create() now runs BEFORE first render, so assigning to this.data is valid
// We only warn on reading from this.data (accessing properties without assignment)
const data_access_regex = /(this|that)\.data\.(\w+)(\s*=)?/g;
let data_match;
while ((data_match = data_access_regex.exec(method_body)) !== null) {
// Skip if this is an assignment (has the = part in capture group 3)
if (data_match[3]) {
continue;
}
// This is a read access, not an assignment - warn about it
const violation_pos = document.positionAt(method_body_start + data_match.index);
const violation_end = document.positionAt(method_body_start + data_match.index + data_match[0].length);
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(violation_pos, violation_end),
`'${data_match[0]}' is populated during on_load, which happens after on_create. Did you mean ${data_match[1]}.args?`,
`'${data_match[0]}' is being read in on_create, but this.data should only be initialized here (assignments like 'this.data.rows = []' are OK)`,
vscode.DiagnosticSeverity.Warning
)
);

View File

@@ -7,6 +7,7 @@ const FRAMEWORK_ATTRIBUTES = [
'Ajax_Endpoint',
'Route',
'Auth',
'Task',
'Relationship',
'Monoprogenic',
'Instantiatable'

View File

@@ -194,15 +194,11 @@ export class RspadeRefactorProvider {
// Check if refactor was successful
if (result.includes('=== Refactor Complete ===') || result.trim().length > 0) {
// Wait 3.5 seconds total (2s + 1.5s), hide panel, then reload files
// Wait for filesystem changes to propagate then reload files
// Note: Panel is kept open so developer can see results and warnings
setTimeout(async () => {
await vscode.commands.executeCommand('workbench.action.closePanel');
// Wait 500ms for filesystem changes to propagate (Samba/network issues)
setTimeout(async () => {
await this.reload_all_open_files();
}, 500);
}, 3500);
await this.reload_all_open_files();
}, 500);
vscode.window.showInformationMessage(
`Successfully refactored ${method_info.class_name}::${method_info.method_name} to ${new_method_name}`

View File

@@ -0,0 +1,115 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
/**
* Redirects files opened from system/rsx/ symlink to their real location in rsx/
*
* The system/rsx/ directory is a symlink to rsx/ for framework compatibility,
* but users should always edit files in the real rsx/ directory.
*/
export class SymlinkRedirectProvider {
private disposables: vscode.Disposable[] = [];
public activate(context: vscode.ExtensionContext) {
// Watch for document opens and switches
this.disposables.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
this.check_and_redirect(editor.document);
}
})
);
// Also check when window first opens or tabs change
this.disposables.push(
vscode.workspace.onDidOpenTextDocument(document => {
this.check_and_redirect(document);
})
);
// Check currently active editor immediately
if (vscode.window.activeTextEditor) {
this.check_and_redirect(vscode.window.activeTextEditor.document);
}
console.log('[RSpade] Symlink redirect provider activated');
}
private async check_and_redirect(document: vscode.TextDocument) {
const file_path = document.uri.fsPath;
// Check if this is a file in system/rsx/
if (!file_path.includes('/system/rsx/') && !file_path.includes('\\system\\rsx\\')) {
return; // Not in system/rsx/, no action needed
}
// Find the workspace folder
const workspace_folder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspace_folder) {
return;
}
const workspace_root = workspace_folder.uri.fsPath;
// Extract the path after system/rsx/
const system_rsx_pattern = /[\/\\]system[\/\\]rsx[\/\\](.*)/;
const match = file_path.match(system_rsx_pattern);
if (!match) {
return; // Pattern doesn't match
}
const relative_path = match[1];
const real_file = path.join(workspace_root, 'rsx', relative_path);
// Check if the real file exists
if (!fs.existsSync(real_file)) {
// Real file doesn't exist, this might be a framework file or something else
return;
}
console.log(`[RSpade] Redirecting from system/rsx/ symlink to real file:`);
console.log(` Symlink: ${file_path}`);
console.log(` Real: ${real_file}`);
// Check if the symlink version is pinned
const is_pinned = document.uri.scheme === 'file' &&
vscode.window.tabGroups.activeTabGroup.activeTab?.isPinned;
// If pinned, unpin it first
if (is_pinned) {
await vscode.commands.executeCommand('workbench.action.unpinEditor');
}
// Open the real file
const real_uri = vscode.Uri.file(real_file);
const real_document = await vscode.workspace.openTextDocument(real_uri);
await vscode.window.showTextDocument(real_document);
// If the original was pinned, pin the new one
if (is_pinned) {
await vscode.commands.executeCommand('workbench.action.pinEditor');
}
// Close the symlink version (now in background)
// Find the tab with the symlink path and close it
const tab_groups = vscode.window.tabGroups.all;
for (const group of tab_groups) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText &&
tab.input.uri.fsPath === file_path) {
await vscode.window.tabGroups.close(tab);
break;
}
}
}
// Show brief notification
vscode.window.setStatusBarMessage('Redirected from system/rsx/ to rsx/', 2000);
}
public dispose() {
this.disposables.forEach(d => d.dispose());
}
}

View File

@@ -0,0 +1,103 @@
import * as vscode from 'vscode';
/**
* Check if position is inside a string
*/
function is_in_string(document: vscode.TextDocument, position: vscode.Position): boolean {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Simple check: count quotes before position
let single_quotes = 0;
let double_quotes = 0;
for (let i = 0; i < char_pos; i++) {
if (line_text[i] === "'" && (i === 0 || line_text[i - 1] !== '\\')) {
single_quotes++;
} else if (line_text[i] === '"' && (i === 0 || line_text[i - 1] !== '\\')) {
double_quotes++;
}
}
// If odd number of quotes, we're inside a string
return (single_quotes % 2 === 1) || (double_quotes % 2 === 1);
}
/**
* Check if position is inside a comment
*/
function is_in_comment(document: vscode.TextDocument, position: vscode.Position): boolean {
const line_text = document.lineAt(position.line).text;
const char_pos = position.character;
// Check for single-line comment
const single_comment_idx = line_text.indexOf('//');
if (single_comment_idx !== -1 && single_comment_idx < char_pos) {
return true;
}
// Check for multi-line comment by looking at text before position
const text_before = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
let in_block_comment = false;
let i = 0;
while (i < text_before.length) {
if (text_before.substring(i, i + 2) === '/*') {
in_block_comment = true;
i += 2;
} else if (text_before.substring(i, i + 2) === '*/') {
in_block_comment = false;
i += 2;
} else {
i++;
}
}
return in_block_comment;
}
/**
* Provides semantic tokens for 'that' variable (dark blue like 'this' keyword)
*/
export class ThatVariableSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
// Only for JavaScript/TypeScript files
if (document.languageId !== 'javascript' && document.languageId !== 'typescript') {
return tokens_builder.build();
}
const text = document.getText();
// Find all occurrences of 'that' as a standalone word
const regex = /\bthat\b/g;
let match;
while ((match = regex.exec(text)) !== null) {
const start_pos = document.positionAt(match.index);
// Skip if inside a string
if (is_in_string(document, start_pos)) {
continue;
}
// Skip if inside a comment
if (is_in_comment(document, start_pos)) {
continue;
}
// Highlight 'that' with same blue color as 'this' keyword
// Using 'variable' type with 'defaultLibrary' modifier to match language variable color
tokens_builder.push(
start_pos.line,
start_pos.character,
4, // length of 'that'
0, // token type index for 'variable'
1 // token modifiers: bit 0 = defaultLibrary
);
}
return tokens_builder.build();
}
}