🎉 Initial commit

This commit is contained in:
Robin van der Linde 2024-12-31 22:53:56 +01:00
commit 997a387a57
Signed by: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
17 changed files with 2302 additions and 0 deletions

0
.github/dependabot.yml vendored Normal file
View file

18
.github/workflows/build.yaml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Build extension
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Instal VSCE
run: npm install -g vsce
- name: Build
run: vsce package -o mapcomplete.vsix
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: mapcomplete
path: mapcomplete.vsix

11
.github/workflows/lint.yaml vendored Normal file
View file

@ -0,0 +1,11 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Lint
run: npm run lint

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
out
*.vsix

9
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

22
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,22 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
}
]
}

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"editor.tabSize": 2
}

20
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# MapCompleteVScode
This is a Visual Studio Code extension for [MapComplete](https://github.com/pietervdvn/MapComplete). It adds autocompletion and defintion support for the MapComplete theme and layer configuration files.
Not everything is supported yet, but currently the following features are supported:
- Autocompletion for the layer names
- Definition support for the layer names
- Definintion support for icons

44
eslint.config.mjs Normal file
View file

@ -0,0 +1,44 @@
/**
* ESLint configuration for the project.
*
* See https://eslint.style and https://typescript-eslint.io for additional linting options.
*/
// @ts-check
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
export default tseslint.config(
{
ignores: [
'.vscode-test',
'out',
]
},
js.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
{
plugins: {
'@stylistic': stylistic
},
rules: {
'curly': 'warn',
'@stylistic/semi': ['warn', 'always'],
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/naming-convention': [
'warn',
{
'selector': 'import',
'format': ['camelCase', 'PascalCase']
}
],
'@typescript-eslint/no-unused-vars': [
'error',
{
'argsIgnorePattern': '^_'
}
]
}
}
);

1703
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "mapcompletevscode",
"displayName": "MapComplete VScode",
"version": "0.0.1",
"publisher": "robinvanderlinde",
"author": {
"name": "Robin van der Linde",
"email": "r@rlin.eu",
"url": "https://rlin.eu"
},
"description": "MapComplete extension for Visual Studio Code",
"keywords": [
"mapcomplete",
"vscode",
"extension"
],
"private": true,
"license": "MIT",
"engines": {
"vscode": "^1.73.0"
},
"categories": [
"Other"
],
"activationEvents": [
"workspaceContains:**/assets/svg/mapcomplete_logo.svg"
],
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"lint": "eslint",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/node": "^20",
"@types/vscode": "^1.73.0",
"eslint": "^9.13.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0"
},
"dependencies": {
"jsonc-parser": "^3.3.1"
}
}

11
src/extension.ts Normal file
View file

@ -0,0 +1,11 @@
import * as vscode from "vscode";
import { layerCompletionProvider, layerDefinitionProvider } from "./theme";
import { iconDefinitionProvider } from "./generic";
export function activate(context: vscode.ExtensionContext) {
// Activate all theme related features
context.subscriptions.push(layerCompletionProvider, layerDefinitionProvider);
// Activate all generic features
context.subscriptions.push(iconDefinitionProvider);
}

91
src/generic.ts Normal file
View file

@ -0,0 +1,91 @@
/**
* This file contains some function that are used both in theme files, as well in layer files
*/
import * as vscode from "vscode";
import {
getCursorPath,
getRawCursorPath,
getStartEnd,
getValueFromPath,
} from "./utils";
import * as path from "path";
/**
* Icon definition provider
*
* This provider will provide a definition for icons, allowing users to jump to the icon file
*/
export const iconDefinitionProvider =
vscode.languages.registerDefinitionProvider(
{
language: "json",
scheme: "file",
pattern: "**/assets/**/*.json",
},
{
provideDefinition(
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("iconDefinitionProvider");
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const rawJsonPath = getRawCursorPath(text, position);
const regexes = [/icon$/, /icon.render/, /icon.mappings.\d+.then$/];
for (const regex of regexes) {
if (regex.exec(jsonPath)) {
const iconPath = getValueFromPath(text, rawJsonPath);
console.log("Found reference to icon", iconPath);
let fullIconPath: string;
// Check if the path starts with a dot, if so, it's a relative path
// if not, it's a built-in icon
if (!iconPath.startsWith(".")) {
fullIconPath = path.join(
(vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: "") || "",
"assets",
"svg",
iconPath + ".svg"
);
} else {
fullIconPath = path.join(
(vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: "") || "",
iconPath
);
}
const startEnd = getStartEnd(text, rawJsonPath);
console.log("fullIconPath", fullIconPath);
console.log(
"startEndLines",
startEnd.start.line,
startEnd.end.line
);
console.log(
"startEndChars",
startEnd.start.character,
startEnd.end.character
);
const link: vscode.DefinitionLink = {
targetUri: vscode.Uri.file(fullIconPath),
targetRange: new vscode.Range(0, 0, 0, 0),
originSelectionRange: startEnd,
};
return [link];
}
}
return [];
},
}
);

141
src/theme.ts Normal file
View file

@ -0,0 +1,141 @@
/**
* This file contains all functions that should be used when editing theme files
* Theme files are located in the /assets/themes/{THEME_NAME}/{THEME_NAME}.json file
*
* This consists of the following functions:
* - layerCompletionProvider: Provides a list of existing layers for autocompletion
* - layerDefinitionProvider: Provides a definition for layers, allowing users to jump to the layer definition
*/
import * as vscode from "vscode";
import * as path from "path";
import { getAvailableLayers, getCursorPath } from "./utils";
/**
* Layer completion provider
*
* This provider will provide a list of available layers for autocompletion
*
* JSON paths:
* - layers.{index} (If this is a string)
* - layers.{index}.builtin
*/
export const layerCompletionProvider =
vscode.languages.registerCompletionItemProvider(
{
language: "json",
scheme: "file",
pattern: "**/assets/themes/*/*.json",
},
{
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("layerCompletionProvider");
// Now we'll need to try and get the current path for the cursor
const text = document.getText();
// Now we need to get the current path
const jsonPath = getCursorPath(text, position);
const regex = /^layers\.\d+(.builtin)*$/;
if (regex.exec(jsonPath)) {
// We need to get the available layers
const layers = await getAvailableLayers();
console.log(`Got ${layers.length} layers`);
const items: vscode.CompletionItem[] = [];
for (const layer of layers) {
const item = new vscode.CompletionItem(layer);
item.kind = vscode.CompletionItemKind.Value;
items.push(item);
}
// Now we need to return the completion items
return items;
}
return [];
},
}
);
/**
* Layer definition provider
*
* This provider will provide a definition for layers, allowing users to jump to the layer definition
*
* JSON paths:
* - layers.{index} (If this is a string)
* - layers.{index}.builtin
*/
export const layerDefinitionProvider =
vscode.languages.registerDefinitionProvider(
{
language: "json",
scheme: "file",
pattern: "**/assets/themes/*/*.json",
},
{
provideDefinition(
document: vscode.TextDocument,
position: vscode.Position
) {
console.log("layerDefinitionProvider");
// We don't want to provide definitions for the license_info.json file
if (document.fileName.endsWith("license_info.json")) {
return null;
}
const text = document.getText();
const jsonPath = getCursorPath(text, position);
const json = JSON.parse(text);
const regex = /^layers\.\d+(.builtin)*$/;
if (regex.exec(jsonPath)) {
const layer = json.layers[parseInt(jsonPath.split(".")[1])];
// If an item is a string, it's a reference to a layer, also if it's an object and has a builtin property
if (typeof layer === "string") {
// We have a reference to a layer
const layerPath = path.join(
(vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: "") || "",
"assets",
"layers",
layer,
layer + ".json"
);
console.log("Found reference to layer", layerPath);
return new vscode.Location(
vscode.Uri.file(layerPath),
new vscode.Position(0, 0)
);
} else if (typeof layer === "object" && layer.builtin) {
// We have a builtin layer
const layerPath = path.join(
(vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri.fsPath
: "") || "",
"assets",
"layers",
layer.builtin,
layer.builtin + ".json"
);
console.log("Found reference to layer (as builtin)", layerPath);
return new vscode.Location(
vscode.Uri.file(layerPath),
new vscode.Position(0, 0)
);
}
}
return null;
},
}
);

154
src/utils.ts Normal file
View file

@ -0,0 +1,154 @@
import * as vscode from "vscode";
import * as path from "path";
import {
findNodeAtLocation,
getLocation,
JSONPath,
parseTree,
} from "jsonc-parser";
/**
* Utility function to get the JSON path at the cursor position, separated by dots
*
* @param jsonText Original unparsed JSON text
* @param position VScode cursor position
* @returns JSON path as a string, separated by dots
*/
export function getCursorPath(
jsonText: string,
position: vscode.Position
): string {
return getRawCursorPath(jsonText, position).join(".");
}
/**
* Utility function to get the JSON path at the cursor position
*
* @param jsonText Original unparsed JSON text
* @param position VScode cursor position
* @returns JSON path as an array of strings
*/
export function getRawCursorPath(
jsonText: string,
position: vscode.Position
): JSONPath {
const offset = positionToOffset(jsonText, position);
const location = getLocation(jsonText, offset);
return location.path;
}
/**
* Utility function to convert a VScode position to a numeric offset
*
* @param text Original text content
* @param position VScode cursor position
* @returns Offset
*/
function positionToOffset(text: string, position: vscode.Position): number {
const lines = text.split("\n");
let offset = 0;
for (let i = 0; i < position.line; i++) {
offset += lines[i].length + 1; // +1 for the newline character
}
offset += position.character;
return offset;
}
/**
* Function to get all available layers on disk
*
* In essence, we look for folders under the assets/layers directory
* and return the names of the folders
* @returns List of layer names
*/
export async function getAvailableLayers(): Promise<string[]> {
const layers: string[] = [];
let files = await vscode.workspace.findFiles(
"assets/layers/**/*.json",
"**/node_modules/**"
);
files = files.filter((file) => {
return !file.fsPath.includes("license_info");
});
for (const file of files) {
const layerName = path.basename(path.dirname(file.fsPath));
layers.push(layerName);
}
return layers;
}
/**
* Function to get a range of a JSON path
* Useful for creating document links
*
* @param json file content
* @param path JSON path
* @returns Range of the path
*/
export function getStartEnd(json: string, path: JSONPath): vscode.Range {
const rootNode = parseTree(json);
if (!rootNode) {
return new vscode.Range(0, 0, 0, 0);
} else {
const node = findNodeAtLocation(rootNode, path);
if (!node) {
return new vscode.Range(0, 0, 0, 0);
} else {
return new vscode.Range(
offsetToPosition(json, node.offset + 1),
offsetToPosition(json, node.offset + node.length - 1)
);
}
}
}
/**
* Utility function to convert an offset to a position
*
* @param text Text content
* @param offset Offset
* @returns Position
*/
function offsetToPosition(text: string, offset: number): vscode.Position {
const lines = text.split("\n");
let currentOffset = 0;
for (let i = 0; i < lines.length; i++) {
if (currentOffset + lines[i].length + 1 >= offset) {
return new vscode.Position(i, offset - currentOffset);
}
currentOffset += lines[i].length + 1;
}
return new vscode.Position(0, 0);
}
/**
* Utility function to get the value of a JSON path
*
* @param json Original JSON content
* @param path JSON path
* @returns Value of the path
*/
// 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) {
console.log("Root node not found");
return undefined;
} else {
console.log("Root node found");
const node = findNodeAtLocation(rootNode, path);
if (!node) {
return undefined;
} else {
console.log(
"Substring",
json.substring(node.offset, node.offset + node.length)
);
return JSON.parse(json.substring(node.offset, node.offset + node.length));
}
}
}

15
tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"lib": ["es2020"],
"outDir": "out",
"sourceMap": true,
"strict": true,
"rootDir": "src"
},
"exclude": [
"node_modules",
".vscode-test"
]
}