diff --git a/src/extension.ts b/src/extension.ts index a9ab619..d9ab2fd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,8 @@ import { ZqInstance } from './zq/ZqInstance'; import { ZqInstanceReference } from './zq/ZqInstanceReference'; import { ZqObject } from './zq/ZqObject'; import { ZqParser } from './zq/parser/ZqParser'; +import { ZqObjectDefinition } from './zq/ZqObjectDefinition'; +import { ZqlCodeActionsProvider } from './zqlEditor/ZqlCodeActionsProvider'; const cp = require('child_process'); @@ -230,6 +232,8 @@ export function openWebBrowser(url: vscode.Uri) { // Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { + registerZqlEditorSupport(context); + outputChannel = vscode.window.createOutputChannel("Mocha"); outputChannel.appendLine("Mocha for VSCode activated"); @@ -278,9 +282,18 @@ export function activate(context: vscode.ExtensionContext) { let decl = ZqParser.parse(line); if (decl !== null) { - (decl as ZqFunctionDefinition).isStub = false; + if ((decl as ZqFunctionDefinition).isStub) { + + (decl as ZqFunctionDefinition).isStub = false; - ed.edit(edit => edit.replace(currentLineRange, decl.toString())); + ed.edit(edit => edit.replace(currentLineRange, decl.toString())); + } + else { + vscode.window.showErrorMessage("This ZQ function already has an implementation!"); + } + } + else { + vscode.window.showErrorMessage("Please place the cursor on a ZQ function before importing its definition!"); } } } @@ -379,7 +392,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(cmd_suv_manager); let cmd_suv_refresh = vscode.commands.registerCommand('mocha.suvmanager.suv_refresh', () => { - vscode.window.showInformationMessage('Add code here to display SUV Manager panel!'); + + treeDataProvider.refresh(); }); context.subscriptions.push(cmd_suv_refresh); @@ -500,16 +514,38 @@ export function activate(context: vscode.ExtensionContext) { }); context.subscriptions.push(cmd_suv_up); - const relationshipDecorations: vscode.TextEditorDecorationType[] = []; + let relationshipDecorations: vscode.TextEditorDecorationType[] = []; + + const noAttrsDecorationType = vscode.window.createTextEditorDecorationType({ + before: + { + contentText: "", + margin: "0px 4px 0px 16px", + color: "#999999" + } + }); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => { if (e.textEditor.document.languageId === "zql") { let clz = ZqParser.parse(e.textEditor.document.getText()) as ZqClass; if (clz !== null) { + if (clz.attributes.length === 0) { + let attrsDef : ZqObjectDefinition | undefined = clz.sectionDefinitions.get('attributes'); + if (attrsDef !== undefined) { + e.textEditor.setDecorations(noAttrsDecorationType, [ + new vscode.Range(attrsDef.lineIndex + 1 + 2/*hack*/, 0, + attrsDef.lineIndex + 1 + 2 /*hack*/, 0) + ]); + } + } else { + e.textEditor.setDecorations(noAttrsDecorationType, []); + } + relationshipDecorations.forEach(element => { e.textEditor.setDecorations(element, []); }); + relationshipDecorations = []; clz.relationships.forEach(rel => { let n = ""; @@ -630,6 +666,25 @@ function indentLines(count: number, lines: string[]): string { retval += indent; return retval; } + +function registerZqlEditorSupport(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider('zql', new ZqlCodeActionsProvider(), { + providedCodeActionKinds: ZqlCodeActionsProvider.providedCodeActionKinds + })); + + // const emojiDiagnostics = vscode.languages.createDiagnosticCollection("emoji"); + // context.subscriptions.push(emojiDiagnostics); + + //subscribeToDocumentChanges(context, emojiDiagnostics); + /* + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider('zql', new Emojinfo(), { + providedCodeActionKinds: Emojinfo.providedCodeActionKinds + }) + ); + */ +} /* // This method is called when your extension is activated diff --git a/src/zq/ZqClass.ts b/src/zq/ZqClass.ts index d6952b8..945cf75 100644 --- a/src/zq/ZqClass.ts +++ b/src/zq/ZqClass.ts @@ -16,8 +16,15 @@ // along with mocha-vscode. If not, see . import { ZqInstance } from "./ZqInstance"; +import { ZqObjectDefinition } from "./ZqObjectDefinition"; export class ZqClass extends ZqInstance { + + private _sectionDefinitions: Map = new Map(); + public get sectionDefinitions() : Map { + return this._sectionDefinitions; + } + toString(): string { diff --git a/src/zq/parser/ZqParser.ts b/src/zq/parser/ZqParser.ts index 328b066..adcc83e 100644 --- a/src/zq/parser/ZqParser.ts +++ b/src/zq/parser/ZqParser.ts @@ -22,12 +22,40 @@ import { ZqClass } from "../ZqClass"; import { ZqFunctionDefinition } from "../ZqFunctionDefinition"; import { ZqInstanceReference } from "../ZqInstanceReference"; import { ZqObject } from "../ZqObject"; +import { ZqObjectDefinition } from "../ZqObjectDefinition"; import { ZqRelationship } from "../ZqRelationship"; import { ZqParserContext } from "./ZqParserContext"; import { ZqTokenInfo } from "./ZqTokenInfo"; export class ZqParser { + public static readonly instanceDeclarationRegExp = new RegExp("\\s*(\\w*)\\s*(?:,\\s*(\\d*\\$\\d*))?\\s*"); + public static readonly attributeDeclarationRegExp = new RegExp("\\s*(\\w*)\\s*(?:,\\s*(\\d*\\$\\d*))?\\s*(?:\\:\\s*(\\w*))?\\s*"); + + static parseInstanceDeclaration(lineText: string): ZqInstanceReference | null { + let result = ZqParser.instanceDeclarationRegExp.exec(lineText); + + let name = result?.at(1); + let instanceKey = result?.at(2); + + if (name === undefined) { + return null; + } + return new ZqInstanceReference(name === undefined ? "" : name, instanceKey === undefined ? null : InstanceKey.parse(instanceKey)); + } + static parseAttributeDeclaration(lineText: string): ZqAttribute | null { + let result = ZqParser.attributeDeclarationRegExp.exec(lineText); + + let name = result?.at(1); + let instanceKey = result?.at(2); + let dataType = result?.at(3); + + if (name === undefined) { + return null; + } + return new ZqAttribute(name === undefined ? "" : name, instanceKey === undefined ? null : InstanceKey.parse(instanceKey), dataType === undefined ? null : dataType); + } + static parse(line: string): ZqObject { let idx: number = 0; @@ -71,12 +99,14 @@ export class ZqParser { obj.isStatic = nextStatic; obj.isStub = nextStub; - obj.name = line.substring(token.nextStart, line.indexOf('(', token.nextStart)).trim(); - token.nextStart = token.nextStart + obj.name.length + 1; + let name = result?.at(5); + if (name !== undefined) { + obj.name = name; + } + token.nextStart = token.nextStart + line.length + 1; retval = obj; + break; } - - line = line.substring(token.nextStart).trim(); // idx = token.NextStart; idx = 0; } @@ -112,21 +142,15 @@ function parseClass(line: string, NextStart: number) { while (!ctx.endOfStream) { if (section === "attributes") { - let line = ctx.readLine(); + let line = ctx.readLine().trim(); if (line === '') { section = ""; continue; } - let re = new RegExp("\\s*(\\w*)\\s*(?:,\\s*(\\d*\\$\\d*))\\s*:\\s*(\\w*)\\s*"); - let result = re.exec(line); - - let name = result?.at(1); - let instanceKey = result?.at(2); - let dataType = result?.at(3); - - if (name !== undefined && instanceKey !== undefined && dataType !== undefined) { - obj.attributes.push(new ZqAttribute(name, InstanceKey.parse(instanceKey), dataType)); + let result = ZqParser.parseAttributeDeclaration(line); + if (result !== null) { + obj.attributes.push(result); } } else if (section === "relationships" || section === "instances") { @@ -161,10 +185,15 @@ function parseClass(line: string, NextStart: number) { } } else if (section === "") { + while (ctx.peekLine().trim() === '') { + ctx.readLine(); + } + let tok = ctx.readToken(); if (tok !== null) { if (tok.token === ":") { section = tok.value; + obj.sectionDefinitions.set(section, new ZqObjectDefinition(ctx.lineIndex, ctx.columnIndex)); ctx.readLine(); } } diff --git a/src/zq/parser/ZqParserContext.ts b/src/zq/parser/ZqParserContext.ts index 42fad66..c57a28b 100644 --- a/src/zq/parser/ZqParserContext.ts +++ b/src/zq/parser/ZqParserContext.ts @@ -83,6 +83,17 @@ export class ZqParserContext { return before; } + peekLine() : string { + + let idxNewline = this._value.indexOf("\n", this._currentIndex); + let retval = this._value.substring(this._currentIndex); + + if (idxNewline > -1) { + retval = this._value.substring(this._currentIndex, idxNewline); + } + return retval; + } + readToken(): ZqTokenInfo | null { let end = this._value.length; diff --git a/src/zqlEditor/ZqlCodeActionsProvider.ts b/src/zqlEditor/ZqlCodeActionsProvider.ts new file mode 100644 index 0000000..897db64 --- /dev/null +++ b/src/zqlEditor/ZqlCodeActionsProvider.ts @@ -0,0 +1,111 @@ +// Copyright (C) 2025 Michael Becker +// +// This file is part of mocha-vscode. +// +// mocha-vscode is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// mocha-vscode is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with mocha-vscode. If not, see . + +import * as vscode from 'vscode'; +import { InstanceKey } from '../mocha/core/InstanceKey'; +import { ZqParser } from '../zq/parser/ZqParser'; +import { ZqClass } from '../zq/ZqClass'; + +export class ZqlCodeActionsProvider implements vscode.CodeActionProvider { + + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix + ]; + + private createIidImportFix(document: vscode.TextDocument, range: vscode.Range, type : string, instanceId: InstanceKey): vscode.CodeAction { + const fix = new vscode.CodeAction("Complete instance declaration from Mocha", vscode.CodeActionKind.QuickFix); + fix.edit = new vscode.WorkspaceEdit(); + + let line = document.lineAt(range.start); + let lineText = line.text.trim(); + + if (type === "attribute") { + let attr = ZqParser.parseAttributeDeclaration(lineText); + if (attr !== null) { + if (attr.instanceKey === null) { + attr.instanceKey = InstanceKey.parse(instanceId.toString()); + } + if (attr.dataType === null) { + attr.dataType = "text"; + } + + fix.edit.replace(document.uri, line.range, " " + attr.toString()); + } + } + else if (type === "instance") { + let inst = ZqParser.parseInstanceDeclaration(lineText); + if (inst !== null) { + if (inst.instanceKey === null) { + inst.instanceKey = InstanceKey.parse(instanceId.toString()); + } + + fix.edit.replace(document.uri, line.range, " " + inst.toString()); + } + } + return fix; + } + + public provideCodeActions(document: vscode.TextDocument, range: vscode.Range): vscode.CodeAction[] | undefined { + + let sectionName = ""; + let lineIndex = range.start.line; + while (lineIndex > 0) { + let szline = document.lineAt(new vscode.Position(lineIndex, 0)); + if (szline.text.endsWith(':')) { + sectionName = szline.text.substring(0, szline.text.length - 1); + break; + } + lineIndex--; + } + + let ary : vscode.CodeAction[] = []; + + let line = document.lineAt(range.start).text.trim(); + if (line !== sectionName + ":") { + + if (sectionName === "attributes") { + // parse attribute declaration + let result = ZqParser.parseAttributeDeclaration(line); + if (result !== null) { + if (result.instanceKey === null || result.dataType === null) { + const fixImportIidFromMocha = this.createIidImportFix(document, range, "attribute", new InstanceKey(1, 1234)); + ary.push(fixImportIidFromMocha); + } + } + } + else if (sectionName === "instances") { + // parse attribute declaration + let result = ZqParser.parseInstanceDeclaration(line); + if (result !== null) { + if (result.instanceKey === null) { + const fixImportIidFromMocha = this.createIidImportFix(document, range, "instance", new InstanceKey(1, 1234)); + ary.push(fixImportIidFromMocha); + } + } + } + } + + /* + const COMMAND = 'mocha.suvManager.suv_select'; + const commandAction = new vscode.CodeAction('Learn more...', vscode.CodeActionKind.Empty); + commandAction.command = { command: COMMAND, title: 'Learn more about emojis', tooltip: 'This will open the unicode emoji page.' }; + + ary.push(commandAction); + */ + return ary; + } +} \ No newline at end of file