Refactor jqhtml integration to use jqhtml.boot() and migrate blade highlighting to jqhtml extension

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-25 03:33:09 +00:00
parent bd5809fdbd
commit 9be3dfc14e
58 changed files with 817 additions and 672 deletions

View File

@@ -1,58 +0,0 @@
import * as vscode from 'vscode';
export const init_blade_language_config = () => {
// HTML empty elements that don't require closing tags
const EMPTY_ELEMENTS: string[] = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr',
];
// Configure Blade language indentation and auto-closing behavior
vscode.languages.setLanguageConfiguration('blade', {
indentationRules: {
increaseIndentPattern:
/<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
decreaseIndentPattern:
/^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/,
},
wordPattern:
/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
// When pressing Enter between opening and closing tags, auto-indent
beforeText: new RegExp(
`<(?!(?:${EMPTY_ELEMENTS.join(
'|'
)}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`,
'i'
),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent },
},
{
// When pressing Enter after opening tag, auto-indent
beforeText: new RegExp(
`<(?!(?:${EMPTY_ELEMENTS.join(
'|'
)}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
'i'
),
action: { indentAction: vscode.IndentAction.Indent },
},
],
});
};

View File

@@ -1,66 +0,0 @@
import * as vscode from 'vscode';
/**
* Provides semantic tokens for uppercase component tags in Blade files
* Highlights component tag names in light green
* Highlights tag="" attribute in orange on jqhtml components
*/
export class BladeComponentSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider {
async provideDocumentSemanticTokens(document: vscode.TextDocument): Promise<vscode.SemanticTokens> {
const tokens_builder = new vscode.SemanticTokensBuilder();
if (document.languageId !== 'blade') {
return tokens_builder.build();
}
const text = document.getText();
// Match opening tags that start with uppercase letter to find jqhtml components
// Matches: <ComponentName ...>, captures the entire tag up to >
const component_tag_regex = /<([A-Z][a-zA-Z0-9_]*)([^>]*?)>/g;
let component_match;
while ((component_match = component_tag_regex.exec(text)) !== null) {
const tag_name = component_match[1];
const tag_attributes = component_match[2];
const tag_start = component_match.index + component_match[0].indexOf(tag_name);
const tag_position = document.positionAt(tag_start);
// Push token for the component tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(tag_position.line, tag_position.character, tag_name.length, 0, 0);
// Now look for tag="" attribute within this component's attributes
// Matches: tag="..." or tag='...'
const tag_attr_regex = /\btag\s*=/g;
let attr_match;
while ((attr_match = tag_attr_regex.exec(tag_attributes)) !== null) {
// Calculate the position of 'tag' within the document
const attr_start = component_match.index + component_match[0].indexOf(tag_attributes) + attr_match.index;
const attr_position = document.positionAt(attr_start);
// Push token for 'tag' attribute name
// Token type 1 maps to 'jqhtmlTagAttribute' which we'll define to be orange
tokens_builder.push(attr_position.line, attr_position.character, 3, 1, 0);
}
}
// Also match closing tags that start with uppercase letter
// Matches: </ComponentName>
const closing_tag_regex = /<\/([A-Z][a-zA-Z0-9_]*)/g;
let closing_match;
while ((closing_match = closing_tag_regex.exec(text)) !== null) {
const tag_name = closing_match[1];
const tag_start = closing_match.index + closing_match[0].indexOf(tag_name);
const position = document.positionAt(tag_start);
// Push token for the tag name
// Token type 0 maps to 'class' which VS Code themes style as entity.name.class (turquoise/cyan)
tokens_builder.push(position.line, position.character, tag_name.length, 0, 0);
}
return tokens_builder.build();
}
}

View File

@@ -1,122 +0,0 @@
import * as vscode from 'vscode';
import { get_config } from './config';
const TAG_DOUBLE = 0;
const TAG_UNESCAPED = 1;
const TAG_COMMENT = 2;
const snippets: Record<number, string> = {
[TAG_DOUBLE]: '{{ ${1:${TM_SELECTED_TEXT/[{}]//g}} }}$0',
[TAG_UNESCAPED]: '{!! ${1:${TM_SELECTED_TEXT/[{} !]//g}} !!}$0',
[TAG_COMMENT]: '{{-- ${1:${TM_SELECTED_TEXT/(--)|[{} ]//g}} --}}$0',
};
const triggers = ['{}', '!', '-', '{'];
const regexes = [
/({{(?!\s|-))(.*?)(}})/,
/({!!(?!\s))(.*?)?(}?)/,
/({{[\s]?--)(.*?)?(}})/,
];
const translate = (position: vscode.Position, offset: number): vscode.Position => {
try {
return position.translate(0, offset);
} catch (error) {
// VS Code doesn't like negative numbers passed
// to translate (even though it works fine), so
// this block prevents debug console errors
}
return position;
};
const chars_for_change = (
doc: vscode.TextDocument,
change: vscode.TextDocumentContentChangeEvent
): number => {
if (change.text === '!') {
return 2;
}
if (change.text !== '-') {
return 1;
}
const start = translate(change.range.start, -2);
const end = translate(change.range.start, -1);
return doc.getText(new vscode.Range(start, end)) === ' ' ? 4 : 3;
};
export const blade_spacer = async (
e: vscode.TextDocumentChangeEvent,
editor?: vscode.TextEditor
) => {
const config = get_config();
if (
!config.get('enableBladeAutoSpacing', true) ||
!editor ||
editor.document.fileName.indexOf('.blade.php') === -1
) {
return;
}
let tag_type: number = -1;
let ranges: vscode.Range[] = [];
let offsets: number[] = [];
// Changes (per line) come in right-to-left when we need them left-to-right
const changes = e.contentChanges.slice().reverse();
changes.forEach((change) => {
if (triggers.indexOf(change.text) === -1) {
return;
}
if (!offsets[change.range.start.line]) {
offsets[change.range.start.line] = 0;
}
const start_offset =
offsets[change.range.start.line] -
chars_for_change(e.document, change);
const start = translate(change.range.start, start_offset);
const line_end = e.document.lineAt(start.line).range.end;
for (let i = 0; i < regexes.length; i++) {
// If we typed a - or a !, don't consider the "double" tag type
if (i === TAG_DOUBLE && ['-', '!'].indexOf(change.text) !== -1) {
continue;
}
// Only look at unescaped tags if we need to
if (i === TAG_UNESCAPED && change.text !== '!') {
continue;
}
// Only look at comment tags if we need to
if (i === TAG_COMMENT && change.text !== '-') {
continue;
}
const tag = regexes[i].exec(
e.document.getText(new vscode.Range(start, line_end))
);
if (tag) {
tag_type = i;
ranges.push(
new vscode.Range(start, start.translate(0, tag[0].length))
);
offsets[start.line] += tag[1].length;
}
}
});
if (ranges.length > 0 && snippets[tag_type]) {
editor.insertSnippet(new vscode.SnippetString(snippets[tag_type]), ranges);
}
};

View File

@@ -7,14 +7,11 @@ import { RspadeDefinitionProvider } from './definition_provider';
import { DebugClient } from './debug_client';
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 { 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';
import { FolderColorProvider } from './folder_color_provider';
import { GitStatusProvider } from './git_status_provider';
@@ -255,18 +252,6 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('RSpade formatter registered for PHP files');
// Initialize Blade language configuration (indentation, auto-closing)
init_blade_language_config();
console.log('Blade language configuration initialized');
// Register Blade auto-spacing on text change
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
blade_spacer(event, vscode.window.activeTextEditor);
})
);
console.log('Blade auto-spacing enabled');
// Register definition provider for JavaScript/TypeScript and PHP/Blade/jqhtml files
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(
@@ -372,19 +357,6 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('PHP attribute provider registered for PHP files');
// Register Blade component provider for uppercase component tags
const blade_component_provider = new BladeComponentSemanticTokensProvider();
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
[{ language: 'blade' }, { pattern: '**/*.blade.php' }],
blade_component_provider,
new vscode.SemanticTokensLegend(['class', 'jqhtmlTagAttribute'])
)
);
console.log('Blade component provider registered for Blade files');
// Debug client disabled
// debug_client = new DebugClient(formatting_provider as any);
// debug_client.start().catch(error => {