build_and_puplish.yml / build_and_publish (release) Successful in 36s
This adds a small DSL-aware helper to extract CDL event declarations and generate a ready-to-use snippet. It also extends hover support to show the snippet and parameter hints for CDL events, improving developer productivity. - Generate a MOM event handler scaffold and local vars - Hover shows the generated snippet and mom_ parameter hints
554 lines
18 KiB
TypeScript
554 lines
18 KiB
TypeScript
import * as vscode from "vscode"
|
|
import {
|
|
cdlEventHandlerAtLine,
|
|
createCdlEventHandlerSnippet
|
|
} from "./cdlEventHandler"
|
|
|
|
export function formatCdlFile(content: string): string {
|
|
let indentLevel = 0
|
|
const formattedLines = []
|
|
for (const line of content.split("\n")) {
|
|
const strippedLine = line.trim()
|
|
if (strippedLine.endsWith("}")) {
|
|
indentLevel = Math.max(indentLevel - 1, 0)
|
|
}
|
|
formattedLines.push(" ".repeat(indentLevel) + strippedLine)
|
|
if (strippedLine.endsWith("{")) {
|
|
indentLevel += 1
|
|
}
|
|
if (strippedLine.startsWith("#")) {
|
|
formattedLines[formattedLines.length - 1] =
|
|
" ".repeat(indentLevel) + strippedLine.replace(/^#\s*/, "# ")
|
|
}
|
|
}
|
|
return formattedLines.join("\n")
|
|
}
|
|
|
|
export function formatDefFile(content: string): string {
|
|
let indentLevel = 0
|
|
const formattedLines = []
|
|
for (const line of content.split("\n")) {
|
|
const strippedLine = line.trim()
|
|
if (strippedLine.endsWith("}")) {
|
|
indentLevel = Math.max(indentLevel - 1, 0)
|
|
}
|
|
formattedLines.push(" ".repeat(indentLevel) + strippedLine)
|
|
if (strippedLine.endsWith("{")) {
|
|
indentLevel += 1
|
|
}
|
|
}
|
|
return formattedLines.join("\n")
|
|
}
|
|
|
|
export function isFirstLineMachine(content: string): boolean {
|
|
const lines = content.split("\n").map((line) => line.trim())
|
|
for (const line of lines) {
|
|
console.log(line)
|
|
if (line === "" || line.startsWith("#")) {
|
|
console.log("skipping line")
|
|
continue
|
|
}
|
|
const machineRegex = /^MACHINE\s+\S+/
|
|
return machineRegex.test(line)
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function diagnosticHandler(document: vscode.TextDocument) {
|
|
const diagnostics: vscode.Diagnostic[] = []
|
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
|
if (!isFirstLineMachine(document.getText())) {
|
|
const range = new vscode.Range(
|
|
document.positionAt(0),
|
|
document.positionAt(document.getText().length)
|
|
)
|
|
const diagnostic = new vscode.Diagnostic(
|
|
range,
|
|
"The first line should contain 'MACHINE'.",
|
|
vscode.DiagnosticSeverity.Error
|
|
)
|
|
diagnostics.push(diagnostic)
|
|
}
|
|
}
|
|
return diagnostics
|
|
}
|
|
|
|
export function completionHandlerCdl(document: vscode.TextDocument, position: vscode.Position) {
|
|
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
|
const categories = ["MILL", "LATHE", "DRILL"]
|
|
|
|
if (linePrefix.endsWith("TYPE ")) {
|
|
return [
|
|
new vscode.CompletionItem("o", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("b", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("i", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("d", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("g", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("s", vscode.CompletionItemKind.TypeParameter)
|
|
]
|
|
}
|
|
|
|
if (linePrefix.endsWith("TOGGLE ")) {
|
|
return [
|
|
new vscode.CompletionItem("off", vscode.CompletionItemKind.TypeParameter),
|
|
new vscode.CompletionItem("on", vscode.CompletionItemKind.TypeParameter)
|
|
]
|
|
}
|
|
|
|
if (linePrefix.endsWith("CATEGORY ")) {
|
|
return categories.map(
|
|
(cat) => new vscode.CompletionItem(cat, vscode.CompletionItemKind.TypeParameter)
|
|
)
|
|
}
|
|
|
|
const categoryPrefix = linePrefix.match(/CATEGORY (.*) /)
|
|
if (categoryPrefix) {
|
|
const usedCategories = categoryPrefix[1].split(" ")
|
|
const remainingCategories = categories.filter((cat) => !usedCategories.includes(cat))
|
|
return remainingCategories.map(
|
|
(cat) => new vscode.CompletionItem(cat, vscode.CompletionItemKind.TypeParameter)
|
|
)
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.Position) {
|
|
const line = document.lineAt(position.line).text
|
|
const eventMatch = /^\s*EVENT\s+([^\s{]+)/.exec(line)
|
|
if (eventMatch) {
|
|
const eventStart = line.indexOf(eventMatch[1], eventMatch.index)
|
|
const declarationEnd = eventStart + eventMatch[1].length
|
|
if (position.character <= declarationEnd) {
|
|
const handler = cdlEventHandlerAtLine(document.getText(), position.line)
|
|
if (handler) {
|
|
const markdown = new vscode.MarkdownString()
|
|
markdown.appendCodeblock(createCdlEventHandlerSnippet(handler), "tcl")
|
|
return new vscode.Hover(
|
|
markdown,
|
|
new vscode.Range(position.line, eventMatch.index, position.line, declarationEnd)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const parameterMatch = /^\s*PARAM\s+([^\s{]+)/.exec(line)
|
|
if (parameterMatch) {
|
|
const parameterStart = line.indexOf(parameterMatch[1], parameterMatch.index)
|
|
const parameterEnd = parameterStart + parameterMatch[1].length
|
|
if (position.character >= parameterStart && position.character <= parameterEnd) {
|
|
return new vscode.Hover(`mom_${parameterMatch[1].replace(/^mom_/i, "")}`)
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
}
|
|
|
|
export function cdlEventAtPosition(
|
|
document: vscode.TextDocument,
|
|
position: vscode.Position
|
|
): string | undefined {
|
|
const line = document.lineAt(position.line).text
|
|
const match = /^\s*EVENT\s+([^\s{]+)/.exec(line)
|
|
if (!match) {
|
|
return undefined
|
|
}
|
|
|
|
const declarationStart = match.index
|
|
const eventEnd = line.indexOf(match[1], match.index) + match[1].length
|
|
if (position.character < declarationStart || position.character > eventEnd) {
|
|
return undefined
|
|
}
|
|
return match[1]
|
|
}
|
|
|
|
export async function definitionCdlEventHandler(
|
|
document: vscode.TextDocument,
|
|
position: vscode.Position,
|
|
token: vscode.CancellationToken
|
|
): Promise<vscode.Location[] | undefined> {
|
|
const eventName = cdlEventAtPosition(document, position)
|
|
if (!eventName) {
|
|
return undefined
|
|
}
|
|
|
|
const handlerName = `MOM_${eventName}`
|
|
try {
|
|
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
|
|
"vscode.executeWorkspaceSymbolProvider",
|
|
handlerName
|
|
)
|
|
const indexedLocations = (symbols || [])
|
|
.filter(
|
|
(symbol) =>
|
|
symbol.kind === vscode.SymbolKind.Function &&
|
|
(symbol.name === handlerName ||
|
|
symbol.name.endsWith(`::${handlerName}`))
|
|
)
|
|
.map((symbol) => symbol.location)
|
|
if (indexedLocations.length > 0) {
|
|
return indexedLocations
|
|
}
|
|
} catch {
|
|
// The Tcl language server may still be starting; use the file fallback below.
|
|
}
|
|
|
|
const declaration = new RegExp(
|
|
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
|
|
)
|
|
const tclFiles = await vscode.workspace.findFiles(
|
|
"**/*.tcl",
|
|
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
|
)
|
|
const locations: vscode.Location[] = []
|
|
|
|
for (const uri of tclFiles) {
|
|
if (token.isCancellationRequested) {
|
|
return undefined
|
|
}
|
|
let tclDocument: vscode.TextDocument
|
|
try {
|
|
tclDocument = await vscode.workspace.openTextDocument(uri)
|
|
} catch {
|
|
continue
|
|
}
|
|
for (let lineNumber = 0; lineNumber < tclDocument.lineCount; lineNumber++) {
|
|
const line = tclDocument.lineAt(lineNumber).text
|
|
const match = declaration.exec(line)
|
|
if (!match) {
|
|
continue
|
|
}
|
|
const start = line.indexOf(handlerName, match.index)
|
|
locations.push(
|
|
new vscode.Location(
|
|
uri,
|
|
new vscode.Range(
|
|
lineNumber,
|
|
start,
|
|
lineNumber,
|
|
start + handlerName.length
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
return locations.length > 0 ? locations : undefined
|
|
}
|
|
|
|
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
|
const symbols: vscode.DocumentSymbol[] = []
|
|
const lines = document.getText().split("\n")
|
|
|
|
interface Scope {
|
|
name: string
|
|
symbol: vscode.DocumentSymbol
|
|
startLine: number
|
|
braceCount: number
|
|
children: Scope[]
|
|
}
|
|
|
|
const rootScope: Scope = {
|
|
name: "",
|
|
symbol: new vscode.DocumentSymbol(
|
|
"root",
|
|
"",
|
|
vscode.SymbolKind.Namespace,
|
|
new vscode.Range(0, 0, lines.length, 0),
|
|
new vscode.Range(0, 0, 0, 0)
|
|
),
|
|
startLine: 0,
|
|
braceCount: 0,
|
|
children: []
|
|
}
|
|
let scopeStack: Scope[] = [rootScope]
|
|
|
|
const namespaceRegex = /^\s*namespace\s+eval\s+([^\s\{]+)/
|
|
const procRegex = /^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{/
|
|
const eventStartRegex = /^\s*LIB_GE_command_buffer_edit_(prepend|append|insert)\b/
|
|
|
|
let pendingEvent: { editType: string; startLine: number } | null = null
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
|
|
const nsMatch = line.match(namespaceRegex)
|
|
const procMatch = line.match(procRegex)
|
|
const eventStartMatch = line.match(eventStartRegex)
|
|
|
|
if (pendingEvent) {
|
|
// Wir suchen nach schließender Klammer mit Eventnamen danach
|
|
const closeMatch = line.match(/^\s*\}\s*(\w+)\s*$/)
|
|
if (closeMatch) {
|
|
const eventName = closeMatch[1]
|
|
const eventSymbol = new vscode.DocumentSymbol(
|
|
eventName,
|
|
`Event (${pendingEvent.editType})`,
|
|
vscode.SymbolKind.Event,
|
|
new vscode.Range(pendingEvent.startLine, 0, i, line.length),
|
|
new vscode.Range(i, 0, i, line.length)
|
|
)
|
|
scopeStack[scopeStack.length - 1].children.push({
|
|
name: eventName,
|
|
symbol: eventSymbol,
|
|
startLine: pendingEvent.startLine,
|
|
braceCount: 0,
|
|
children: []
|
|
})
|
|
pendingEvent = null
|
|
continue // nichts anderes prüfen, wenn wir Event abgeschlossen haben
|
|
}
|
|
}
|
|
|
|
if (eventStartMatch) {
|
|
// Beginn eines Event-Blocks erkannt
|
|
pendingEvent = { editType: eventStartMatch[1], startLine: i }
|
|
continue
|
|
}
|
|
|
|
if (nsMatch) {
|
|
const nsName = nsMatch[1]
|
|
const nsSymbol = new vscode.DocumentSymbol(
|
|
nsName,
|
|
"Namespace",
|
|
vscode.SymbolKind.Namespace,
|
|
new vscode.Range(i, 0, i, line.length),
|
|
new vscode.Range(i, 0, i, line.length)
|
|
)
|
|
const nsScope: Scope = {
|
|
name: nsName,
|
|
symbol: nsSymbol,
|
|
startLine: i,
|
|
braceCount: 0,
|
|
children: []
|
|
}
|
|
scopeStack[scopeStack.length - 1].children.push(nsScope)
|
|
scopeStack.push(nsScope)
|
|
} else if (procMatch) {
|
|
const procName = procMatch[1]
|
|
const procSymbol = new vscode.DocumentSymbol(
|
|
procName,
|
|
"Procedure",
|
|
vscode.SymbolKind.Function,
|
|
new vscode.Range(i, 0, i, line.length),
|
|
new vscode.Range(i, 0, i, line.length)
|
|
)
|
|
scopeStack[scopeStack.length - 1].children.push({
|
|
name: procName,
|
|
symbol: procSymbol,
|
|
startLine: i,
|
|
braceCount: 0,
|
|
children: []
|
|
})
|
|
}
|
|
|
|
// Brace-Balancing für Namespaces und Procs
|
|
const openCount = (line.match(/\{/g) || []).length
|
|
const closeCount = (line.match(/\}/g) || []).length
|
|
|
|
scopeStack[scopeStack.length - 1].braceCount += openCount - closeCount
|
|
|
|
while (scopeStack.length > 1 && scopeStack[scopeStack.length - 1].braceCount <= 0) {
|
|
const finishedScope = scopeStack.pop()
|
|
if (finishedScope) {
|
|
finishedScope.symbol.range = new vscode.Range(
|
|
finishedScope.startLine,
|
|
0,
|
|
i,
|
|
line.length
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectSymbols(scope: Scope): vscode.DocumentSymbol[] {
|
|
for (const child of scope.children) {
|
|
child.symbol.children = collectSymbols(child)
|
|
}
|
|
return scope.children.map((child) => child.symbol)
|
|
}
|
|
|
|
return collectSymbols(rootScope)
|
|
}
|
|
|
|
export function cdlDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
|
const lines = document.getText().split("\n")
|
|
|
|
interface Scope {
|
|
symbol: vscode.DocumentSymbol
|
|
startLine: number
|
|
}
|
|
|
|
const root = new vscode.DocumentSymbol(
|
|
"root",
|
|
"",
|
|
vscode.SymbolKind.Namespace,
|
|
new vscode.Range(0, 0, lines.length, 0),
|
|
new vscode.Range(0, 0, 0, 0)
|
|
)
|
|
root.children = []
|
|
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
|
|
|
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
const trimmed = line.trim()
|
|
|
|
const createSymbol = () => {
|
|
if (!pending) {
|
|
return
|
|
}
|
|
const startRange = new vscode.Range(
|
|
pending.startLine,
|
|
0,
|
|
pending.startLine,
|
|
lines[pending.startLine].length
|
|
)
|
|
const symbol = new vscode.DocumentSymbol(
|
|
pending.name,
|
|
"",
|
|
pending.kind,
|
|
startRange,
|
|
startRange
|
|
)
|
|
symbol.children = []
|
|
stack[stack.length - 1].symbol.children?.push(symbol)
|
|
stack.push({ symbol, startLine: pending.startLine })
|
|
pending = null
|
|
}
|
|
|
|
if (pending && trimmed.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
|
|
if (trimmed.startsWith("}")) {
|
|
const scope = stack.pop()
|
|
if (scope) {
|
|
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
|
}
|
|
continue
|
|
}
|
|
|
|
let match
|
|
if ((match = /^\s*EVENT\s+(\w+)/.exec(line))) {
|
|
pending = {
|
|
name: match[1],
|
|
kind: vscode.SymbolKind.Event,
|
|
startLine: i
|
|
}
|
|
if (line.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
} else if ((match = /^\s*PARAM\s+(\w+)/.exec(line))) {
|
|
pending = {
|
|
name: match[1],
|
|
kind: vscode.SymbolKind.Field,
|
|
startLine: i
|
|
}
|
|
if (line.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
}
|
|
}
|
|
|
|
return root.children || []
|
|
}
|
|
|
|
export function defDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
|
const lines = document.getText().split("\n")
|
|
|
|
interface Scope {
|
|
symbol: vscode.DocumentSymbol
|
|
startLine: number
|
|
}
|
|
|
|
const root = new vscode.DocumentSymbol(
|
|
"root",
|
|
"",
|
|
vscode.SymbolKind.Namespace,
|
|
new vscode.Range(0, 0, lines.length, 0),
|
|
new vscode.Range(0, 0, 0, 0)
|
|
)
|
|
root.children = []
|
|
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
|
|
|
|
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
const trimmed = line.trim()
|
|
|
|
const createSymbol = () => {
|
|
if (!pending) {
|
|
return
|
|
}
|
|
const startRange = new vscode.Range(
|
|
pending.startLine,
|
|
0,
|
|
pending.startLine,
|
|
lines[pending.startLine].length
|
|
)
|
|
const symbol = new vscode.DocumentSymbol(
|
|
pending.name,
|
|
"",
|
|
pending.kind,
|
|
startRange,
|
|
startRange
|
|
)
|
|
symbol.children = []
|
|
stack[stack.length - 1].symbol.children?.push(symbol)
|
|
stack.push({ symbol, startLine: pending.startLine })
|
|
pending = null
|
|
}
|
|
|
|
if (pending && trimmed.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
|
|
if (trimmed.startsWith("}")) {
|
|
const scope = stack.pop()
|
|
if (scope) {
|
|
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
|
|
}
|
|
continue
|
|
}
|
|
|
|
let match
|
|
if ((match = /^\s*FORMATTING\b/.exec(line))) {
|
|
pending = {
|
|
name: "FORMATTING",
|
|
kind: vscode.SymbolKind.Module,
|
|
startLine: i
|
|
}
|
|
if (line.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
} else if ((match = /^\s*ADDRESS\s+(\w+)/.exec(line))) {
|
|
pending = {
|
|
name: match[1],
|
|
kind: vscode.SymbolKind.Field,
|
|
startLine: i
|
|
}
|
|
if (line.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
} else if ((match = /^\s*BLOCK_TEMPLATE\s+(\w+)/.exec(line))) {
|
|
pending = {
|
|
name: match[1],
|
|
kind: vscode.SymbolKind.Function,
|
|
startLine: i
|
|
}
|
|
if (line.includes("{")) {
|
|
createSymbol()
|
|
}
|
|
}
|
|
}
|
|
|
|
return root.children || []
|
|
}
|