blueprint/extension/server/out/server.js
obvTiger d125640fe7 beta/code-blocks (#1)
Reviewed-on: #1
Co-authored-by: obvTiger <obvtiger@epilogue.team>
Co-committed-by: obvTiger <obvtiger@epilogue.team>
2025-04-01 15:22:15 +02:00

289 lines
No EOL
12 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_1 = require("vscode-languageserver/node");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
// Create a connection for the server
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
// Create a text document manager
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
// Blueprint template
const blueprintTemplate = `page {
title { "$1" }
description { "$2" }
keywords { "$3" }
author { "$4" }
}
navbar {
horizontal {
link(href:$5) { text(bold) { "$6" } }
links {
link(href:$7) { "$8" }
link(href:$9) { "$10" }
link(href:$11) { "$12" }
}
}
}
horizontal(centered) {
vertical(centered) {
title(huge,margin:0) { "$13" }
text(subtle,margin:0) { "$14" }
}
}`;
// Blueprint elements that can be used
const elements = [
'section', 'grid', 'horizontal', 'vertical', 'title', 'text',
'link', 'links', 'button', 'button-light', 'button-secondary', 'button-compact',
'card', 'badge', 'alert', 'tooltip', 'input', 'textarea', 'select',
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
];
// Script blocks
const scriptBlocks = [
'client', 'server'
];
// Single instance elements
const singleElements = ['page', 'navbar'];
// Blueprint properties
const properties = [
'wide', 'centered', 'alternate', 'padding', 'margin', 'columns', 'responsive',
'gap', 'spaced', 'huge', 'large', 'small', 'tiny', 'bold', 'light', 'normal',
'italic', 'underline', 'strike', 'uppercase', 'lowercase', 'capitalize',
'subtle', 'accent', 'error', 'success', 'warning', 'hover-scale', 'hover-raise',
'hover-glow', 'hover-underline', 'hover-fade', 'focus-glow', 'focus-outline',
'focus-scale', 'active-scale', 'active-color', 'active-raise', 'mobile-stack',
'mobile-hide', 'tablet-wrap', 'tablet-hide', 'desktop-wide', 'desktop-hide'
];
// Page configuration properties
const pageProperties = ['title', 'description', 'keywords', 'author'];
// ID attribute suggestion - using underscore format
const idAttributeTemplate = 'id:$1_$2';
// Container elements that can have children
const containerElements = [
'horizontal', 'vertical', 'section', 'grid', 'navbar',
'links', 'card'
];
connection.onInitialize((params) => {
const result = {
capabilities: {
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
completionProvider: {
resolveProvider: false,
triggerCharacters: ['{', '(', ' ', '!']
}
}
};
return result;
});
// Check if an element exists in the document
function elementExists(text, element) {
const regex = new RegExp(`\\b${element}\\s*{`, 'i');
return regex.test(text);
}
// This handler provides the initial list of completion items.
connection.onCompletion((textDocumentPosition) => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return [];
}
const text = document.getText();
const lines = text.split('\n');
const position = textDocumentPosition.position;
const line = lines[position.line];
const linePrefix = line.slice(0, position.character);
// Suggest script blocks after @ symbol
if (linePrefix.trim().endsWith('@')) {
return scriptBlocks.map(block => ({
label: `@${block}`,
kind: node_1.CompletionItemKind.Snippet,
insertText: `@${block} {\n $1\n}`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: block === 'client' ?
'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.' :
'Create a server-side JavaScript block that runs on the server.'
}));
}
// Check if this is a template completion trigger
if (linePrefix.trim() === '!') {
return [{
label: '!blueprint',
kind: node_1.CompletionItemKind.Snippet,
insertText: blueprintTemplate,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: 'Insert Blueprint starter template with customizable placeholders',
preselect: true,
// Add a command to delete the '!' character
additionalTextEdits: [{
range: {
start: { line: position.line, character: linePrefix.indexOf('!') },
end: { line: position.line, character: linePrefix.indexOf('!') + 1 }
},
newText: ''
}]
}];
}
// Inside page block
if (text.includes('page {') && !text.includes('}')) {
return pageProperties.map(prop => ({
label: prop,
kind: node_1.CompletionItemKind.Property,
insertText: `${prop} { "$1" }`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: `Add ${prop} to the page configuration`
}));
}
// After an opening parenthesis, suggest properties including ID with underscore format
if (linePrefix.trim().endsWith('(')) {
return [
...properties.map(prop => ({
label: prop,
kind: node_1.CompletionItemKind.Property,
documentation: `Apply ${prop} property`
})),
{
label: 'id',
kind: node_1.CompletionItemKind.Property,
insertText: idAttributeTemplate,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: 'Add an ID to the element (use underscores instead of hyphens for JavaScript compatibility)'
}
];
}
// After a container element's opening brace, suggest child elements
const containerMatch = /\b(horizontal|vertical|section|grid|navbar|links|card)\s*{\s*$/.exec(linePrefix);
if (containerMatch) {
const parentElement = containerMatch[1];
let suggestedElements = elements;
// Customize suggestions based on parent element
switch (parentElement) {
case 'navbar':
suggestedElements = ['horizontal', 'vertical', 'link', 'links', 'text'];
break;
case 'links':
suggestedElements = ['link'];
break;
case 'card':
suggestedElements = ['title', 'text', 'button', 'image'];
break;
}
// Include client/server block suggestions for interactive elements
if (['button', 'button-light', 'button-secondary', 'button-compact'].includes(parentElement)) {
return [
...suggestedElements.map(element => ({
label: element,
kind: node_1.CompletionItemKind.Class,
insertText: `${element} {\n $1\n}`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: `Create a ${element} block inside ${parentElement}`
})),
{
label: '@client',
kind: node_1.CompletionItemKind.Snippet,
insertText: `@client {\n $1\n}`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: 'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.'
}
];
}
return suggestedElements.map(element => ({
label: element,
kind: node_1.CompletionItemKind.Class,
insertText: `${element} {\n $1\n}`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: `Create a ${element} block inside ${parentElement}`
}));
}
// Inside interactive elements, suggest @client blocks
const interactiveElementMatch = /\b(button|button-light|button-secondary|button-compact|input|textarea|select|checkbox|radio|switch)\s*(?:\([^)]*\))?\s*{\s*$/.exec(linePrefix);
if (interactiveElementMatch) {
return [
{
label: '@client',
kind: node_1.CompletionItemKind.Snippet,
insertText: `@client {\n $1\n}`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: 'Create a client-side JavaScript block that runs when the element is clicked. The "e" event object is available.'
},
{
label: 'text',
kind: node_1.CompletionItemKind.Class,
insertText: `"$1"`,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: 'Add text content to the element'
}
];
}
// Get available single instance elements
const availableSingleElements = singleElements.filter(element => !elementExists(text, element));
// Combine regular elements with available single instance elements
const availableElements = [
...elements,
...availableSingleElements
];
// Default: suggest elements
return availableElements.map(element => {
const isPage = element === 'page';
const insertText = isPage ?
'page {\n title { "$1" }\n description { "$2" }\n keywords { "$3" }\n author { "$4" }\n}' :
`${element} {\n $1\n}`;
return {
label: element,
kind: node_1.CompletionItemKind.Class,
insertText: insertText,
insertTextFormat: node_1.InsertTextFormat.Snippet,
documentation: `Create a ${element} block${isPage ? ' (only one allowed per file)' : ''}`
};
});
});
// Find all occurrences of an element in the document
function findElementOccurrences(text, element) {
const occurrences = [];
const lines = text.split('\n');
const regex = new RegExp(`\\b(${element})\\s*{`, 'g');
lines.forEach((line, lineIndex) => {
let match;
while ((match = regex.exec(line)) !== null) {
const startChar = match.index;
const endChar = match.index + match[1].length;
occurrences.push({
start: { line: lineIndex, character: startChar },
end: { line: lineIndex, character: endChar }
});
}
});
return occurrences;
}
// Validate the document for duplicate elements
function validateDocument(document) {
const text = document.getText();
const diagnostics = [];
// Check for duplicate single instance elements
singleElements.forEach(element => {
const occurrences = findElementOccurrences(text, element);
if (occurrences.length > 1) {
// Add diagnostic for each duplicate occurrence (skip the first one)
occurrences.slice(1).forEach(occurrence => {
diagnostics.push({
severity: node_1.DiagnosticSeverity.Error,
range: node_1.Range.create(occurrence.start, occurrence.end),
message: `Only one ${element} element is allowed per file.`,
source: 'blueprint'
});
});
}
});
// Send the diagnostics to the client
connection.sendDiagnostics({ uri: document.uri, diagnostics });
}
// Set up document validation events
documents.onDidChangeContent((change) => {
validateDocument(change.document);
});
documents.onDidOpen((event) => {
validateDocument(event.document);
});
// Make the text document manager listen on the connection
documents.listen(connection);
// Listen on the connection
connection.listen();
//# sourceMappingURL=server.js.map