This commit is contained in:
obvTiger 2025-01-21 14:00:09 +01:00
commit 47f67eea8c
43 changed files with 5819 additions and 0 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

3
extension/client/out/extension.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
import { ExtensionContext } from 'vscode';
export declare function activate(context: ExtensionContext): void;
export declare function deactivate(): Thenable<void> | undefined;

View file

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.activate = activate;
exports.deactivate = deactivate;
// File: client/src/extension.ts
const path = require("path");
const vscode_1 = require("vscode");
const node_1 = require("vscode-languageclient/node");
let client;
function activate(context) {
// The server is implemented in node
const serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
// The debug options for the server
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
const serverOptions = {
run: { module: serverModule, transport: node_1.TransportKind.ipc },
debug: {
module: serverModule,
transport: node_1.TransportKind.ipc,
options: debugOptions
}
};
// Options to control the language client
const clientOptions = {
// Register the server for Blueprint documents
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
synchronize: {
// Notify the server about file changes to '.bp files contained in the workspace
fileEvents: vscode_1.workspace.createFileSystemWatcher('**/*.bp')
}
};
// Create and start the client
client = new node_1.LanguageClient('blueprintLanguageServer', 'Blueprint Language Server', serverOptions, clientOptions);
// Start the client. This will also launch the server
client.start();
}
function deactivate() {
if (!client) {
return undefined;
}
return client.stop();
}
//# sourceMappingURL=extension.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;AAYA,4BAwCC;AAED,gCAKC;AA3DD,gCAAgC;AAChC,6BAA6B;AAC7B,mCAAqD;AACrD,qDAKoC;AAEpC,IAAI,MAAsB,CAAC;AAE3B,SAAgB,QAAQ,CAAC,OAAyB;IAC9C,oCAAoC;IACpC,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,CACvC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,WAAW,CAAC,CAC1C,CAAC;IAEF,mCAAmC;IACnC,MAAM,YAAY,GAAG,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC,EAAE,CAAC;IAElE,oFAAoF;IACpF,qCAAqC;IACrC,MAAM,aAAa,GAAkB;QACjC,GAAG,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,oBAAa,CAAC,GAAG,EAAE;QAC3D,KAAK,EAAE;YACH,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,oBAAa,CAAC,GAAG;YAC5B,OAAO,EAAE,YAAY;SACxB;KACJ,CAAC;IAEF,yCAAyC;IACzC,MAAM,aAAa,GAA0B;QACzC,8CAA8C;QAC9C,gBAAgB,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QAC7D,WAAW,EAAE;YACT,gFAAgF;YAChF,UAAU,EAAE,kBAAS,CAAC,uBAAuB,CAAC,SAAS,CAAC;SAC3D;KACJ,CAAC;IAEF,8BAA8B;IAC9B,MAAM,GAAG,IAAI,qBAAc,CACvB,yBAAyB,EACzB,2BAA2B,EAC3B,aAAa,EACb,aAAa,CAChB,CAAC;IAEF,qDAAqD;IACrD,MAAM,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,SAAgB,UAAU;IACtB,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC"}

View file

@ -0,0 +1,60 @@
// File: client/src/extension.ts
import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
let client: LanguageClient;
export function activate(context: ExtensionContext) {
// The server is implemented in node
const serverModule = context.asAbsolutePath(
path.join('server', 'out', 'server.js')
);
// The debug options for the server
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions
}
};
// Options to control the language client
const clientOptions: LanguageClientOptions = {
// Register the server for Blueprint documents
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
synchronize: {
// Notify the server about file changes to '.bp files contained in the workspace
fileEvents: workspace.createFileSystemWatcher('**/*.bp')
}
};
// Create and start the client
client = new LanguageClient(
'blueprintLanguageServer',
'Blueprint Language Server',
serverOptions,
clientOptions
);
// Start the client. This will also launch the server
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"outDir": "out",
"rootDir": "src",
"composite": true,
"tsBuildInfoFile": "./out/.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

View file

@ -0,0 +1,77 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.activate = activate;
exports.deactivate = deactivate;
// File: client/src/extension.ts
const path = __importStar(require("path"));
const vscode_1 = require("vscode");
const node_1 = require("vscode-languageclient/node");
let client;
function activate(context) {
// The server is implemented in node
const serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
// The debug options for the server
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
const serverOptions = {
run: { module: serverModule, transport: node_1.TransportKind.ipc },
debug: {
module: serverModule,
transport: node_1.TransportKind.ipc,
options: debugOptions
}
};
// Options to control the language client
const clientOptions = {
// Register the server for Blueprint documents
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
synchronize: {
// Notify the server about file changes to '.bp files contained in the workspace
fileEvents: vscode_1.workspace.createFileSystemWatcher('**/*.bp')
}
};
// Create and start the client
client = new node_1.LanguageClient('blueprintLanguageServer', 'Blueprint Language Server', serverOptions, clientOptions);
// Start the client. This will also launch the server
client.start();
}
function deactivate() {
if (!client) {
return undefined;
}
return client.stop();
}

View file

@ -0,0 +1 @@
{"root":["../client/src/extension.ts"],"version":"5.7.2"}

View file

@ -0,0 +1 @@
{"root":["../client/src/extension.ts","../server/src/server.ts"],"version":"5.7.3"}

49
extension/package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "blueprint-language",
"displayName": "Blueprint Language Support",
"description": "Language support for Blueprint layout files",
"version": "0.0.1",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages"
],
"main": "./client/out/extension.js",
"contributes": {
"languages": [{
"id": "blueprint",
"aliases": ["Blueprint", "blueprint"],
"extensions": [".bp"],
"configuration": "./language-configuration.json"
}],
"grammars": [{
"language": "blueprint",
"scopeName": "source.blueprint",
"path": "./syntaxes/blueprint.tmLanguage.json"
}]
},
"activationEvents": [
"onLanguage:blueprint"
],
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -b",
"watch": "tsc -b -w",
"compile:client": "tsc -b ./client/tsconfig.json",
"compile:server": "tsc -b ./server/tsconfig.json"
},
"dependencies": {
"vscode-languageclient": "^8.1.0",
"vscode-languageserver": "^8.1.0",
"vscode-languageserver-textdocument": "^1.0.8"
},
"devDependencies": {
"@types/node": "^16.11.7",
"@types/vscode": "^1.75.0",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"eslint": "^8.35.0",
"typescript": "^5.0.2"
}
}

File diff suppressed because one or more lines are too long

1
extension/server/out/server.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1,223 @@
"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'
];
// 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'];
// 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);
// 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
if (linePrefix.trim().endsWith('(')) {
return properties.map(prop => ({
label: prop,
kind: node_1.CompletionItemKind.Property,
documentation: `Apply ${prop} property`
}));
}
// 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;
}
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}`
}));
}
// 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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,271 @@
import {
createConnection,
TextDocuments,
ProposedFeatures,
InitializeParams,
TextDocumentSyncKind,
InitializeResult,
CompletionItem,
CompletionItemKind,
TextDocumentPositionParams,
InsertTextFormat,
Diagnostic,
DiagnosticSeverity,
Position,
Range,
TextDocumentChangeEvent
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
// Create a connection for the server
const connection = createConnection(ProposedFeatures.all);
// Create a text document manager
const documents: TextDocuments<TextDocument> = new TextDocuments(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'
];
// 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'];
// Container elements that can have children
const containerElements = [
'horizontal', 'vertical', 'section', 'grid', 'navbar',
'links', 'card'
];
connection.onInitialize((params: InitializeParams) => {
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: {
resolveProvider: false,
triggerCharacters: ['{', '(', ' ', '!']
}
}
};
return result;
});
// Check if an element exists in the document
function elementExists(text: string, element: string): boolean {
const regex = new RegExp(`\\b${element}\\s*{`, 'i');
return regex.test(text);
}
// This handler provides the initial list of completion items.
connection.onCompletion(
(textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
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);
// Check if this is a template completion trigger
if (linePrefix.trim() === '!') {
return [{
label: '!blueprint',
kind: CompletionItemKind.Snippet,
insertText: blueprintTemplate,
insertTextFormat: 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: CompletionItemKind.Property,
insertText: `${prop} { "$1" }`,
insertTextFormat: InsertTextFormat.Snippet,
documentation: `Add ${prop} to the page configuration`
}));
}
// After an opening parenthesis, suggest properties
if (linePrefix.trim().endsWith('(')) {
return properties.map(prop => ({
label: prop,
kind: CompletionItemKind.Property,
documentation: `Apply ${prop} property`
}));
}
// 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;
}
return suggestedElements.map(element => ({
label: element,
kind: CompletionItemKind.Class,
insertText: `${element} {\n $1\n}`,
insertTextFormat: InsertTextFormat.Snippet,
documentation: `Create a ${element} block inside ${parentElement}`
}));
}
// 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: CompletionItemKind.Class,
insertText: insertText,
insertTextFormat: 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: string, element: string): { start: Position; end: Position; }[] {
const occurrences: { start: Position; end: Position; }[] = [];
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: TextDocument): void {
const text = document.getText();
const diagnostics: Diagnostic[] = [];
// 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: DiagnosticSeverity.Error,
range: 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: TextDocumentChangeEvent<TextDocument>) => {
validateDocument(change.document);
});
documents.onDidOpen((event: TextDocumentChangeEvent<TextDocument>) => {
validateDocument(event.document);
});
// Make the text document manager listen on the connection
documents.listen(connection);
// Listen on the connection
connection.listen();

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"outDir": "out",
"rootDir": "src",
"composite": true,
"tsBuildInfoFile": "./out/.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

View file

@ -0,0 +1,136 @@
{
"name": "Blueprint",
"scopeName": "source.blueprint",
"fileTypes": ["bp"],
"patterns": [
{
"include": "#comments"
},
{
"include": "#page-config"
},
{
"include": "#elements"
},
{
"include": "#properties"
},
{
"include": "#strings"
},
{
"include": "#punctuation"
}
],
"repository": {
"comments": {
"match": "//.*$",
"name": "comment.line.double-slash.blueprint"
},
"page-config": {
"begin": "\\b(page)\\b",
"end": "(?=})",
"beginCaptures": {
"1": { "name": "entity.name.tag.blueprint" }
},
"patterns": [
{
"begin": "\\b(description|keywords|author)\\b\\s*\\{",
"end": "\\}",
"beginCaptures": {
"1": { "name": "entity.name.tag.blueprint" }
},
"patterns": [
{ "include": "#strings" }
]
},
{
"begin": "\\b(title)\\b\\s*\\{",
"end": "\\}",
"beginCaptures": {
"1": { "name": "entity.name.tag.title.blueprint" }
},
"patterns": [
{ "include": "#strings" }
]
}
]
},
"elements": {
"patterns": [
{
"match": "\\b(section|grid|horizontal|vertical|navbar|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|media)\\b",
"name": "entity.name.tag.blueprint"
}
]
},
"properties": {
"patterns": [
{
"match": "\\b(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)\\b",
"name": "support.type.property-name.blueprint"
},
{
"match": "(?<!:)(src|type|href|\\w+)\\s*:",
"captures": {
"1": { "name": "support.type.property-name.blueprint" }
}
},
{
"match": "(?<=type:)\\s*(img|video)\\b",
"name": "string.other.media-type.blueprint"
},
{
"match": "(?<=src:|href:)\\s*https?:\\/\\/[\\w\\-\\.]+(?:\\/[\\w\\-\\.\\/?=&%]*)?",
"name": "string.url.blueprint"
},
{
"match": "#[0-9a-fA-F]{3,6}",
"name": "constant.other.color.hex.blueprint"
},
{
"match": "\\b\\d+(%|px|rem|em)?\\b",
"name": "constant.numeric.blueprint"
}
]
},
"strings": {
"patterns": [
{
"begin": "\"",
"end": "\"",
"name": "string.quoted.double.blueprint",
"patterns": [
{
"match": "\\\\.",
"name": "constant.character.escape.blueprint"
}
]
},
{
"begin": "'",
"end": "'",
"name": "string.quoted.single.blueprint",
"patterns": [
{
"match": "\\\\.",
"name": "constant.character.escape.blueprint"
}
]
}
]
},
"punctuation": {
"patterns": [
{
"match": "[{}()]",
"name": "punctuation.section.blueprint"
},
{
"match": ",",
"name": "punctuation.separator.blueprint"
}
]
}
}
}

26
extension/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "out",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2020"],
"sourceMap": true
},
"include": [
"client/src",
"server/src"
],
"exclude": [
"node_modules",
".vscode-test"
],
"references": [
{ "path": "./client" },
{ "path": "./server" }
]
}