diff --git a/README.md b/README.md index efb8993..aaad0ef 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,5 @@ Not everything is supported yet, but currently the following features are suppor - Autocompletion for the layer names - Definition support for the layer names - Definintion support for icons +- Autocompletion for tagRenderings in questions.json +- Definition support for tagRenderings in questions.json diff --git a/src/extension.ts b/src/extension.ts index a6cbcd6..628df51 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,15 @@ import * as vscode from "vscode"; import { layerCompletionProvider, layerDefinitionProvider } from "./theme"; import { iconDefinitionProvider } from "./generic"; +import { tagRenderingCompletionProvider } from "./layers"; export function activate(context: vscode.ExtensionContext) { // Activate all theme related features context.subscriptions.push(layerCompletionProvider, layerDefinitionProvider); + // Activate all layer related features + context.subscriptions.push(tagRenderingCompletionProvider); + // Activate all generic features context.subscriptions.push(iconDefinitionProvider); } diff --git a/src/generic.ts b/src/generic.ts index d4b1a1e..833b590 100644 --- a/src/generic.ts +++ b/src/generic.ts @@ -35,54 +35,41 @@ export const iconDefinitionProvider = const regexes = [/icon$/, /icon.render/, /icon.mappings.\d+.then$/]; - for (const regex of regexes) { - if (regex.exec(jsonPath)) { - const iconPath = getValueFromPath(text, rawJsonPath); - console.log("Found reference to icon", iconPath); + if (regexes.some((regex) => regex.exec(jsonPath))) { + const iconPath = getValueFromPath(text, rawJsonPath); + console.log("Found reference to icon", iconPath); - let fullIconPath: string; + let fullIconPath: string; - // Check if the path starts with a dot, if so, it's a relative path - // if not, it's a built-in icon - if (!iconPath.startsWith(".")) { - fullIconPath = path.join( - (vscode.workspace.workspaceFolders - ? vscode.workspace.workspaceFolders[0].uri.fsPath - : "") || "", - "assets", - "svg", - iconPath + ".svg" - ); - } else { - fullIconPath = path.join( - (vscode.workspace.workspaceFolders - ? vscode.workspace.workspaceFolders[0].uri.fsPath - : "") || "", - iconPath - ); - } - - const startEnd = getStartEnd(text, rawJsonPath); - console.log("fullIconPath", fullIconPath); - console.log( - "startEndLines", - startEnd.start.line, - startEnd.end.line + // Check if the path starts with a dot, if so, it's a relative path + // if not, it's a built-in icon + if (!iconPath.startsWith(".")) { + fullIconPath = path.join( + (vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri.fsPath + : "") || "", + "assets", + "svg", + iconPath + ".svg" ); - console.log( - "startEndChars", - startEnd.start.character, - startEnd.end.character + } else { + fullIconPath = path.join( + (vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri.fsPath + : "") || "", + iconPath ); - - const link: vscode.DefinitionLink = { - targetUri: vscode.Uri.file(fullIconPath), - targetRange: new vscode.Range(0, 0, 0, 0), - originSelectionRange: startEnd, - }; - - return [link]; } + + const startEnd = getStartEnd(text, rawJsonPath); + + const link: vscode.DefinitionLink = { + targetUri: vscode.Uri.file(fullIconPath), + targetRange: new vscode.Range(0, 0, 0, 0), + originSelectionRange: startEnd, + }; + + return [link]; } return []; diff --git a/src/layers.ts b/src/layers.ts new file mode 100644 index 0000000..168f5a5 --- /dev/null +++ b/src/layers.ts @@ -0,0 +1,156 @@ +/** + * This file contains all functions that should be used when editing layers + * Layer files are located in the /assets/layers/{LAYER_NAME}.json file + * This is also loaded when editing the theme files, as layers can be inline in the theme files + * + * This consists of the following functions: + * - tagRenderingCompletionProvider: Provides a list of existing tag renderings for autocompletion + * - tagRenderingDefinitionProvider: Provides a definition for tag renderings, allowing users to jump to the tag rendering definition + */ + +import * as vscode from "vscode"; +import { + getCursorPath, + getRawCursorPath, + getStartEnd, + getTagRenderings, + getValueFromPath, +} from "./utils"; +import { JSONPath } from "jsonc-parser"; + +export const tagRenderingCompletionProvider = + vscode.languages.registerCompletionItemProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/layers/*/*.json", + }, + { + async provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position + ) { + // Stop running if the file is called license_info.json + if (document.fileName.includes("license_info")) { + return []; + } + + console.log("tagRenderingCompletionProvider"); + const text = document.getText(); + const jsonPath = getCursorPath(text, position); + + const regex = /^(layers.\d+.)?tagRenderings\.\d+(.builtin)?$/; + if (regex.exec(jsonPath)) { + const tagRenderings = await getTagRenderings(); + console.log(`Got ${tagRenderings.length} tagRenderings`); + + // Now we need to return the completion items + return tagRenderings; + } + + return []; + }, + } + ); + +/** + * Tag rendering definition provider + */ +export const tagRenderingDefinitionProvider = + vscode.languages.registerDefinitionProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/layers/*/*.json", + }, + { + async provideDefinition( + document: vscode.TextDocument, + position: vscode.Position + ) { + console.log("tagRenderingDefinitionProvider"); + const text = document.getText(); + const jsonPath = getCursorPath(text, position); + const rawJsonPath = getRawCursorPath(text, position); + + const regex = /^(layers.\d.)?tagRenderings.\d*(.builtin)?$/; + + if (regex.exec(jsonPath)) { + const tagRendering = getValueFromPath(text, rawJsonPath); + + if (typeof tagRendering === "string") { + console.log("Found reference to tagRendering", tagRendering); + if (tagRendering.indexOf(".") === -1) { + console.log("This is a built-in tag rendering"); + // This is a built-in tag rendering + // Read the built-in tag renderings file + const layerFile = await vscode.workspace.findFiles( + "assets/layers/questions/questions.json" + ); + if (layerFile.length === 0) { + return null; + } + + const layerText = await vscode.workspace.fs.readFile( + layerFile[0] + ); + const layerTextString = new TextDecoder().decode(layerText); + const layer = JSON.parse(layerTextString); + + const tagRenderingIndex = layer.tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => tr.id === tagRendering + ); + + const path: JSONPath = ["tagRenderings", tagRenderingIndex]; + const startEnd = getStartEnd(layerTextString, path); + + const link: vscode.DefinitionLink = { + targetUri: layerFile[0], + targetRange: startEnd, + originSelectionRange: getStartEnd(text, rawJsonPath), + }; + + return [link]; + } else { + // This is a reference to a tag rendering in another layer + // We need to find the layer and the tag rendering + const layerName = tagRendering.split(".")[0]; + const tagRenderingName = tagRendering.split(".")[1]; + + const layerFile = await vscode.workspace.findFiles( + `assets/layers/${layerName}/${layerName}.json` + ); + if (layerFile.length === 0) { + return null; + } + + const layerText = await vscode.workspace.fs.readFile( + layerFile[0] + ); + const layerTextString = new TextDecoder().decode(layerText); + const layer = JSON.parse(layerTextString); + + const tagRenderingIndex = layer.tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => tr.id === tagRenderingName + ); + + const path: JSONPath = ["tagRenderings", tagRenderingIndex]; + const startEnd = getStartEnd(layerTextString, path); + + const link: vscode.DefinitionLink = { + targetUri: layerFile[0], + targetRange: startEnd, + originSelectionRange: getStartEnd(text, rawJsonPath), + }; + + return [link]; + } + } + } + + return null; + }, + } + ); diff --git a/src/theme.ts b/src/theme.ts index 4aed098..8f8739a 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,5 +1,6 @@ /** - * This file contains all functions that should be used when editing theme files + * This file contains all functions that should mainly be used when editing theme files + * All of these are related to looking for layer references in the theme files, but some layers also reference other layers * Theme files are located in the /assets/themes/{THEME_NAME}/{THEME_NAME}.json file * * This consists of the following functions: @@ -9,7 +10,12 @@ import * as vscode from "vscode"; import * as path from "path"; -import { getAvailableLayers, getCursorPath } from "./utils"; +import { + getAvailableLayers, + getCursorPath, + getRawCursorPath, + getValueFromPath, +} from "./utils"; /** * Layer completion provider @@ -25,22 +31,27 @@ export const layerCompletionProvider = { language: "json", scheme: "file", - pattern: "**/assets/themes/*/*.json", + pattern: "**/assets/*/*/*.json", }, { async provideCompletionItems( document: vscode.TextDocument, position: vscode.Position ) { - console.log("layerCompletionProvider"); - // Now we'll need to try and get the current path for the cursor - const text = document.getText(); + // Stop running if the file is called license_info.json + if (document.fileName.includes("license_info")) { + return []; + } - // Now we need to get the current path + console.log("layerCompletionProvider"); + const text = document.getText(); const jsonPath = getCursorPath(text, position); - const regex = /^layers\.\d+(.builtin)*$/; - if (regex.exec(jsonPath)) { + const regexes = [ + /^layers\.\d+(.builtin)?$/, + /^(layers.\d+.)?presets.\d+.snapToLayer(.\d)*$/, + ]; + if (regexes.some((regex) => regex.exec(jsonPath))) { // We need to get the available layers const layers = await getAvailableLayers(); console.log(`Got ${layers.length} layers`); @@ -70,33 +81,38 @@ export const layerCompletionProvider = * JSON paths: * - layers.{index} (If this is a string) * - layers.{index}.builtin + * - (layers.{index}.)presets.{index}.snapToLayer(.{index}) */ export const layerDefinitionProvider = vscode.languages.registerDefinitionProvider( { language: "json", scheme: "file", - pattern: "**/assets/themes/*/*.json", + pattern: "**/assets/*/*/*.json", }, { provideDefinition( document: vscode.TextDocument, position: vscode.Position ) { - console.log("layerDefinitionProvider"); - - // We don't want to provide definitions for the license_info.json file - if (document.fileName.endsWith("license_info.json")) { - return null; + // Stop running if the file is called license_info.json + if (document.fileName.includes("license_info")) { + return []; } + console.log("layerDefinitionProvider"); + const text = document.getText(); const jsonPath = getCursorPath(text, position); - const json = JSON.parse(text); + const rawJsonPath = getRawCursorPath(text, position); - const regex = /^layers\.\d+(.builtin)*$/; - if (regex.exec(jsonPath)) { - const layer = json.layers[parseInt(jsonPath.split(".")[1])]; + const regexes = [ + /^layers\.\d+(.builtin)?$/, + /^(layers.\d+.)?presets.\d+.snapToLayer(.\d)*$/, + ]; + if (regexes.some((regex) => regex.exec(jsonPath))) { + console.log("Found reference to layer"); + const layer = getValueFromPath(text, rawJsonPath); // If an item is a string, it's a reference to a layer, also if it's an object and has a builtin property if (typeof layer === "string") { // We have a reference to a layer diff --git a/src/utils.ts b/src/utils.ts index 2f53dea..31e8107 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,3 +152,38 @@ export function getValueFromPath(json: string, path: JSONPath): any { } } } + +/** + * Utility function to get the tagRenderings from the questions layer + * //TODO: This should get ALL tagRenderings, not just from the questions layer + * + * @returns List of CompletionItems for tagRenderings + */ +export async function getTagRenderings(): Promise { + const tagRenderings: vscode.CompletionItem[] = []; + + // Open the questions layer file + const questionsFile = await vscode.workspace.findFiles( + "assets/layers/questions/questions.json", + "**/node_modules/**" + ); + + if (questionsFile.length === 0) { + console.error("questions.json not found"); + return []; + } + + const content = await vscode.workspace.fs.readFile(questionsFile[0]); + const questions = JSON.parse(new TextDecoder().decode(content)); + + for (const tagRendering of questions.tagRenderings) { + tagRenderings.push( + new vscode.CompletionItem( + tagRendering.id, + vscode.CompletionItemKind.Value + ) + ); + } + + return tagRenderings; +}