Allow toggling cache, refresh command

This commit is contained in:
Robin van der Linde 2025-01-15 21:43:00 +01:00
parent 720f4e6c2f
commit 619f1d7fb1
Signed by: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
7 changed files with 310 additions and 205 deletions

View file

@ -25,9 +25,10 @@ Currently the following features are supported:
- 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.
Upon the first activation of this extension it will scan the workspace for all layers, tagRenderings and filters and cache them. This will take about 30 seconds, but the results are saved so they will only be updated when there are changes to the files. Alternatively, it is possible to manually update the cache by running the "MapComplete: Update cache" command.
It is also possible to deactivate the caching using setting `mapcomplete.caching`, but this will disable the implementation support, as well as the autocompletion for filters and tagRenderings that are not in the `filters.json` and `questions.json` files.
![Demo showing autcomplete for layers and icon definition](images/demo.gif)
![Demo showing tagRendering definition and autocomplete, color picker and autocomplete for filters](images/demo.gif)
All notable changes to this project are documented in the [CHANGELOG](CHANGELOG.md) file.

View file

@ -49,5 +49,24 @@
},
"dependencies": {
"jsonc-parser": "^3.3.1"
},
"contributes": {
"configuration": {
"type": "object",
"title": "MapComplete",
"properties": {
"mapcomplete.caching": {
"type": "boolean",
"default": true,
"description": "Enable caching of MapComplete themes and layers."
}
}
},
"commands": [
{
"command": "mapcomplete.refresh",
"title": "MapComplete: Refresh cache for themes, layers, filters and tagRenderings"
}
]
}
}

View file

@ -14,6 +14,27 @@ import { pathDefinitionProvider } from "./license_info";
import { CacheWorker } from "./utils/cache";
export async function activate(context: vscode.ExtensionContext) {
let cacheWorker: CacheWorker | undefined;
// Listen for changes in the caching setting
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration("mapcomplete.caching")) {
if (vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
cacheWorker = await CacheWorker.create(context);
} else {
cacheWorker?.dispose();
cacheWorker = undefined;
}
}
});
// Listen for refreshCache command
vscode.commands.registerCommand("mapcomplete.refreshCache", async () => {
if (cacheWorker) {
await cacheWorker.refreshCache();
}
});
// Activate all theme related features
context.subscriptions.push(layerCompletionProvider, layerDefinitionProvider);
@ -34,9 +55,8 @@ export async 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);
// Activate the cache worker, if caching is enabled
if (vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
cacheWorker = await CacheWorker.create(context);
}
}

View file

@ -12,6 +12,9 @@ import {
vsCodeToHex,
} from "./utils/color";
/**
* This provider will provide completions for icons, based on the files in the assets/svg folder
*/
export const iconCompletionProvider =
vscode.languages.registerCompletionItemProvider(
{

View file

@ -185,64 +185,66 @@ export const tagRenderingImplementationProvider =
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("tagRenderingImplementationProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
if (vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
console.log("tagRenderingImplementationProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
const regex = /^tagRenderings(\+)?\.\d+\.id$/;
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}`;
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);
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}`);
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
);
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);
// 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;
}
console.log(`Found ${links.length} implementations`);
return links;
} catch (error) {
console.error("Error get implementation", error);
}
} catch (error) {
console.error("Error get implementation", error);
}
}
return null;
return null;
}
},
}
);
@ -417,65 +419,67 @@ export const filterImplementationProvider =
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("filterImplementationProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
if (vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
console.log("filterImplementationProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
const regex = /^filter(\+)?\.\d+\.id$/;
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}`;
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);
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}`);
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
);
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);
// 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;
}
console.log(`Found ${links.length} implementations`);
return links;
} catch (error) {
console.error("Error get implementation", error);
}
} catch (error) {
console.error("Error get implementation", error);
}
}
return null;
return null;
}
},
}
);
@ -492,61 +496,63 @@ export const layerImplementationProvider =
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("layerImplementationProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
if (vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
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$/;
// Easiest regex in this package for sure
const regex = /^id$/;
if (regex.exec(jsonPath)) {
const layerId = getValueFromPath(text, rawJsonPath);
const to = `layers.${layerId}`;
if (regex.exec(jsonPath)) {
const layerId = getValueFromPath(text, rawJsonPath);
const to = `layers.${layerId}`;
try {
const cache = await Cache.create();
const references = cache.getReferences(to);
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}`);
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
);
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);
// 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;
}
console.log(`Found ${links.length} implementations`);
return links;
} catch (error) {
console.error("Error get implementation", error);
}
} catch (error) {
console.error("Error get implementation", error);
}
}
},

View file

@ -24,6 +24,11 @@ export class CacheWorker {
files: {},
};
/**
* File system watcher
*/
private watcher: vscode.FileSystemWatcher | undefined;
/**
* Creates a new cache
*
@ -51,6 +56,16 @@ export class CacheWorker {
this.createFileSystemWatcher();
}
/**
* Disposes the cache worker
*/
public dispose() {
// Save the cache before disposing
this.save();
// Dispose the file system watcher
this.watcher?.dispose();
}
/**
* Saves the current cache to the storage as JSON
* TODO: Find a more elegant way to do this
@ -84,16 +99,32 @@ export class CacheWorker {
}
}
/**
* Completely clears the cache
*/
private async clearCache() {
this.cache = { timestamp: 0, items: [], files: {} };
this.save();
}
/**
* Refreshes the cache
*/
async refreshCache() {
await this.clearCache();
await this.scanWorkspace();
}
/**
* Create a file system watcher
*/
private createFileSystemWatcher() {
const watcher = vscode.workspace.createFileSystemWatcher(
this.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);
this.watcher.onDidChange(this.onChanged, this, this.context.subscriptions);
this.watcher.onDidCreate(this.onCreated, this, this.context.subscriptions);
this.watcher.onDidDelete(this.onDeleted, this, this.context.subscriptions);
}
private onChanged(uri: vscode.Uri) {

View file

@ -33,16 +33,20 @@ export async function getAvailableLayers(): Promise<string[]> {
}
/**
* Utility function to get the tagRenderings from the questions layer
* Utility function to get the tagRenderings from all available layers
*
* 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
// First, we try to get the tagRenderings from the cache, if it is enabled
// If the cache is not available, instead return the tagRenderings from the questions layer
if (!vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
return getTagRenderingsUncached();
}
try {
const cache = await Cache.create();
return cache.getTagRenderings();
@ -52,46 +56,58 @@ export async function getTagRenderings(): Promise<vscode.CompletionItem[]> {
error
);
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;
return getTagRenderingsUncached();
}
}
/**
* Utility function to get the filters from the filters layer
* Utility function to get the tagRenderings from the questions layer
*
* @returns List of CompletionItems for tagRenderings
*/
async function getTagRenderingsUncached(): Promise<vscode.CompletionItem[]> {
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 all available layers
*
* 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
// First, we try to get the filters from the cache, if it is enabled
// If the cache is not available, instead return the filters from the filters layer
if (!vscode.workspace.getConfiguration("mapcomplete").get("caching")) {
return getFiltersUncached();
}
try {
const cache = await Cache.create();
return cache.getFilters();
@ -101,28 +117,37 @@ export async function getFilters(): Promise<vscode.CompletionItem[]> {
error
);
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;
return getFiltersUncached();
}
}
/**
* Utility function to get the filters from the filters layer
*
* @returns List of CompletionItems for tagRenderings
*/
async function getFiltersUncached(): Promise<vscode.CompletionItem[]> {
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;
}