From b61270e51e59f889de38d336bb5caf0ab80559a1 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 7 Jan 2025 00:02:33 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=9A=A7=20Experiment=20with=20cache=20?= =?UTF-8?q?for=20all=20filters=20and=20tagRenderings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 9 +- src/utils/cache.ts | 450 +++++++++++++++++++++++++++++++++++++++ src/utils/mapcomplete.ts | 119 +++++++---- 3 files changed, 532 insertions(+), 46 deletions(-) create mode 100644 src/utils/cache.ts diff --git a/src/extension.ts b/src/extension.ts index cf0b5d1..da2b645 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,8 +8,9 @@ import { tagRenderingDefinitionProvider, } from "./layers"; import { pathDefinitionProvider } from "./license_info"; +import { CacheWorker } from "./utils/cache"; -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { // Activate all theme related features context.subscriptions.push(layerCompletionProvider, layerDefinitionProvider); @@ -26,4 +27,10 @@ export function activate(context: vscode.ExtensionContext) { // Activate all generic features context.subscriptions.push(iconDefinitionProvider, colorProvider); + + // Upon activation, we also scan the workspace for all themes and layers + // and save them in a cache, so we can quickly look up definitions and completions + // for each theme and layer + // We should also listen for changes in the workspace, so we can update the cache + CacheWorker.create(context); } diff --git a/src/utils/cache.ts b/src/utils/cache.ts new file mode 100644 index 0000000..db06044 --- /dev/null +++ b/src/utils/cache.ts @@ -0,0 +1,450 @@ +/** + * This file contains all functions related to caching the workspace + */ + +import * as vscode from "vscode"; +import { JSONPath } from "jsonc-parser"; + +/** + * Worker class to handle the cache creation and updates + */ +export class CacheWorker { + /** + * Creates a new cache + * + * @param context The extension context + * @returns Promise The cache + */ + public static async create( + context: vscode.ExtensionContext + ): Promise { + const cache = new CacheWorker(context); + await cache.initialize(); + return cache; + } + + private async initialize() { + await this.scanWorkspace(); + } + private readonly context: vscode.ExtensionContext; + private cache: CacheItem[] = []; + + /** + * Saves the current cache to the storage as JSON + */ + private save() { + const jsonString = JSON.stringify(this.cache); + // Save it in the cache.json file in the .cache folder in the workspace + const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + if (workspaceFolder) { + const cacheUri = vscode.Uri.file(`${workspaceFolder}/.cache/cache.json`); + vscode.workspace.fs.writeFile(cacheUri, Buffer.from(jsonString)); + } else { + console.error("No workspace folder found"); + } + } + + /** + * Create a new cache + * + * @param context The extension context + */ + constructor(context: vscode.ExtensionContext) { + this.context = context; + // We probably want to create a fileSystemWatcher here + // to listen for changes in the workspace + this.createFileSystemWatcher(); + } + + /** + * Create a file system watcher + */ + private createFileSystemWatcher() { + const watcher = vscode.workspace.createFileSystemWatcher( + "**/assets/**/*.json" + ); + watcher.onDidChange(this.onChanged, this, this.context.subscriptions); + watcher.onDidCreate(this.onCreated, this, this.context.subscriptions); + watcher.onDidDelete(this.onDeleted, this, this.context.subscriptions); + } + + private onChanged(uri: vscode.Uri) { + this.saveFileToCache(uri); + } + + private onCreated(uri: vscode.Uri) { + this.saveFileToCache(uri); + } + + private onDeleted(uri: vscode.Uri) { + this.deleteFileFromCache(uri); + } + + /** + * Scans the workspace for all themes and layers + */ + private async scanWorkspace() { + const files = await vscode.workspace.findFiles("**/assets/**/*.json"); + for (const file of files) { + this.saveFileToCache(file); + } + } + + /** + * Scans a file, extracts all relevant information and saves it to the cache + * + * @param uri File URI + */ + private saveFileToCache(uri: vscode.Uri) { + // First, we determine what kind of file it is, and whether we actually care about it + // If we don't care about it, we return early + // Possible relevant patterns: + // - ./assets/themes/{name}/{name}.json + // - ./assets/layers/{name}/{name}.json + + const filePath = uri.fsPath; + const regex = /\/assets\/(?themes|layers)\/([^/]+)\/\2\.json/; + const match = regex.exec(filePath); + const asset = match?.groups?.asset; + if (!match) { + return; + } else { + console.log("We care about this file", filePath); + this.deleteFileFromCache(uri); + + // Determine what kind of file we're dealing with + switch (asset) { + case "themes": + console.log("Theme found", filePath); + this.saveThemeToCache(uri); + break; + case "layers": + console.log("Layer found", filePath); + this.saveLayerToCache(uri); + break; + default: + console.error("Unknown asset type", filePath); + break; + } + } + } + + /** + * Deletes a file from the cache + * + * @param uri File URI + */ + private deleteFileFromCache(uri: vscode.Uri) { + this.cache = this.cache.filter( + (item) => item.filePath.fsPath !== uri.fsPath + ); + + this.save(); + } + + /** + * Saves a theme to the cache + * + * @param filePath The file path + */ + private async saveThemeToCache(uri: vscode.Uri) { + const filePath = uri.fsPath; + console.log("Saving theme to cache", filePath); + /** + * For now, we only care about the layer references in the theme + */ + + // Read the file + const content = vscode.workspace.fs.readFile(uri); + const text = new TextDecoder().decode(await content); + const json = JSON.parse(text); + + // Look through the layers + for (const layer of json.layers) { + // Check if it is a string and not an object + if (typeof layer === "string") { + // It is a reference + console.log(`Reference found to ${layer} in ${filePath}`); + + const from = `themes.${json.id}`; + const to = layer.includes(".") + ? `themes.${layer.split(".")[0]}layers.${layer.split(".")[1]}` + : `layers.${layer}`; + + this.cache.push({ + id: layer, + filePath: uri, + jsonPath: ["layers"], + type: "reference", + reference: { + from, + to, + type: "layer", + }, + }); + } + } + + this.save(); + this.printCache(); + } + + /** + * Saves a layer to the cache + * + * @param uri The URI of the layer + */ + private async saveLayerToCache(uri: vscode.Uri) { + // Read the file + const content = vscode.workspace.fs.readFile(uri); + const text = new TextDecoder().decode(await content); + const uriPath = uri.path; + const uriPathSplit = uriPath.split("/"); + const uriFileName = uriPathSplit[uriPathSplit.length - 1]; + const from = `layers.${uriFileName.split(".")[0]}`; + + this.saveLayerTextToCache(text, uri, from); + } + + /** + * Save a layer to the cache, given the text of the layer + * + * @param text The text of the layer + * @param uri The URI of the layer file + * @param from The theme or layer where the layer is from, e.g. layers.bicycle_rental or themes.cyclofix.layers.0 + */ + private saveLayerTextToCache(text: string, uri: vscode.Uri, from: string) { + const filePath = uri.fsPath; + console.log("Saving layer to cache", filePath); + /** + * For now, we only care about the tagRenderings and filters in the layer + */ + const json = JSON.parse(text); + + // Look through the tagRenderings, if the layer has any + if (json.tagRenderings) { + for (const tagRendering of json.tagRenderings) { + // Check if it is a string and not an object + if (typeof tagRendering === "string") { + // It is a reference + console.log(`Reference found to ${tagRendering} in ${filePath}`); + + const to = tagRendering.includes(".") + ? `layers.${tagRendering.split(".")[0]}.tagRenderings.${ + tagRendering.split(".")[1] + }` + : `layers.questions.tagRenderings.${tagRendering}`; + + this.cache.push({ + id: tagRendering, + filePath: uri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from, + to, + type: "tagRendering", + }, + }); + } else if (typeof tagRendering === "object") { + // This is a tagRendering, which can be reused + console.log(`TagRendering found in ${filePath}`); + this.cache.push({ + id: `${json.id}.${tagRendering.id}`, + filePath: uri, + jsonPath: ["tagRenderings"], + type: "tagRendering", + }); + } + } + } else { + console.log("No tagRenderings found in", filePath); + } + + if (json.filter) { + // Look through the filters + for (const filter of json.filter) { + // Check if it is a string and not an object + if (typeof filter === "string") { + // It is a reference + console.log(`Reference found to ${filter} in ${filePath}`); + + const from = `layers.${json.id}`; + const to = `layers.${filter}`; + + this.cache.push({ + id: filter, + filePath: uri, + jsonPath: ["filters"], + type: "reference", + reference: { + from, + to, + type: "filter", + }, + }); + } else if (typeof filter === "object") { + // This is a filter, which can be reused + console.log(`Filter found in ${filePath}`); + this.cache.push({ + id: `${json.id}.${filter.id}`, + filePath: uri, + jsonPath: ["filters"], + type: "filter", + }); + } + } + } else { + console.log("No filters found in", filePath); + } + + this.save(); + this.printCache(); + } + + /** + * Print the current cache state + */ + public printCache() { + console.log("Current cache state:", this.cache); + } +} + +/** + * Cache for interacting with the cache + */ +export class Cache { + private cache: CacheItem[] = []; + + public static async create() { + const cache = new Cache(); + await cache.loadCache(); + return cache; + } + + private async loadCache() { + // Get the cache from the .cache/cache.json file in the workspace folder + const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + if (workspaceFolder) { + const cacheUri = vscode.Uri.file(`${workspaceFolder}/.cache/cache.json`); + const cache = await vscode.workspace.fs.readFile(cacheUri); + const cacheString = new TextDecoder().decode(cache); + this.cache = JSON.parse(cacheString); + console.log(`Cache loaded, ${this.cache.length} items`); + } else { + console.error("No workspace folder found"); + throw new Error("No workspace folder found"); + } + } + + /** + * Get all tag renderings from the cache + * + * @returns List of CompletionItems for tagRenderings + */ + public getTagRenderings(): vscode.CompletionItem[] { + console.log("Getting tag renderings from cache"); + const tagRenderings: vscode.CompletionItem[] = []; + for (const item of this.cache) { + if (item.type === "tagRendering") { + if (item.id.startsWith("questions.")) { + const completionItem = new vscode.CompletionItem( + item.id.replace("questions.", ""), + vscode.CompletionItemKind.Value + ); + // To give built-in tagRenderings a higher priority, we sort them to the top + completionItem.sortText = `#${item.id}`; + tagRenderings.push(completionItem); + } else { + tagRenderings.push( + new vscode.CompletionItem(item.id, vscode.CompletionItemKind.Value) + ); + } + } + } + return tagRenderings; + } + + /** + * Get all filters from the cache + */ + public getFilters(): vscode.CompletionItem[] { + console.log("Getting filters from cache"); + const filters: vscode.CompletionItem[] = []; + for (const item of this.cache) { + if (item.type === "filter") { + if (item.id.startsWith("filters.")) { + const completionItem = new vscode.CompletionItem( + item.id.replace("filters.", ""), + vscode.CompletionItemKind.Value + ); + // To give built-in filters a higher priority, we sort them to the top + completionItem.sortText = `#${item.id}`; + filters.push(completionItem); + } else { + filters.push( + new vscode.CompletionItem(item.id, vscode.CompletionItemKind.Value) + ); + } + } + } + return filters; + } +} + +interface CacheItem { + /** + * Where the item is defined in the workspace + */ + filePath: vscode.Uri; + + /** + * The JSON path to the item in the file + */ + jsonPath: JSONPath; + + /** + * What kind of item it is + */ + type: "tagRendering" | "filter" | "layer" | "reference"; + + /** + * The ID of the item + * + * When we need to reuse an items in a configuration, we can use the ID + * This does mean the ID definitely doesn't have to be unique, because when this is a reference, + * the ID is the same as the reference ID and items can be reused by multiple themes or layers + */ + id: string; + + /** + * In case of a reference, this contains the reference + */ + reference?: Reference; +} + +/** + * A reference between two items + */ +interface Reference { + /** + * The theme or layer where the reference is from + * + * @example themes.cyclofix + */ + from: string; + + /** + * The path of the file where the reference points to + * This can also be more specific, like a tagRendering or filter + * + * @example layers.bicycle_rental + * @example layers.questions.tagRenderings.name + */ + to: string; + + /** + * The type of item being referenced/reused + */ + type: "tagRendering" | "filter" | "layer"; +} diff --git a/src/utils/mapcomplete.ts b/src/utils/mapcomplete.ts index 2abf840..8a6ec55 100644 --- a/src/utils/mapcomplete.ts +++ b/src/utils/mapcomplete.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; import * as path from "path"; +import { Cache } from "./cache"; /** * Function to get all available layers on disk @@ -33,67 +34,95 @@ export async function getAvailableLayers(): Promise { /** * Utility function to get the tagRenderings from the questions layer - * //TODO: This should get ALL tagRenderings, not just from the questions layer + * + * This function first tries to get all of the tagRenderings from the cache, and if that fails, it falls back to the questions.json file * * @returns List of CompletionItems for tagRenderings */ export async function getTagRenderings(): Promise { - const tagRenderings: vscode.CompletionItem[] = []; + // First, we try to get the tagRenderings from the cache + // If the cache is not available, instead return the tagRenderings from the questions layer - // 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 - ) + try { + const cache = await Cache.create(); + return cache.getTagRenderings(); + } catch (error) { + console.error( + "Error getting tagRenderings from cache, falling back to questions.json", + error ); - } - return tagRenderings; + 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; + } } /** * Utility function to get the filters from the filters layer - * //TODO: This should get ALL filters, not just from the filters layer + * + * This function first tries to get all of the filters from the cache, and if that fails, it falls back to the filters.json file * * @returns List of CompletionItems for tagRenderings */ export async function getFilters(): Promise { - const filtersList: vscode.CompletionItem[] = []; + // First, we try to get the filters from the cache + // If the cache is not available, instead return the filters from the filters layer - // Open the filters layer file - const filtersFile = await vscode.workspace.findFiles( - "assets/layers/filters/filters.json", - "**/node_modules/**" - ); - - if (filtersFile.length === 0) { - console.error("filters.json not found"); - return []; - } - - const content = await vscode.workspace.fs.readFile(filtersFile[0]); - const filters = JSON.parse(new TextDecoder().decode(content)); - - for (const filter of filters.filter) { - filtersList.push( - new vscode.CompletionItem(filter.id, vscode.CompletionItemKind.Value) + try { + const cache = await Cache.create(); + return cache.getFilters(); + } catch (error) { + console.error( + "Error getting filters from cache, falling back to filters.json", + error ); - } - return filtersList; + const filtersList: vscode.CompletionItem[] = []; + + // Open the filters layer file + const filtersFile = await vscode.workspace.findFiles( + "assets/layers/filters/filters.json", + "**/node_modules/**" + ); + + if (filtersFile.length === 0) { + console.error("filters.json not found"); + return []; + } + + const content = await vscode.workspace.fs.readFile(filtersFile[0]); + const filters = JSON.parse(new TextDecoder().decode(content)); + + for (const filter of filters.filter) { + filtersList.push( + new vscode.CompletionItem(filter.id, vscode.CompletionItemKind.Value) + ); + } + + return filtersList; + } } From b7ebfee84dec0c899aac4535f54806c6ca5a6945 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 7 Jan 2025 23:34:27 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Save=20some=20missing=20refe?= =?UTF-8?q?rences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 +- src/utils/cache.ts | 192 +++++++++++++++++++++++++++++++++++++-------- src/utils/json.ts | 1 - 3 files changed, 162 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6e8eb8..4c98449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcompletevscode", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mapcompletevscode", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "jsonc-parser": "^3.3.1" diff --git a/src/utils/cache.ts b/src/utils/cache.ts index db06044..02405d7 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -9,6 +9,16 @@ import { JSONPath } from "jsonc-parser"; * Worker class to handle the cache creation and updates */ export class CacheWorker { + /** + * The extension context + */ + private readonly context: vscode.ExtensionContext; + + /** + * List of cache items + */ + private cache: CacheItem[] = []; + /** * Creates a new cache * @@ -19,18 +29,25 @@ export class CacheWorker { context: vscode.ExtensionContext ): Promise { const cache = new CacheWorker(context); - await cache.initialize(); + await cache.scanWorkspace(); return cache; } - private async initialize() { - await this.scanWorkspace(); + /** + * Create a new cache + * + * @param context The extension context + */ + private constructor(context: vscode.ExtensionContext) { + this.context = context; + // We probably want to create a fileSystemWatcher here + // to listen for changes in the workspace + this.createFileSystemWatcher(); } - private readonly context: vscode.ExtensionContext; - private cache: CacheItem[] = []; /** * Saves the current cache to the storage as JSON + * TODO: Find a more elegant way to do this */ private save() { const jsonString = JSON.stringify(this.cache); @@ -44,18 +61,6 @@ export class CacheWorker { } } - /** - * Create a new cache - * - * @param context The extension context - */ - constructor(context: vscode.ExtensionContext) { - this.context = context; - // We probably want to create a fileSystemWatcher here - // to listen for changes in the workspace - this.createFileSystemWatcher(); - } - /** * Create a file system watcher */ @@ -161,15 +166,13 @@ export class CacheWorker { // Look through the layers for (const layer of json.layers) { - // Check if it is a string and not an object + // Reference if it's a string if (typeof layer === "string") { // It is a reference console.log(`Reference found to ${layer} in ${filePath}`); const from = `themes.${json.id}`; - const to = layer.includes(".") - ? `themes.${layer.split(".")[0]}layers.${layer.split(".")[1]}` - : `layers.${layer}`; + const to = `layers.${layer}`; this.cache.push({ id: layer, @@ -183,6 +186,55 @@ export class CacheWorker { }, }); } + // Builtin layer if we have a builtin property + else if (layer.builtin) { + if (typeof layer.builtin === "string") { + // Single layer + console.log(`Reference found to ${layer.builtin} in ${filePath}`); + + const from = `themes.${json.id}`; + const to = `layers.${layer.builtin}`; + + this.cache.push({ + id: layer.builtin, + filePath: uri, + jsonPath: ["layers"], + type: "reference", + reference: { + from, + to, + type: "layer", + }, + }); + } else { + // Multiple layers + for (const builtinLayer of layer.builtin) { + console.log(`Reference found to ${builtinLayer} in ${filePath}`); + + const from = `themes.${json.id}`; + const to = `layers.${builtinLayer}`; + + this.cache.push({ + id: builtinLayer, + filePath: uri, + jsonPath: ["layers"], + type: "reference", + reference: { + from, + to, + type: "layer", + }, + }); + } + } + } + // Inline layer else + else { + console.log(`Found inline layer ${layer.id} in ${filePath}`); + const text = JSON.stringify(layer); + const from = `themes.${json.id}.layers.${layer.id}`; + this.saveLayerTextToCache(text, uri, from, true); + } } this.save(); @@ -212,8 +264,14 @@ export class CacheWorker { * @param text The text of the layer * @param uri The URI of the layer file * @param from The theme or layer where the layer is from, e.g. layers.bicycle_rental or themes.cyclofix.layers.0 + * @param referencesOnly Whether to only save references, or also the tagRenderings and filters. This is useful for inline layers, because their filters and tagRenderings can't be reused */ - private saveLayerTextToCache(text: string, uri: vscode.Uri, from: string) { + private saveLayerTextToCache( + text: string, + uri: vscode.Uri, + from: string, + referencesOnly = false + ) { const filePath = uri.fsPath; console.log("Saving layer to cache", filePath); /** @@ -221,6 +279,12 @@ export class CacheWorker { */ const json = JSON.parse(text); + // Check if this layer doesn't have a special source, or uses a geoJson source + if (json.source === "special" || json.source.geoJson) { + console.log("Layer has a special source, only saving references"); + referencesOnly = true; + } + // Look through the tagRenderings, if the layer has any if (json.tagRenderings) { for (const tagRendering of json.tagRenderings) { @@ -247,14 +311,68 @@ export class CacheWorker { }, }); } else if (typeof tagRendering === "object") { - // This is a tagRendering, which can be reused - console.log(`TagRendering found in ${filePath}`); - this.cache.push({ - id: `${json.id}.${tagRendering.id}`, - filePath: uri, - jsonPath: ["tagRenderings"], - type: "tagRendering", - }); + // This is a tagRendering, or a reference to one + if (tagRendering.builtin) { + // This is a reference to a built-in tagRendering (or multiple ones) + if (typeof tagRendering.builtin === "string") { + // Single tagRendering + console.log( + `Reference found to ${tagRendering.builtin} in ${filePath}` + ); + + const to = tagRendering.builtin.includes(".") + ? `layers.${tagRendering.builtin.split(".")[0]}.tagRenderings.${ + tagRendering.builtin.split(".")[1] + }` + : `layers.questions.tagRenderings.${tagRendering.builtin}`; + + this.cache.push({ + id: tagRendering.builtin, + filePath: uri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from, + to, + type: "tagRendering", + }, + }); + } else { + // Multiple tagRenderings + for (const builtinTagRendering of tagRendering.builtin) { + console.log( + `Reference found to ${builtinTagRendering} in ${filePath}` + ); + + const to = builtinTagRendering.includes(".") + ? `layers.${ + builtinTagRendering.split(".")[0] + }.tagRenderings.${builtinTagRendering.split(".")[1]}` + : `layers.questions.tagRenderings.${builtinTagRendering}`; + + this.cache.push({ + id: builtinTagRendering, + filePath: uri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from, + to, + type: "tagRendering", + }, + }); + } + } + } else if (!referencesOnly) { + // This is a tagRendering, which can be reused + console.log(`TagRendering found in ${filePath}`); + this.cache.push({ + id: `${json.id}.${tagRendering.id}`, + filePath: uri, + jsonPath: ["tagRenderings"], + type: "tagRendering", + }); + } } } } else { @@ -283,7 +401,7 @@ export class CacheWorker { type: "filter", }, }); - } else if (typeof filter === "object") { + } else if (typeof filter === "object" && !referencesOnly) { // This is a filter, which can be reused console.log(`Filter found in ${filePath}`); this.cache.push({ @@ -304,6 +422,7 @@ export class CacheWorker { /** * Print the current cache state + * TODO: This probably needs to be removed at some point */ public printCache() { console.log("Current cache state:", this.cache); @@ -322,6 +441,10 @@ export class Cache { return cache; } + /** + * Load the cache from the .cache/cache.json file + * TODO: Find a more elegant way to do this + */ private async loadCache() { // Get the cache from the .cache/cache.json file in the workspace folder const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath; @@ -392,6 +515,11 @@ export class Cache { } } +/** + * A cached item + * Can be a tagRendering or filter from a(n) (inline) layer + * Can also be a reference between files + */ interface CacheItem { /** * Where the item is defined in the workspace @@ -406,7 +534,7 @@ interface CacheItem { /** * What kind of item it is */ - type: "tagRendering" | "filter" | "layer" | "reference"; + type: "tagRendering" | "filter" | "reference"; /** * The ID of the item diff --git a/src/utils/json.ts b/src/utils/json.ts index 5c4f838..2b3fb31 100644 --- a/src/utils/json.ts +++ b/src/utils/json.ts @@ -13,7 +13,6 @@ import { JSONPath, findNodeAtLocation, parseTree } from "jsonc-parser"; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getValueFromPath(json: string, path: JSONPath): any { - console.log("getValueFromPath", path); const rootNode = parseTree(json); if (!rootNode) { return undefined; From 4d1ea863dfbc0cc9a049a79b3464720e6f31c3a9 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 8 Jan 2025 00:46:51 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9A=A7=20First=20kind=20of=20working?= =?UTF-8?q?=20version=20for=20tagRenderingImplementationProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 4 +- src/layers.ts | 146 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/cache.ts | 18 +++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index da2b645..cee3eca 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { filterDefinitionProvider, tagRenderingCompletionProvider, tagRenderingDefinitionProvider, + tagRenderingImplementationProvider, } from "./layers"; import { pathDefinitionProvider } from "./license_info"; import { CacheWorker } from "./utils/cache"; @@ -19,7 +20,8 @@ export async function activate(context: vscode.ExtensionContext) { tagRenderingCompletionProvider, tagRenderingDefinitionProvider, filterCompletionProvider, - filterDefinitionProvider + filterDefinitionProvider, + tagRenderingImplementationProvider ); // Activate all license info related features diff --git a/src/layers.ts b/src/layers.ts index 137c4b8..bde1e41 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -13,6 +13,7 @@ import { getCursorPath, getRawCursorPath, getStartEnd } from "./utils/cursor"; import { getFilters, getTagRenderings } from "./utils/mapcomplete"; import { getValueFromPath } from "./utils/json"; import { JSONPath } from "jsonc-parser"; +import { Cache } from "./utils/cache"; /** * Tag rendering completion provider @@ -172,6 +173,151 @@ export const tagRenderingDefinitionProvider = } ); +export const tagRenderingImplementationProvider = + vscode.languages.registerImplementationProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/*/*/*.json", + }, + { + async provideImplementation( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken + ) { + console.log("tagRenderingImplementationProvider"); + const text = document.getText(); + const jsonPath = getCursorPath(text, position); + const rawJsonPath = getRawCursorPath(text, position); + + const regex = /^tagRenderings(\+)?\.\d+\.id$/; + + if (regex.exec(jsonPath)) { + const tagRenderingId = getValueFromPath(text, rawJsonPath); + const layerName = document.fileName.split("/").pop()?.split(".")[0]; + const to = `layers.${layerName}.tagRenderings.${tagRenderingId}`; + + try { + const cache = await Cache.create(); + const references = cache.getReferences(to); + + if (references.length === 0) { + return null; + } else { + // TODO: This is way too much to be executing at this time, most of this should be cached + // TODO: Also, this seems to fail for the first time, but work for every subsequent time + console.log(`Found ${references.length} references to ${to}`); + + const links: vscode.DefinitionLink[] = []; + for (const reference of references) { + const originType = reference.reference?.from.split(".")[0]; + const originName = reference.reference?.from.split(".")[1]; + + // We need to open the file where the reference is located + const originFile = await vscode.workspace.findFiles( + `assets/${originType}/${originName}/${originName}.json` + ); + if (originFile.length === 0) { + continue; + } + + const originText = await vscode.workspace.fs.readFile( + originFile[0] + ); + const originTextString = new TextDecoder().decode(originText); + const origin = JSON.parse(originTextString); + + let tagRenderings: unknown[] = []; + let tagRenderingsPath: JSONPath = []; + + // Now we'll need to find the tagRenderings object, and its path + if (originType === "themes") { + const parts = reference.reference?.from.split("."); + if (!parts) { + continue; + } else { + console.log("Parts", parts); + // Now we need to find the correct inline layer + const layerIndex = origin.layers.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (layer: any) => layer.id === parts[3] + ); + + const path: JSONPath = [ + parts[2], + layerIndex, + ...reference.jsonPath, + ]; + + console.log("Trying to get tagRenderings from theme", path); + + const tagRenderingsFromOrigin = getValueFromPath( + originTextString, + path + ); + if (!tagRenderingsFromOrigin) { + console.error( + "Could not find tagRenderings in theme", + originName + ); + continue; + } else { + // Yaay, we found the tagRenderings + console.log("Found tagRenderings in theme", originName); + tagRenderings = tagRenderingsFromOrigin as unknown[]; + tagRenderingsPath = path; + } + } + } else if (originType === "layers") { + tagRenderings = origin.tagRenderings; + tagRenderingsPath = ["tagRenderings"]; + } + + // The index is actually a really complicated, because a reference could be a string or an object with a builtin property, which can be a string or a list of strings + // Also if the reference is from an inline layer + const tagRenderingIndex = tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => { + if (typeof tr === "string") { + return tr === reference.id; + } else if (typeof tr.builtin === "string") { + return tr.builtin === reference.id; + } + // } else if (tr.builtin) { + // return tr.builtin.includes(reference.id); + // } + } + ); + const path: JSONPath = [ + ...tagRenderingsPath, + tagRenderingIndex, + ]; + const startEnd = getStartEnd(originTextString, path); + + console.log( + `Pushing link from ${document.fileName} to ${originFile[0].path} at ${startEnd.start.line}.${startEnd.start.character} to ${startEnd.end.line}.${startEnd.end.character}` + ); + + links.push({ + originSelectionRange: getStartEnd(text, rawJsonPath), + targetRange: startEnd, + targetUri: originFile[0], + }); + } + console.log(`Found ${links.length} implementations`); + return links; + } + } catch (error) { + console.error("Error get implementation", error); + } + } + + return null; + }, + } + ); + /** * Filter completion provider * diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 02405d7..bfaa5ab 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -280,7 +280,7 @@ export class CacheWorker { const json = JSON.parse(text); // Check if this layer doesn't have a special source, or uses a geoJson source - if (json.source === "special" || json.source.geoJson) { + if (json.source === "special" || json.source?.geoJson) { console.log("Layer has a special source, only saving references"); referencesOnly = true; } @@ -453,7 +453,6 @@ export class Cache { const cache = await vscode.workspace.fs.readFile(cacheUri); const cacheString = new TextDecoder().decode(cache); this.cache = JSON.parse(cacheString); - console.log(`Cache loaded, ${this.cache.length} items`); } else { console.error("No workspace folder found"); throw new Error("No workspace folder found"); @@ -513,6 +512,21 @@ export class Cache { } return filters; } + + /** + * Get all references to a specific item + * + * @param to Item to get references for (e.g. layers.bicycle_rental) + * @returns List of references + */ + public getReferences(to: string): CacheItem[] { + return this.cache.filter((item) => { + if (item.type === "reference") { + return item.reference?.to === to; + } + return false; + }); + } } /** From 3fa4e39342ad20b10a6a59f934445679427fd2bf Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 8 Jan 2025 16:22:47 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=9A=A7=20Move=20some=20code=20to=20Ca?= =?UTF-8?q?cheWorker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layers.ts | 106 ++------- src/utils/cache.ts | 555 +++++++++++++++++++++++++++++++++------------ 2 files changed, 426 insertions(+), 235 deletions(-) diff --git a/src/layers.ts b/src/layers.ts index bde1e41..0e21040 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -205,105 +205,27 @@ export const tagRenderingImplementationProvider = if (references.length === 0) { return null; } else { - // TODO: This is way too much to be executing at this time, most of this should be cached - // TODO: Also, this seems to fail for the first time, but work for every subsequent time console.log(`Found ${references.length} references to ${to}`); const links: vscode.DefinitionLink[] = []; for (const reference of references) { - const originType = reference.reference?.from.split(".")[0]; - const originName = reference.reference?.from.split(".")[1]; - - // We need to open the file where the reference is located - const originFile = await vscode.workspace.findFiles( - `assets/${originType}/${originName}/${originName}.json` - ); - if (originFile.length === 0) { - continue; - } - - const originText = await vscode.workspace.fs.readFile( - originFile[0] - ); - const originTextString = new TextDecoder().decode(originText); - const origin = JSON.parse(originTextString); - - let tagRenderings: unknown[] = []; - let tagRenderingsPath: JSONPath = []; - - // Now we'll need to find the tagRenderings object, and its path - if (originType === "themes") { - const parts = reference.reference?.from.split("."); - if (!parts) { - continue; - } else { - console.log("Parts", parts); - // Now we need to find the correct inline layer - const layerIndex = origin.layers.findIndex( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (layer: any) => layer.id === parts[3] - ); - - const path: JSONPath = [ - parts[2], - layerIndex, - ...reference.jsonPath, - ]; - - console.log("Trying to get tagRenderings from theme", path); - - const tagRenderingsFromOrigin = getValueFromPath( - originTextString, - path - ); - if (!tagRenderingsFromOrigin) { - console.error( - "Could not find tagRenderings in theme", - originName - ); - continue; - } else { - // Yaay, we found the tagRenderings - console.log("Found tagRenderings in theme", originName); - tagRenderings = tagRenderingsFromOrigin as unknown[]; - tagRenderingsPath = path; - } - } - } else if (originType === "layers") { - tagRenderings = origin.tagRenderings; - tagRenderingsPath = ["tagRenderings"]; - } - - // The index is actually a really complicated, because a reference could be a string or an object with a builtin property, which can be a string or a list of strings - // Also if the reference is from an inline layer - const tagRenderingIndex = tagRenderings.findIndex( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (tr: any) => { - if (typeof tr === "string") { - return tr === reference.id; - } else if (typeof tr.builtin === "string") { - return tr.builtin === reference.id; - } - // } else if (tr.builtin) { - // return tr.builtin.includes(reference.id); - // } - } - ); - const path: JSONPath = [ - ...tagRenderingsPath, - tagRenderingIndex, - ]; - const startEnd = getStartEnd(originTextString, path); - console.log( - `Pushing link from ${document.fileName} to ${originFile[0].path} at ${startEnd.start.line}.${startEnd.start.character} to ${startEnd.end.line}.${startEnd.end.character}` + `Pushing link from ${document.fileName} to ${reference.reference?.to.uri?.fsPath} at ${reference.reference?.to.range?.start.line}.${reference.reference?.to.range?.start.character} to ${reference.reference?.to.range?.end.line}.${reference.reference?.to.range?.end.character}` ); - links.push({ - originSelectionRange: getStartEnd(text, rawJsonPath), - targetRange: startEnd, - targetUri: originFile[0], - }); + // Check if we have a targetRange and targetUri + if ( + reference.reference?.to.range && + reference.reference?.to.uri + ) { + links.push({ + originSelectionRange: reference.reference?.from.range, + targetRange: reference.reference?.to.range, + targetUri: reference.reference?.to.uri, + }); + } else { + console.error("Incomplete reference", reference); + } } console.log(`Found ${links.length} implementations`); return links; diff --git a/src/utils/cache.ts b/src/utils/cache.ts index bfaa5ab..4b5c0f9 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; import { JSONPath } from "jsonc-parser"; +import { getStartEnd } from "./cursor"; /** * Worker class to handle the cache creation and updates @@ -164,15 +165,24 @@ export class CacheWorker { const text = new TextDecoder().decode(await content); const json = JSON.parse(text); + // Determine some locations + const from = `themes.${json.id}`; + const fromFile = uri; + // Look through the layers for (const layer of json.layers) { + const layerIndex = json.layers.indexOf(layer); + // Reference if it's a string if (typeof layer === "string") { // It is a reference console.log(`Reference found to ${layer} in ${filePath}`); - const from = `themes.${json.id}`; + const fromStartEnd = getStartEnd(text, ["layers", layerIndex]); const to = `layers.${layer}`; + const toFile = await vscode.workspace.findFiles( + `**/assets/layers/${layer}/${layer}.json` + ); this.cache.push({ id: layer, @@ -180,8 +190,15 @@ export class CacheWorker { jsonPath: ["layers"], type: "reference", reference: { - from, - to, + from: { + id: from, + uri: fromFile, + range: fromStartEnd, + }, + to: { + id: to, + uri: toFile[0], + }, type: "layer", }, }); @@ -192,8 +209,15 @@ export class CacheWorker { // Single layer console.log(`Reference found to ${layer.builtin} in ${filePath}`); - const from = `themes.${json.id}`; + const fromStartEnd = getStartEnd(text, [ + "layers", + layerIndex, + "builtin", + ]); const to = `layers.${layer.builtin}`; + const toFile = await vscode.workspace.findFiles( + `**/assets/layers/${layer.builtin}/${layer.builtin}.json` + ); this.cache.push({ id: layer.builtin, @@ -201,8 +225,15 @@ export class CacheWorker { jsonPath: ["layers"], type: "reference", reference: { - from, - to, + from: { + id: from, + uri: fromFile, + range: fromStartEnd, + }, + to: { + id: to, + uri: toFile[0], + }, type: "layer", }, }); @@ -211,8 +242,17 @@ export class CacheWorker { for (const builtinLayer of layer.builtin) { console.log(`Reference found to ${builtinLayer} in ${filePath}`); - const from = `themes.${json.id}`; + const builtinLayerIndex = layer.builtin.indexOf(builtinLayer); + const fromStartEnd = getStartEnd(text, [ + "layers", + layerIndex, + "builtin", + builtinLayerIndex, + ]); const to = `layers.${builtinLayer}`; + const toFile = await vscode.workspace.findFiles( + `**/assets/layers/${builtinLayer}/${builtinLayer}.json` + ); this.cache.push({ id: builtinLayer, @@ -220,8 +260,15 @@ export class CacheWorker { jsonPath: ["layers"], type: "reference", reference: { - from, - to, + from: { + id: from, + uri: fromFile, + range: fromStartEnd, + }, + to: { + id: to, + uri: toFile[0], + }, type: "layer", }, }); @@ -231,9 +278,9 @@ export class CacheWorker { // Inline layer else else { console.log(`Found inline layer ${layer.id} in ${filePath}`); - const text = JSON.stringify(layer); + const layerText = JSON.stringify(layer); const from = `themes.${json.id}.layers.${layer.id}`; - this.saveLayerTextToCache(text, uri, from, true); + await this.saveLayerTextToCache(layerText, uri, from, true, text); } } @@ -255,22 +302,24 @@ export class CacheWorker { const uriFileName = uriPathSplit[uriPathSplit.length - 1]; const from = `layers.${uriFileName.split(".")[0]}`; - this.saveLayerTextToCache(text, uri, from); + await this.saveLayerTextToCache(text, uri, from); } /** * Save a layer to the cache, given the text of the layer * - * @param text The text of the layer + * @param text Text representation of layer * @param uri The URI of the layer file * @param from The theme or layer where the layer is from, e.g. layers.bicycle_rental or themes.cyclofix.layers.0 * @param referencesOnly Whether to only save references, or also the tagRenderings and filters. This is useful for inline layers, because their filters and tagRenderings can't be reused + * @param fullFileText The full text of the original theme file, used for calculating position */ - private saveLayerTextToCache( + private async saveLayerTextToCache( text: string, uri: vscode.Uri, from: string, - referencesOnly = false + referencesOnly = false, + fullFileText?: string ) { const filePath = uri.fsPath; console.log("Saving layer to cache", filePath); @@ -287,93 +336,16 @@ export class CacheWorker { // Look through the tagRenderings, if the layer has any if (json.tagRenderings) { - for (const tagRendering of json.tagRenderings) { - // Check if it is a string and not an object - if (typeof tagRendering === "string") { - // It is a reference - console.log(`Reference found to ${tagRendering} in ${filePath}`); - - const to = tagRendering.includes(".") - ? `layers.${tagRendering.split(".")[0]}.tagRenderings.${ - tagRendering.split(".")[1] - }` - : `layers.questions.tagRenderings.${tagRendering}`; - - this.cache.push({ - id: tagRendering, - filePath: uri, - jsonPath: ["tagRenderings"], - type: "reference", - reference: { - from, - to, - type: "tagRendering", - }, - }); - } else if (typeof tagRendering === "object") { - // This is a tagRendering, or a reference to one - if (tagRendering.builtin) { - // This is a reference to a built-in tagRendering (or multiple ones) - if (typeof tagRendering.builtin === "string") { - // Single tagRendering - console.log( - `Reference found to ${tagRendering.builtin} in ${filePath}` - ); - - const to = tagRendering.builtin.includes(".") - ? `layers.${tagRendering.builtin.split(".")[0]}.tagRenderings.${ - tagRendering.builtin.split(".")[1] - }` - : `layers.questions.tagRenderings.${tagRendering.builtin}`; - - this.cache.push({ - id: tagRendering.builtin, - filePath: uri, - jsonPath: ["tagRenderings"], - type: "reference", - reference: { - from, - to, - type: "tagRendering", - }, - }); - } else { - // Multiple tagRenderings - for (const builtinTagRendering of tagRendering.builtin) { - console.log( - `Reference found to ${builtinTagRendering} in ${filePath}` - ); - - const to = builtinTagRendering.includes(".") - ? `layers.${ - builtinTagRendering.split(".")[0] - }.tagRenderings.${builtinTagRendering.split(".")[1]}` - : `layers.questions.tagRenderings.${builtinTagRendering}`; - - this.cache.push({ - id: builtinTagRendering, - filePath: uri, - jsonPath: ["tagRenderings"], - type: "reference", - reference: { - from, - to, - type: "tagRendering", - }, - }); - } - } - } else if (!referencesOnly) { - // This is a tagRendering, which can be reused - console.log(`TagRendering found in ${filePath}`); - this.cache.push({ - id: `${json.id}.${tagRendering.id}`, - filePath: uri, - jsonPath: ["tagRenderings"], - type: "tagRendering", - }); - } - } + try { + await this.saveTagRenderingsToCache( + text, + from, + uri, + referencesOnly, + fullFileText + ); + } catch (error) { + console.error(`Error saving tagRenderings for ${from}`, error); } } else { console.log("No tagRenderings found in", filePath); @@ -381,36 +353,16 @@ export class CacheWorker { if (json.filter) { // Look through the filters - for (const filter of json.filter) { - // Check if it is a string and not an object - if (typeof filter === "string") { - // It is a reference - console.log(`Reference found to ${filter} in ${filePath}`); - - const from = `layers.${json.id}`; - const to = `layers.${filter}`; - - this.cache.push({ - id: filter, - filePath: uri, - jsonPath: ["filters"], - type: "reference", - reference: { - from, - to, - type: "filter", - }, - }); - } else if (typeof filter === "object" && !referencesOnly) { - // This is a filter, which can be reused - console.log(`Filter found in ${filePath}`); - this.cache.push({ - id: `${json.id}.${filter.id}`, - filePath: uri, - jsonPath: ["filters"], - type: "filter", - }); - } + try { + await this.saveFiltersToCache( + text, + from, + uri, + referencesOnly, + fullFileText + ); + } catch (error) { + console.error(`Error saving filters for ${from}`, error); } } else { console.log("No filters found in", filePath); @@ -420,6 +372,305 @@ export class CacheWorker { this.printCache(); } + private async saveTagRenderingsToCache( + text: string, + from: string, + fromUri: vscode.Uri, + referencesOnly = false, + fullFileText?: string + ) { + const json = JSON.parse(text); + + for (const tagRendering of json.tagRenderings) { + // Check if it is a string and not an object + if (typeof tagRendering === "string") { + // It is a reference + console.log( + `Reference found to tagRendering ${tagRendering} in ${from}` + ); + + // The range is dependent on whether we're dealing with a full file or not + // const fromStartEnd = fullFileText + // ? getStartEnd(fullFileText, [ + // ...from.split("."), + // "", + // filterReferenceIndex, + // ]) + // : getStartEnd(text, ["filter", filterReferenceIndex]); + + // The range depends on whether we're dealing with a full file or not + // const fromStartEnd = fullFileText + // ? getStartEnd(fullFileText, [ + // ...from.split("."), + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]) + // : getStartEnd(text, [ + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]); + + // const to = tagRendering.includes(".") + // ? `layers.${tagRendering.split(".")[0]}.tagRenderings.${ + // tagRendering.split(".")[1] + // }` + // : `layers.questions.tagRenderings.${tagRendering}`; + // const toFile = await vscode.workspace.findFiles( + // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` + // ); + // // Read toFile and get the text + // const toContent = await vscode.workspace.fs.readFile(toFile[0]); + // const toText = new TextDecoder().decode(toContent); + // const toJson = JSON.parse(toText); + // const trIndex = toJson.tagRenderings.findIndex( + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // (tr: any) => tr.id === tagRendering.split(".")?.pop() + // ); + // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + + // this.cache.push({ + // id: tagRendering, + // filePath: fromUri, + // jsonPath: ["tagRenderings"], + // type: "reference", + // reference: { + // from: { + // id: from, + // uri: fromUri, + // range: fromStartEnd, + // }, + // to: { + // id: to, + // uri: toFile[0], + // range: toRange, + // }, + // type: "tagRendering", + // }, + // }); + } + // } else if (typeof tagRendering === "object") { + // // This is a tagRendering, or a reference to one + // if (tagRendering.builtin) { + // // This is a reference to a built-in tagRendering (or multiple ones) + // if (typeof tagRendering.builtin === "string") { + // // Single tagRendering + // console.log( + // `Reference found to ${tagRendering.builtin} in ${from}` + // ); + + // // The range depends on whether we're dealing with a full file or not + // const fromStartEnd = fullFileText + // ? getStartEnd(fullFileText, [ + // ...from.split("."), + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]) + // : getStartEnd(text, [ + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]); + + // const to = tagRendering.builtin.includes(".") + // ? `layers.${tagRendering.builtin.split(".")[0]}.tagRenderings.${ + // tagRendering.builtin.split(".")[1] + // }` + // : `layers.questions.tagRenderings.${tagRendering.builtin}`; + // const toFile = await vscode.workspace.findFiles( + // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` + // ); + // // Read toFile and get the text + // const toContent = await vscode.workspace.fs.readFile(toFile[0]); + // const toText = new TextDecoder().decode(toContent); + // const toJson = JSON.parse(toText); + // const trIndex = toJson.tagRenderings.findIndex( + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // (tr: any) => tr.id === tagRendering.builtin.split(".")?.pop() + // ); + // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + + // this.cache.push({ + // id: tagRendering.builtin, + // filePath: fromUri, + // jsonPath: ["tagRenderings"], + // type: "reference", + // reference: { + // from: { + // id: from, + // uri: fromUri, + // range: fromStartEnd, + // }, + // to: { + // id: to, + // uri: toFile[0], + // range: toRange, + // }, + // type: "tagRendering", + // }, + // }); + // } else { + // // Multiple tagRenderings + // for (const builtinTagRendering of tagRendering.builtin) { + // console.log( + // `Reference found to ${builtinTagRendering} in ${from}` + // ); + + // // The range depends on whether we're dealing with a full file or not + // const fromStartEnd = fullFileText + // ? getStartEnd(fullFileText, [ + // ...from.split("."), + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]) + // : getStartEnd(text, [ + // "tagRenderings", + // json.tagRenderings.indexOf(tagRendering), + // ]); + + // const to = builtinTagRendering.includes(".") + // ? `layers.${builtinTagRendering.split(".")[0]}.tagRenderings.${ + // builtinTagRendering.split(".")[1] + // }` + // : `layers.questions.tagRenderings.${builtinTagRendering}`; + // const toFile = await vscode.workspace.findFiles( + // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` + // ); + // // Read toFile and get the text + // const toContent = await vscode.workspace.fs.readFile(toFile[0]); + // const toText = new TextDecoder().decode(toContent); + // const toJson = JSON.parse(toText); + // const trIndex = toJson.tagRenderings.findIndex( + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // (tr: any) => tr.id === builtinTagRendering.split(".")?.pop() + // ); + // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + + // this.cache.push({ + // id: builtinTagRendering, + // filePath: fromUri, + // jsonPath: ["tagRenderings"], + // type: "reference", + // reference: { + // from: { + // id: from, + // uri: fromUri, + // range: fromStartEnd, + // }, + // to: { + // id: to, + // uri: toFile[0], + // range: toRange, + // }, + // type: "tagRendering", + // }, + // }); + // } + // } + // } else if (!referencesOnly) { + // // This is a tagRendering, which can be reused + // console.log(`TagRendering found in ${from}`); + // this.cache.push({ + // id: `${json.id}.${tagRendering.id}`, + // filePath: fromUri, + // jsonPath: ["tagRenderings"], + // type: "tagRendering", + // }); + // } + } + } + + /** + * Save filters to cache + * + * @param text Text representation of layer + * @param from The theme or layer where the layer is from, e.g. layers.bicycle_rental or themes.cyclofix.layers.0 + * @param fromUri URI of the layer file + * @param referencesOnly Whether to only save references, or also the tagRenderings and filters. This is useful for inline layers, because their filters and tagRenderings can't be reused + * @param fullFileText The full text of the original theme file, used for calculating position + */ + private async saveFiltersToCache( + text: string, + from: string, + fromUri: vscode.Uri, + referencesOnly = false, + fullFileText?: string + ) { + const json = JSON.parse(text); + + for (const filter of json.filter) { + const filterReferenceIndex = json.filter.indexOf(filter); + + // Check if it is a string and not an object + if (typeof filter === "string") { + // It is a reference + console.log(`Reference found to filter ${filter} in ${from}`); + + // The range is dependent on whether we're dealing with a full file or not + const fromStartEnd = fullFileText + ? getStartEnd(fullFileText, [ + ...from.split("."), + "filter", + filterReferenceIndex, + ]) + : getStartEnd(text, ["filter", filterReferenceIndex]); + + const filterId = filter.includes(".") ? filter.split(".")[1] : filter; + const to = filter.includes(".") + ? `layers.${filter.split(".")[0]}` + : `layers.filters`; + // Now we'll need to determine what file we need to look in + const toFileName = filter.includes(".") + ? `**/assets/layers/${filter.split(".")[0]}/${ + filter.split(".")[0] + }.json` + : `**/assets/layers/filters/filters.json`; + const toFile = await vscode.workspace.findFiles(toFileName); + + const toContent = await vscode.workspace.fs.readFile(toFile[0]); + const toText = new TextDecoder().decode(toContent); + const toJson = JSON.parse(toText); + const toFilterIndex = toJson.filter.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (f: any) => f.id === filterId + ); + + if (toFilterIndex === -1) { + console.error(`Filter ${filter} not found`); + continue; + } + const toRange = getStartEnd(toText, ["filter", toFilterIndex]); + + this.cache.push({ + id: filter, + filePath: fromUri, + jsonPath: ["filters"], + type: "reference", + reference: { + from: { + id: from, + uri: fromUri, + range: fromStartEnd, + }, + to: { + id: to, + uri: toFile[0], + range: toRange, + }, + type: "filter", + }, + }); + } else if (typeof filter === "object" && !referencesOnly) { + // This is a filter, which can be reused + console.log(`Filter found in ${from}`); + this.cache.push({ + id: `${json.id}.${filter.id}`, + filePath: fromUri, + jsonPath: ["filters"], + type: "filter", + }); + } + } + } + /** * Print the current cache state * TODO: This probably needs to be removed at some point @@ -522,7 +773,7 @@ export class Cache { public getReferences(to: string): CacheItem[] { return this.cache.filter((item) => { if (item.type === "reference") { - return item.reference?.to === to; + return item.reference?.to.id === to; } return false; }); @@ -570,23 +821,41 @@ interface CacheItem { */ interface Reference { /** - * The theme or layer where the reference is from - * - * @example themes.cyclofix + * The place where the item is being used (eg. themes.cyclofix) */ - from: string; + from: ReferenceDetail; /** - * The path of the file where the reference points to - * This can also be more specific, like a tagRendering or filter - * - * @example layers.bicycle_rental - * @example layers.questions.tagRenderings.name + * The place where the item is defined (eg. layers.bicycle_rental) */ - to: string; + to: ReferenceDetail; /** * The type of item being referenced/reused */ type: "tagRendering" | "filter" | "layer"; } + +interface ReferenceDetail { + /** + * The path of the file for this side of the reference + * + * @example layers.bicycle_rental + * @example layers.questions.tagRenderings.name + * @example themes.cyclofix + */ + id: string; + + /** + * The URI of the file for this side of the reference + */ + uri?: vscode.Uri; + + /** + * The range of the reference in the file + * Useful for highlighting the reference in the file + * + * Only defined when referencing to a part of a file + */ + range?: vscode.Range; +} From 3b0b58e44bff191f535d5dc73bed8f739b4fd55b Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Thu, 9 Jan 2025 00:08:02 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20(most)=20bugs=20in=20c?= =?UTF-8?q?ache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layers.ts | 26 ++- src/utils/cache.ts | 425 ++++++++++++++++++++++++--------------------- 2 files changed, 244 insertions(+), 207 deletions(-) diff --git a/src/layers.ts b/src/layers.ts index 0e21040..7ea860b 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -210,18 +210,26 @@ export const tagRenderingImplementationProvider = const links: vscode.DefinitionLink[] = []; for (const reference of references) { console.log( - `Pushing link from ${document.fileName} to ${reference.reference?.to.uri?.fsPath} at ${reference.reference?.to.range?.start.line}.${reference.reference?.to.range?.start.character} to ${reference.reference?.to.range?.end.line}.${reference.reference?.to.range?.end.character}` + `Pushing link from ${document.fileName} to ${reference.reference?.from.uri?.fsPath} at ${reference.reference?.to.range?.[0]?.line}:${reference.reference?.to.range?.[0]?.character}`, + reference ); - // Check if we have a targetRange and targetUri - if ( - reference.reference?.to.range && - reference.reference?.to.uri - ) { + // Check if we have a targetUri + if (reference.reference?.from.uri) { links.push({ - originSelectionRange: reference.reference?.from.range, - targetRange: reference.reference?.to.range, - targetUri: reference.reference?.to.uri, + originSelectionRange: new vscode.Range( + reference.reference?.to?.range?.[0]?.line ?? 0, + reference.reference?.to?.range?.[0]?.character ?? 0, + reference.reference?.to?.range?.[1]?.line ?? 0, + reference.reference?.to?.range?.[1]?.character ?? 0 + ), + targetRange: new vscode.Range( + reference.reference?.from?.range?.[0]?.line ?? 0, + reference.reference?.from?.range?.[0]?.character ?? 0, + reference.reference?.from?.range?.[1]?.line ?? 0, + reference.reference?.from?.range?.[1]?.character ?? 0 + ), + targetUri: reference.reference?.from?.uri, }); } else { console.error("Incomplete reference", reference); diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 4b5c0f9..08a7769 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -176,7 +176,7 @@ export class CacheWorker { // Reference if it's a string if (typeof layer === "string") { // It is a reference - console.log(`Reference found to ${layer} in ${filePath}`); + console.log(`Reference found to layer ${layer} in ${filePath}`); const fromStartEnd = getStartEnd(text, ["layers", layerIndex]); const to = `layers.${layer}`; @@ -193,7 +193,7 @@ export class CacheWorker { from: { id: from, uri: fromFile, - range: fromStartEnd, + range: [fromStartEnd.start, fromStartEnd.end], }, to: { id: to, @@ -207,7 +207,9 @@ export class CacheWorker { else if (layer.builtin) { if (typeof layer.builtin === "string") { // Single layer - console.log(`Reference found to ${layer.builtin} in ${filePath}`); + console.log( + `Reference found to builtin layer ${layer.builtin} in ${filePath}` + ); const fromStartEnd = getStartEnd(text, [ "layers", @@ -228,7 +230,7 @@ export class CacheWorker { from: { id: from, uri: fromFile, - range: fromStartEnd, + range: [fromStartEnd.start, fromStartEnd.end], }, to: { id: to, @@ -240,7 +242,9 @@ export class CacheWorker { } else { // Multiple layers for (const builtinLayer of layer.builtin) { - console.log(`Reference found to ${builtinLayer} in ${filePath}`); + console.log( + `Reference found to builtin layer ${builtinLayer} in ${filePath}` + ); const builtinLayerIndex = layer.builtin.indexOf(builtinLayer); const fromStartEnd = getStartEnd(text, [ @@ -263,7 +267,7 @@ export class CacheWorker { from: { id: from, uri: fromFile, - range: fromStartEnd, + range: [fromStartEnd.start, fromStartEnd.end], }, to: { id: to, @@ -279,7 +283,7 @@ export class CacheWorker { else { console.log(`Found inline layer ${layer.id} in ${filePath}`); const layerText = JSON.stringify(layer); - const from = `themes.${json.id}.layers.${layer.id}`; + const from = `themes.${json.id}.layers.${layerIndex}`; await this.saveLayerTextToCache(layerText, uri, from, true, text); } } @@ -294,6 +298,10 @@ export class CacheWorker { * @param uri The URI of the layer */ private async saveLayerToCache(uri: vscode.Uri) { + if (uri.fsPath.endsWith("favourite.json")) { + return; + } + // Read the file const content = vscode.workspace.fs.readFile(uri); const text = new TextDecoder().decode(await content); @@ -372,6 +380,16 @@ export class CacheWorker { this.printCache(); } + /** + * Save tag renderings to cache + * TODO: references for tagRenderings can also be a label/group, which we don't support yet + * + * @param text Text representation of layer + * @param from The theme or layer where the layer is from, e.g. layers.bicycle_rental or themes.cyclofix.layers.0 + * @param fromUri URI of the layer file + * @param referencesOnly Whether to only save references, or also the tagRenderings and filters. This is useful for inline layers, because their filters and tagRenderings can't be reused + * @param fullFileText The full text of the original theme file, used for calculating position + */ private async saveTagRenderingsToCache( text: string, from: string, @@ -382,6 +400,9 @@ export class CacheWorker { const json = JSON.parse(text); for (const tagRendering of json.tagRenderings) { + const tagRenderingReferenceIndex = + json.tagRenderings.indexOf(tagRendering); + // Check if it is a string and not an object if (typeof tagRendering === "string") { // It is a reference @@ -390,191 +411,192 @@ export class CacheWorker { ); // The range is dependent on whether we're dealing with a full file or not - // const fromStartEnd = fullFileText - // ? getStartEnd(fullFileText, [ - // ...from.split("."), - // "", - // filterReferenceIndex, - // ]) - // : getStartEnd(text, ["filter", filterReferenceIndex]); + const path = fullFileText + ? [ + from.split(".")[2], + parseInt(from.split(".")[3]), + "tagRenderings", + tagRenderingReferenceIndex, + ] + : ["tagRenderings", tagRenderingReferenceIndex]; + const fromStartEnd = fullFileText + ? getStartEnd(fullFileText, path) + : getStartEnd(text, path); + const to = tagRendering.includes(".") + ? `layers.${tagRendering.split(".")[0]}.tagRenderings.${ + tagRendering.split(".")[1] + }` + : `layers.questions.tagRenderings.${tagRendering}`; + const toFileName = tagRendering.includes(".") + ? `**/assets/layers/${tagRendering.split(".")[0]}/${ + tagRendering.split(".")[0] + }.json` + : `**/assets/layers/questions/questions.json`; + const toFile = await vscode.workspace.findFiles(toFileName); - // The range depends on whether we're dealing with a full file or not - // const fromStartEnd = fullFileText - // ? getStartEnd(fullFileText, [ - // ...from.split("."), - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]) - // : getStartEnd(text, [ - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]); + // Read toFile and get the text + const toContent = await vscode.workspace.fs.readFile(toFile[0]); + const toText = new TextDecoder().decode(toContent); + const toJson = JSON.parse(toText); + const trIndex = toJson.tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => tr.id === tagRendering.split(".")?.pop() + ); + const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); - // const to = tagRendering.includes(".") - // ? `layers.${tagRendering.split(".")[0]}.tagRenderings.${ - // tagRendering.split(".")[1] - // }` - // : `layers.questions.tagRenderings.${tagRendering}`; - // const toFile = await vscode.workspace.findFiles( - // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` - // ); - // // Read toFile and get the text - // const toContent = await vscode.workspace.fs.readFile(toFile[0]); - // const toText = new TextDecoder().decode(toContent); - // const toJson = JSON.parse(toText); - // const trIndex = toJson.tagRenderings.findIndex( - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // (tr: any) => tr.id === tagRendering.split(".")?.pop() - // ); - // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + this.cache.push({ + id: tagRendering, + filePath: fromUri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from: { + id: from, + uri: fromUri, + range: [fromStartEnd.start, fromStartEnd.end], + }, + to: { + id: to, + uri: toFile[0], + range: [toRange.start, toRange.end], + }, + type: "tagRendering", + }, + }); + } else if (typeof tagRendering === "object") { + // This is a tagRendering, or a reference to one, but with an override + if (tagRendering.builtin) { + // This is a reference to a built-in tagRendering (or multiple ones) + if (typeof tagRendering.builtin === "string") { + // Single tagRendering reference + console.log( + `Reference found to builtin tagRendering ${tagRendering.builtin} in ${from}` + ); + // The range is dependent on whether we're dealing with a full file or not + const path = fullFileText + ? [ + from.split(".")[2], + parseInt(from.split(".")[3]), + "tagRenderings", + tagRenderingReferenceIndex, + "builtin", + ] + : ["tagRenderings", tagRenderingReferenceIndex, "builtin"]; + const fromStartEnd = fullFileText + ? getStartEnd(fullFileText, path) + : getStartEnd(text, path); + const to = tagRendering.builtin.includes(".") + ? `layers.${tagRendering.builtin.split(".")[0]}.tagRenderings.${ + tagRendering.builtin.split(".")[1] + }` + : `layers.questions.tagRenderings.${tagRendering}`; + const toFileName = tagRendering.builtin.includes(".") + ? `**/assets/layers/${tagRendering.builtin.split(".")[0]}/${ + tagRendering.builtin.split(".")[0] + }.json` + : `**/assets/layers/questions/questions.json`; + const toFile = await vscode.workspace.findFiles(toFileName); - // this.cache.push({ - // id: tagRendering, - // filePath: fromUri, - // jsonPath: ["tagRenderings"], - // type: "reference", - // reference: { - // from: { - // id: from, - // uri: fromUri, - // range: fromStartEnd, - // }, - // to: { - // id: to, - // uri: toFile[0], - // range: toRange, - // }, - // type: "tagRendering", - // }, - // }); + // Read toFile and get the text + const toContent = await vscode.workspace.fs.readFile(toFile[0]); + const toText = new TextDecoder().decode(toContent); + const toJson = JSON.parse(toText); + const trIndex = toJson.tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => tr.id === tagRendering.builtin.split(".")?.pop() + ); + const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + + this.cache.push({ + id: tagRendering.builtin, + filePath: fromUri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from: { + id: from, + uri: fromUri, + range: [fromStartEnd.start, fromStartEnd.end], + }, + to: { + id: to, + uri: toFile[0], + range: [toRange.start, toRange.end], + }, + type: "tagRendering", + }, + }); + } else { + // Multiple tagRenderings + for (const builtinTagRendering of tagRendering.builtin) { + console.log( + `Reference found to builtin tagRendering ${builtinTagRendering} in ${from}` + ); + // The range depends on whether we're dealing with a full file or not + const fromStartEnd = fullFileText + ? getStartEnd(fullFileText, [ + ...from.split("."), + "tagRenderings", + json.tagRenderings.indexOf(tagRendering), + ]) + : getStartEnd(text, [ + "tagRenderings", + json.tagRenderings.indexOf(tagRendering), + ]); + const to = builtinTagRendering.includes(".") + ? `layers.${builtinTagRendering.split(".")[0]}.tagRenderings.${ + builtinTagRendering.split(".")[1] + }` + : `layers.questions.tagRenderings.${builtinTagRendering}`; + const toFileName = builtinTagRendering.includes(".") + ? `**/assets/layers/${builtinTagRendering.split(".")[0]}/${ + builtinTagRendering.split(".")[0] + }.json` + : `**/assets/layers/questions/questions.json`; + const toFile = await vscode.workspace.findFiles(toFileName); + + // Read toFile and get the text + const toContent = await vscode.workspace.fs.readFile(toFile[0]); + const toText = new TextDecoder().decode(toContent); + const toJson = JSON.parse(toText); + const trIndex = toJson.tagRenderings.findIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (tr: any) => tr.id === builtinTagRendering.split(".")?.pop() + ); + const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); + + this.cache.push({ + id: builtinTagRendering, + filePath: fromUri, + jsonPath: ["tagRenderings"], + type: "reference", + reference: { + from: { + id: from, + uri: fromUri, + range: [fromStartEnd.start, fromStartEnd.end], + }, + to: { + id: to, + uri: toFile[0], + range: [toRange.start, toRange.end], + }, + type: "tagRendering", + }, + }); + } + } + } else if (!referencesOnly) { + // We've now had all possible references, so now we must have an acutal tagRendering + console.log(`TagRendering found in ${from}`); + this.cache.push({ + id: `${json.id}.${tagRendering.id}`, + filePath: fromUri, + jsonPath: ["tagRenderings"], + type: "tagRendering", + }); + } } - // } else if (typeof tagRendering === "object") { - // // This is a tagRendering, or a reference to one - // if (tagRendering.builtin) { - // // This is a reference to a built-in tagRendering (or multiple ones) - // if (typeof tagRendering.builtin === "string") { - // // Single tagRendering - // console.log( - // `Reference found to ${tagRendering.builtin} in ${from}` - // ); - - // // The range depends on whether we're dealing with a full file or not - // const fromStartEnd = fullFileText - // ? getStartEnd(fullFileText, [ - // ...from.split("."), - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]) - // : getStartEnd(text, [ - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]); - - // const to = tagRendering.builtin.includes(".") - // ? `layers.${tagRendering.builtin.split(".")[0]}.tagRenderings.${ - // tagRendering.builtin.split(".")[1] - // }` - // : `layers.questions.tagRenderings.${tagRendering.builtin}`; - // const toFile = await vscode.workspace.findFiles( - // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` - // ); - // // Read toFile and get the text - // const toContent = await vscode.workspace.fs.readFile(toFile[0]); - // const toText = new TextDecoder().decode(toContent); - // const toJson = JSON.parse(toText); - // const trIndex = toJson.tagRenderings.findIndex( - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // (tr: any) => tr.id === tagRendering.builtin.split(".")?.pop() - // ); - // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); - - // this.cache.push({ - // id: tagRendering.builtin, - // filePath: fromUri, - // jsonPath: ["tagRenderings"], - // type: "reference", - // reference: { - // from: { - // id: from, - // uri: fromUri, - // range: fromStartEnd, - // }, - // to: { - // id: to, - // uri: toFile[0], - // range: toRange, - // }, - // type: "tagRendering", - // }, - // }); - // } else { - // // Multiple tagRenderings - // for (const builtinTagRendering of tagRendering.builtin) { - // console.log( - // `Reference found to ${builtinTagRendering} in ${from}` - // ); - - // // The range depends on whether we're dealing with a full file or not - // const fromStartEnd = fullFileText - // ? getStartEnd(fullFileText, [ - // ...from.split("."), - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]) - // : getStartEnd(text, [ - // "tagRenderings", - // json.tagRenderings.indexOf(tagRendering), - // ]); - - // const to = builtinTagRendering.includes(".") - // ? `layers.${builtinTagRendering.split(".")[0]}.tagRenderings.${ - // builtinTagRendering.split(".")[1] - // }` - // : `layers.questions.tagRenderings.${builtinTagRendering}`; - // const toFile = await vscode.workspace.findFiles( - // `**/assets/layers/${to.split("."[1])}/${to.split(".")[1]}.json` - // ); - // // Read toFile and get the text - // const toContent = await vscode.workspace.fs.readFile(toFile[0]); - // const toText = new TextDecoder().decode(toContent); - // const toJson = JSON.parse(toText); - // const trIndex = toJson.tagRenderings.findIndex( - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // (tr: any) => tr.id === builtinTagRendering.split(".")?.pop() - // ); - // const toRange = getStartEnd(toText, ["tagRenderings", trIndex]); - - // this.cache.push({ - // id: builtinTagRendering, - // filePath: fromUri, - // jsonPath: ["tagRenderings"], - // type: "reference", - // reference: { - // from: { - // id: from, - // uri: fromUri, - // range: fromStartEnd, - // }, - // to: { - // id: to, - // uri: toFile[0], - // range: toRange, - // }, - // type: "tagRendering", - // }, - // }); - // } - // } - // } else if (!referencesOnly) { - // // This is a tagRendering, which can be reused - // console.log(`TagRendering found in ${from}`); - // this.cache.push({ - // id: `${json.id}.${tagRendering.id}`, - // filePath: fromUri, - // jsonPath: ["tagRenderings"], - // type: "tagRendering", - // }); - // } } } @@ -605,14 +627,17 @@ export class CacheWorker { console.log(`Reference found to filter ${filter} in ${from}`); // The range is dependent on whether we're dealing with a full file or not - const fromStartEnd = fullFileText - ? getStartEnd(fullFileText, [ - ...from.split("."), + const path = fullFileText + ? [ + from.split(".")[2], + parseInt(from.split(".")[3]), "filter", filterReferenceIndex, - ]) - : getStartEnd(text, ["filter", filterReferenceIndex]); - + ] + : ["filter", filterReferenceIndex]; + const fromStartEnd = fullFileText + ? getStartEnd(fullFileText, path) + : getStartEnd(text, path); const filterId = filter.includes(".") ? filter.split(".")[1] : filter; const to = filter.includes(".") ? `layers.${filter.split(".")[0]}` @@ -624,7 +649,6 @@ export class CacheWorker { }.json` : `**/assets/layers/filters/filters.json`; const toFile = await vscode.workspace.findFiles(toFileName); - const toContent = await vscode.workspace.fs.readFile(toFile[0]); const toText = new TextDecoder().decode(toContent); const toJson = JSON.parse(toText); @@ -648,12 +672,12 @@ export class CacheWorker { from: { id: from, uri: fromUri, - range: fromStartEnd, + range: [fromStartEnd.start, fromStartEnd.end], }, to: { id: to, uri: toFile[0], - range: toRange, + range: [toRange.start, toRange.end], }, type: "filter", }, @@ -857,5 +881,10 @@ interface ReferenceDetail { * * Only defined when referencing to a part of a file */ - range?: vscode.Range; + range?: [ReferencePosition, ReferencePosition]; +} + +interface ReferencePosition { + character: number; + line: number; } From 62c25bc6932b90b7dd1caa844a139b4a20d628aa Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Thu, 9 Jan 2025 00:24:47 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Add=20implementationProvider=20?= =?UTF-8?q?for=20filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 4 ++- src/layers.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils/cache.ts | 4 +-- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index cee3eca..f6dd869 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import { colorProvider, iconDefinitionProvider } from "./generic"; import { filterCompletionProvider, filterDefinitionProvider, + filterImplementationProvider, tagRenderingCompletionProvider, tagRenderingDefinitionProvider, tagRenderingImplementationProvider, @@ -21,7 +22,8 @@ export async function activate(context: vscode.ExtensionContext) { tagRenderingDefinitionProvider, filterCompletionProvider, filterDefinitionProvider, - tagRenderingImplementationProvider + tagRenderingImplementationProvider, + filterImplementationProvider ); // Activate all license info related features diff --git a/src/layers.ts b/src/layers.ts index 7ea860b..01608cc 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -405,3 +405,79 @@ export const filterDefinitionProvider = }, } ); + +export const filterImplementationProvider = + vscode.languages.registerImplementationProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/*/*/*.json", + }, + { + async provideImplementation( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken + ) { + console.log("filterImplementationProvider"); + const text = document.getText(); + const jsonPath = getCursorPath(text, position); + const rawJsonPath = getRawCursorPath(text, position); + + const regex = /^filter(\+)?\.\d+\.id$/; + + if (regex.exec(jsonPath)) { + const filterId = getValueFromPath(text, rawJsonPath); + const layerName = document.fileName.split("/").pop()?.split(".")[0]; + const to = `layers.${layerName}.filter.${filterId}`; + + try { + const cache = await Cache.create(); + const references = cache.getReferences(to); + + if (references.length === 0) { + console.log("No references found to", to); + return null; + } else { + console.log(`Found ${references.length} references to ${to}`); + + const links: vscode.DefinitionLink[] = []; + for (const reference of references) { + console.log( + `Pushing link from ${document.fileName} to ${reference.reference?.from.uri?.fsPath} at ${reference.reference?.to.range?.[0]?.line}:${reference.reference?.to.range?.[0]?.character}`, + reference + ); + + // Check if we have a targetUri + if (reference.reference?.from.uri) { + links.push({ + originSelectionRange: new vscode.Range( + reference.reference?.to?.range?.[0]?.line ?? 0, + reference.reference?.to?.range?.[0]?.character ?? 0, + reference.reference?.to?.range?.[1]?.line ?? 0, + reference.reference?.to?.range?.[1]?.character ?? 0 + ), + targetRange: new vscode.Range( + reference.reference?.from?.range?.[0]?.line ?? 0, + reference.reference?.from?.range?.[0]?.character ?? 0, + reference.reference?.from?.range?.[1]?.line ?? 0, + reference.reference?.from?.range?.[1]?.character ?? 0 + ), + targetUri: reference.reference?.from?.uri, + }); + } else { + console.error("Incomplete reference", reference); + } + } + console.log(`Found ${links.length} implementations`); + return links; + } + } catch (error) { + console.error("Error get implementation", error); + } + } + + return null; + }, + } + ); diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 08a7769..c0dd0a6 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -640,8 +640,8 @@ export class CacheWorker { : getStartEnd(text, path); const filterId = filter.includes(".") ? filter.split(".")[1] : filter; const to = filter.includes(".") - ? `layers.${filter.split(".")[0]}` - : `layers.filters`; + ? `layers.${filter.split(".")[0]}.filter.${filterId}` + : `layers.filters.filter.${filterId}`; // Now we'll need to determine what file we need to look in const toFileName = filter.includes(".") ? `**/assets/layers/${filter.split(".")[0]}/${ From 9e2ee806998dbf0c387f2a290075a5ec42bdcfd3 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Thu, 9 Jan 2025 01:02:16 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20Add=20layerImplementationProvid?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 4 ++- src/layers.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index f6dd869..6ebfdbe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { filterCompletionProvider, filterDefinitionProvider, filterImplementationProvider, + layerImplementationProvider, tagRenderingCompletionProvider, tagRenderingDefinitionProvider, tagRenderingImplementationProvider, @@ -23,7 +24,8 @@ export async function activate(context: vscode.ExtensionContext) { filterCompletionProvider, filterDefinitionProvider, tagRenderingImplementationProvider, - filterImplementationProvider + filterImplementationProvider, + layerImplementationProvider ); // Activate all license info related features diff --git a/src/layers.ts b/src/layers.ts index 01608cc..e7dd349 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -183,8 +183,7 @@ export const tagRenderingImplementationProvider = { async provideImplementation( document: vscode.TextDocument, - position: vscode.Position, - _token: vscode.CancellationToken + position: vscode.Position ) { console.log("tagRenderingImplementationProvider"); const text = document.getText(); @@ -416,8 +415,7 @@ export const filterImplementationProvider = { async provideImplementation( document: vscode.TextDocument, - position: vscode.Position, - _token: vscode.CancellationToken + position: vscode.Position ) { console.log("filterImplementationProvider"); const text = document.getText(); @@ -481,3 +479,76 @@ export const filterImplementationProvider = }, } ); + +export const layerImplementationProvider = + vscode.languages.registerImplementationProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/layers/*/*.json", + }, + { + async provideImplementation( + document: vscode.TextDocument, + position: vscode.Position + ) { + console.log("layerImplementationProvider"); + const text = document.getText(); + const jsonPath = getCursorPath(text, position); + const rawJsonPath = getRawCursorPath(text, position); + + // Easiest regex in this package for sure + const regex = /^id$/; + + if (regex.exec(jsonPath)) { + const layerId = getValueFromPath(text, rawJsonPath); + const to = `layers.${layerId}`; + + try { + const cache = await Cache.create(); + const references = cache.getReferences(to); + + if (references.length === 0) { + console.log("No references found to", to); + return null; + } else { + console.log(`Found ${references.length} references to ${to}`); + + const links: vscode.DefinitionLink[] = []; + for (const reference of references) { + console.log( + `Pushing link from ${document.fileName} to ${reference.reference?.from.uri?.fsPath} at ${reference.reference?.to.range?.[0]?.line}:${reference.reference?.to.range?.[0]?.character}`, + reference + ); + + // Check if we have a targetUri + if (reference.reference?.from.uri) { + links.push({ + originSelectionRange: new vscode.Range( + reference.reference?.to?.range?.[0]?.line ?? 0, + reference.reference?.to?.range?.[0]?.character ?? 0, + reference.reference?.to?.range?.[1]?.line ?? 0, + reference.reference?.to?.range?.[1]?.character ?? 0 + ), + targetRange: new vscode.Range( + reference.reference?.from?.range?.[0]?.line ?? 0, + reference.reference?.from?.range?.[0]?.character ?? 0, + reference.reference?.from?.range?.[1]?.line ?? 0, + reference.reference?.from?.range?.[1]?.character ?? 0 + ), + targetUri: reference.reference?.from?.uri, + }); + } else { + console.error("Incomplete reference", reference); + } + } + console.log(`Found ${links.length} implementations`); + return links; + } + } catch (error) { + console.error("Error get implementation", error); + } + } + }, + } + ); From 97fe45f6f9fe59672ef55cb24a4200af8a59c023 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Thu, 9 Jan 2025 01:02:49 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9D=20Update=20readme=20and=20chan?= =?UTF-8?q?gelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ README.md | 48 +++++++++++++++++++++++++++++++---------------- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 084bb7c..4fa1aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## Unreleased Version 1.2.0 (2025-XX-XX) + +### Added + +- New caching mechanism for references, tagRenderings and filters. + This allows the completions to show _all_ filters, not just the ones in `filters.json` and `questions.json`. + It's also possible to look up uses of a filter or a tagRendering. + The caching will take about 30 seconds to complete, but it will only run once per session, and will update individual files as they are saved or removed. + ## Version 1.1.1 (2025-01-07) ### Fixed diff --git a/README.md b/README.md index e5a058f..1a4c511 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,27 @@ It adds autocompletion and defintion support for the MapComplete theme and layer Currently the following features are supported: -- 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 -- Autocompletion for filter keys in questions.json -- Definition support for filter keys -- Definition support for paths in license_info.json files -- Colour support for markers, lines and fills +- Layers: + - Autocompletion for the layer names + - Definition support for the layer names + - Implementation support for the layer names (showing where they are used) +- TagRenderings: + - Autocompletion for _all_ tagRenderings + - Definition support for tagRenderings + - Implementation support for tagRenderings (showing where they are used) +- Filters: + - Autocompletion for _all_ filters + - Definition support for filters + - Implementation support for filters (showing where they are used) +- Icons: + - Autocompletion for icons + - Definition support for icons +- Colours: + - Support for colours in markers, lines and fills +- License info: + - Definition support for paths in license_info.json files + +To achieve some of these feature, on startup the extension will parse all the files, which takes about 30 seconds. This will only happen once per session, and will update individual files as they are saved or removed. ![Demo showing autcomplete for layers and icon definition](images/demo.gif) @@ -23,20 +35,24 @@ All notable changes to this project are documented in the [CHANGELOG](CHANGELOG. The extension can be installed in several ways: -### From the Visual Studio Code marketplace +### From the marketplace -You can install this extension from the [Visual Studio Code marketplace](https://marketplace.visualstudio.com/items?itemName=robin-van-der-linde.mapcompletevscode). Just search for "MapComplete" and you should find it. +The extension is available both in the [Visual Studio Code marketplace](https://marketplace.visualstudio.com/items?itemName=robin-van-der-linde.mapcompletevscode) and the [Open VSX registry](https://open-vsx.org/extension/robin-van-der-linde/mapcompletevscode). -Alternatively you van press `Ctrl+P` and paste the following command: +So for both Visual Studio Code and VSCodium, you should just be able to search for "MapComplete" in the extensions tab and install it from there. + +Alternatively you can press `Ctrl+P` and paste the following command: ```cmd ext install robin-van-der-linde.mapcompletevscode ``` -### From Open VSX - -You can also install the extension from the [Open VSX registry](https://open-vsx.org/extension/robin-van-der-linde/mapcompletevscode). Just search for "MapComplete" and you should find it. - ### From the .vsix file You can also install the extension from the .vsix file. You can download the latest version from the [releases page](https://github.com/RobinLinde/MapCompleteVScode/releases). After downloading the .vsix file, you should be able to install it by going to extensions in Visual Studio Code and clicking on the three dots in the top right corner. Then click on "Install from VSIX..." and select the downloaded .vsix file. + +It's also possible to install builds for any commit in any branch by checking out the workflow run for the commit you want to install, and downloading the .vsix file from the artifacts. + +## Usage + +Most of the features should be pretty self-explanatory. As for the implementation support, the 'anchor' for this is the id property of the layer, tagRendering or filter. This means that if you want to see where a layer is used, you should be able to see all uses by using `CTRL+F12` on the id property of the layer, or by right-clicking on the id property and selecting "Go to Implementations". diff --git a/package-lock.json b/package-lock.json index 4c98449..e991058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcompletevscode", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mapcompletevscode", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { "jsonc-parser": "^3.3.1" diff --git a/package.json b/package.json index 2bfc322..b139d50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mapcompletevscode", "displayName": "MapComplete VScode", - "version": "1.1.1", + "version": "1.2.0", "publisher": "robin-van-der-linde", "author": { "name": "Robin van der Linde",