Compare commits

...

9 commits
main ... cache

Author SHA1 Message Date
Robin van der Linde
97fe45f6f9
📝 Update readme and changelog 2025-01-09 01:02:49 +01:00
Robin van der Linde
9e2ee80699
Add layerImplementationProvider 2025-01-09 01:02:16 +01:00
Robin van der Linde
62c25bc693
Add implementationProvider for filters 2025-01-09 00:24:47 +01:00
Robin van der Linde
3b0b58e44b
🐛 Fix (most) bugs in cache 2025-01-09 00:08:02 +01:00
Robin van der Linde
3fa4e39342
🚧 Move some code to CacheWorker 2025-01-08 16:22:47 +01:00
Robin van der Linde
4d1ea863df
🚧 First kind of working version for tagRenderingImplementationProvider 2025-01-08 00:46:51 +01:00
Robin van der Linde
b7ebfee84d
🐛 Save some missing references 2025-01-07 23:34:27 +01:00
Robin van der Linde
a7513fab8c
🔀 Merge branch 'main' into cache 2025-01-07 23:33:05 +01:00
Robin van der Linde
b61270e51e
🚧 Experiment with cache for all filters and tagRenderings 2025-01-07 00:02:33 +01:00
9 changed files with 1246 additions and 67 deletions

View file

@ -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

View file

@ -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".

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mapcompletevscode",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mapcompletevscode",
"version": "1.1.0",
"version": "1.2.0",
"license": "MIT",
"dependencies": {
"jsonc-parser": "^3.3.1"

View file

@ -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",

View file

@ -4,12 +4,16 @@ import { colorProvider, iconDefinitionProvider } from "./generic";
import {
filterCompletionProvider,
filterDefinitionProvider,
filterImplementationProvider,
layerImplementationProvider,
tagRenderingCompletionProvider,
tagRenderingDefinitionProvider,
tagRenderingImplementationProvider,
} 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);
@ -18,7 +22,10 @@ export function activate(context: vscode.ExtensionContext) {
tagRenderingCompletionProvider,
tagRenderingDefinitionProvider,
filterCompletionProvider,
filterDefinitionProvider
filterDefinitionProvider,
tagRenderingImplementationProvider,
filterImplementationProvider,
layerImplementationProvider
);
// Activate all license info related features
@ -26,4 +33,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);
}

View file

@ -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,80 @@ export const tagRenderingDefinitionProvider =
}
);
export const tagRenderingImplementationProvider =
vscode.languages.registerImplementationProvider(
{
language: "json",
scheme: "file",
pattern: "**/assets/*/*/*.json",
},
{
async provideImplementation(
document: vscode.TextDocument,
position: vscode.Position
) {
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 {
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;
},
}
);
/**
* Filter completion provider
*
@ -329,3 +404,151 @@ export const filterDefinitionProvider =
},
}
);
export const filterImplementationProvider =
vscode.languages.registerImplementationProvider(
{
language: "json",
scheme: "file",
pattern: "**/assets/*/*/*.json",
},
{
async provideImplementation(
document: vscode.TextDocument,
position: vscode.Position
) {
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;
},
}
);
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);
}
}
},
}
);

890
src/utils/cache.ts Normal file
View file

@ -0,0 +1,890 @@
/**
* This file contains all functions related to caching the workspace
*/
import * as vscode from "vscode";
import { JSONPath } from "jsonc-parser";
import { getStartEnd } from "./cursor";
/**
* 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
*
* @param context The extension context
* @returns Promise<Cache> The cache
*/
public static async create(
context: vscode.ExtensionContext
): Promise<CacheWorker> {
const cache = new CacheWorker(context);
await cache.scanWorkspace();
return cache;
}
/**
* 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();
}
/**
* 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);
// 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 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\/(?<asset>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);
// 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 ${layer} in ${filePath}`);
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,
filePath: uri,
jsonPath: ["layers"],
type: "reference",
reference: {
from: {
id: from,
uri: fromFile,
range: [fromStartEnd.start, fromStartEnd.end],
},
to: {
id: to,
uri: toFile[0],
},
type: "layer",
},
});
}
// Builtin layer if we have a builtin property
else if (layer.builtin) {
if (typeof layer.builtin === "string") {
// Single layer
console.log(
`Reference found to builtin layer ${layer.builtin} in ${filePath}`
);
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,
filePath: uri,
jsonPath: ["layers"],
type: "reference",
reference: {
from: {
id: from,
uri: fromFile,
range: [fromStartEnd.start, fromStartEnd.end],
},
to: {
id: to,
uri: toFile[0],
},
type: "layer",
},
});
} else {
// Multiple layers
for (const builtinLayer of layer.builtin) {
console.log(
`Reference found to builtin layer ${builtinLayer} in ${filePath}`
);
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,
filePath: uri,
jsonPath: ["layers"],
type: "reference",
reference: {
from: {
id: from,
uri: fromFile,
range: [fromStartEnd.start, fromStartEnd.end],
},
to: {
id: to,
uri: toFile[0],
},
type: "layer",
},
});
}
}
}
// Inline layer else
else {
console.log(`Found inline layer ${layer.id} in ${filePath}`);
const layerText = JSON.stringify(layer);
const from = `themes.${json.id}.layers.${layerIndex}`;
await this.saveLayerTextToCache(layerText, uri, from, true, text);
}
}
this.save();
this.printCache();
}
/**
* Saves a layer to the cache
*
* @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);
const uriPath = uri.path;
const uriPathSplit = uriPath.split("/");
const uriFileName = uriPathSplit[uriPathSplit.length - 1];
const from = `layers.${uriFileName.split(".")[0]}`;
await this.saveLayerTextToCache(text, uri, from);
}
/**
* Save a layer to the cache, given 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 async saveLayerTextToCache(
text: string,
uri: vscode.Uri,
from: string,
referencesOnly = false,
fullFileText?: 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);
// 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) {
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);
}
if (json.filter) {
// Look through the filters
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);
}
this.save();
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,
fromUri: vscode.Uri,
referencesOnly = false,
fullFileText?: string
) {
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
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 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);
// 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);
// 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",
});
}
}
}
}
/**
* 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 path = fullFileText
? [
from.split(".")[2],
parseInt(from.split(".")[3]),
"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]}.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]}/${
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.start, fromStartEnd.end],
},
to: {
id: to,
uri: toFile[0],
range: [toRange.start, toRange.end],
},
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
*/
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;
}
/**
* 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;
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);
} 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;
}
/**
* 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.id === to;
}
return false;
});
}
}
/**
* 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
*/
filePath: vscode.Uri;
/**
* The JSON path to the item in the file
*/
jsonPath: JSONPath;
/**
* What kind of item it is
*/
type: "tagRendering" | "filter" | "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 place where the item is being used (eg. themes.cyclofix)
*/
from: ReferenceDetail;
/**
* The place where the item is defined (eg. layers.bicycle_rental)
*/
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?: [ReferencePosition, ReferencePosition];
}
interface ReferencePosition {
character: number;
line: number;
}

View file

@ -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;

View file

@ -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,11 +34,24 @@ export async function getAvailableLayers(): Promise<string[]> {
/**
* 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<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
try {
const cache = await Cache.create();
return cache.getTagRenderings();
} catch (error) {
console.error(
"Error getting tagRenderings from cache, falling back to questions.json",
error
);
const tagRenderings: vscode.CompletionItem[] = [];
// Open the questions layer file
@ -64,15 +78,29 @@ export async function getTagRenderings(): Promise<vscode.CompletionItem[]> {
}
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<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
try {
const cache = await Cache.create();
return cache.getFilters();
} catch (error) {
console.error(
"Error getting filters from cache, falling back to filters.json",
error
);
const filtersList: vscode.CompletionItem[] = [];
// Open the filters layer file
@ -96,4 +124,5 @@ export async function getFilters(): Promise<vscode.CompletionItem[]> {
}
return filtersList;
}
}