diff --git a/package.json b/package.json index 94caccb..9b51592 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zotonic-ls", "displayName": "Zotonic", "description": "Syntax highlight for Zotonic templates (.tpl).", - "version": "0.4.0", + "version": "0.4.1", "publisher": "williamthome", "license": "Apache-2.0", "icon": "images/z_logo.png", @@ -36,7 +36,12 @@ "definitionProvider": "true", "hoverProvider": "true", "completionProvider": { - "triggerCharacters": [ ".", "[", "{", "|" ] + "triggerCharacters": [ + ".", + "[", + "{", + "|" + ] } }, "main": "./out/extension.js", @@ -141,4 +146,4 @@ "dependencies": { "axios": "^0.27.2" } -} +} \ No newline at end of file diff --git a/src/config/docs.ts b/src/config/docs.ts index 9e3b88b..71e077d 100644 --- a/src/config/docs.ts +++ b/src/config/docs.ts @@ -1,11 +1,11 @@ -import { Docs } from "../utils/docs" +import { Docs } from "../utils/docs"; export type DocsConfig = { docs: Docs, language: string, version: string, branch: string, -} +}; const docsConfig: DocsConfig = { docs: { @@ -464,6 +464,6 @@ const docsConfig: DocsConfig = { language: "en", version: "latest", branch: "master", -} +}; -export default docsConfig +export default docsConfig; diff --git a/src/config/index.ts b/src/config/index.ts index 5f299f3..b0274cc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,13 +1,13 @@ -import getDoc from "../utils/docs" -import docsConfig, { DocsConfig } from "./docs" +import getDoc from "../utils/docs"; +import docsConfig, { DocsConfig } from "./docs"; type Config = { docs: DocsConfig, -} +}; const config: Config = { docs: docsConfig -} +}; export default { defaults: config, @@ -17,4 +17,4 @@ export default { docsConfig.version, docsConfig.branch ) -} +}; diff --git a/src/extension.ts b/src/extension.ts index 7ae4ac7..511e4b8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import axios from "axios"; import config from "./config"; -import { m_get, Expression, FindFile } from './utils/snippets'; +import { mGetExpressions, Expression, FindFile } from './utils/snippets'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -21,54 +21,54 @@ export function activate(context: vscode.ExtensionContext) { // @see this question in StackOverflow here https://stackoverflow.com/questions/72554515/go-to-definition-highlight-in-vscode-lsp const definitionProvider = vscode.languages.registerDefinitionProvider('tpl', { async provideDefinition(document, position, _token) { - const wordRange = document.getWordRangeAtPosition(position, /(?<=").*?(?=")/) - if (!wordRange || wordRange.isEmpty) return + const wordRange = document.getWordRangeAtPosition(position, /(?<=").*?(?=")/); + if (!wordRange || wordRange.isEmpty) { return; }; - const text = document.getText(wordRange) - const lastDotIndex = text.lastIndexOf(".") - if (lastDotIndex <= 0) return + const text = document.getText(wordRange); + const lastDotIndex = text.lastIndexOf("."); + if (lastDotIndex <= 0) { return; }; - const ext = text.substring(lastDotIndex) - if (!ext) return + const ext = text.substring(lastDotIndex + 1); + if (!ext) { return; }; - const lastSlashIndex = text.lastIndexOf("/") - const fileName = text.substring(lastSlashIndex).replace("/", "") - if (!fileName) return + const lastSlashIndex = text.lastIndexOf("/"); + const fileName = text.substring(lastSlashIndex).replace("/", ""); + if (!fileName) { return; }; - const tplLocationPattern = "**/priv/templates/**/" - const jsLocationPattern = "**/priv/lib/**/" - const cssLocationPattern = "**/priv/lib/**/" - const imageLocationPattern = "**/priv/lib/images/**/" + const tplLocationPattern = "**/priv/templates/**/"; + const jsLocationPattern = "**/priv/lib/**/"; + const cssLocationPattern = "**/priv/lib/**/"; + const imageLocationPattern = "**/priv/lib/images/**/"; const extLocationPattern = { - ".tpl": tplLocationPattern, - ".js": jsLocationPattern, - ".css": cssLocationPattern, - ".apng": imageLocationPattern, - ".gif": imageLocationPattern, - ".ico": imageLocationPattern, - ".cur": imageLocationPattern, - ".jpg": imageLocationPattern, - ".jpeg": imageLocationPattern, - ".jfif": imageLocationPattern, - ".pjpeg": imageLocationPattern, - ".pjp": imageLocationPattern, - ".png": imageLocationPattern, - ".svg": imageLocationPattern, - } - - const locationPattern = extLocationPattern[ext as keyof typeof extLocationPattern] - if (!locationPattern) return + tpl: tplLocationPattern, + js: jsLocationPattern, + css: cssLocationPattern, + apng: imageLocationPattern, + gif: imageLocationPattern, + ico: imageLocationPattern, + cur: imageLocationPattern, + jpg: imageLocationPattern, + jpeg: imageLocationPattern, + jfif: imageLocationPattern, + pjpeg: imageLocationPattern, + pjp: imageLocationPattern, + png: imageLocationPattern, + svg: imageLocationPattern, + }; + + const locationPattern = extLocationPattern[ext as keyof typeof extLocationPattern]; + if (!locationPattern) { return; }; const filePattern = `${locationPattern}${fileName}`; - const ignorePattern = "**/_build/**" + const ignorePattern = "**/_build/**"; const files = await vscode.workspace.findFiles(filePattern, ignorePattern); - if (!files) return; + if (!files) { return; } return files.map(uri => new vscode.Location( uri, new vscode.Position(0, 0) - )) + )); } }); @@ -77,17 +77,17 @@ export function activate(context: vscode.ExtensionContext) { const hoverProvider = vscode.languages.registerHoverProvider('tpl', { async provideHover(document, position, _token) { const patternMatch = (pattern: RegExp) => { - const wordRange = document.getWordRangeAtPosition(position, pattern) + const wordRange = document.getWordRangeAtPosition(position, pattern); return !!wordRange && !wordRange.isEmpty ? document.getText(wordRange) - : undefined - } + : undefined; + }; - const doc = config.getDoc(patternMatch) - if (!doc || !doc.raw) return + const doc = config.getDoc(patternMatch); + if (!doc || !doc.raw) { return; }; const response = await axios.get(doc.raw); - if (response.status !== 200) return + if (response.status !== 200) { return; }; const markdown = new vscode.MarkdownString(response.data); markdown.supportHtml = true; @@ -107,78 +107,77 @@ export function activate(context: vscode.ExtensionContext) { _token: vscode.CancellationToken, _context: vscode.CompletionContext ) { - const modelNameRe = /\bm\.(\w+)?/ - const modelNameRange = document.getWordRangeAtPosition(position, modelNameRe) + const modelNameRe = /\bm\.(\w+)?/; + const modelNameRange = document.getWordRangeAtPosition(position, modelNameRe); if (!!modelNameRange && !modelNameRange.isEmpty) { - const modelsPattern = "{apps,apps_user}/**/src/models/**/m_*.erl" + const modelsPattern = "{apps,apps_user}/**/src/models/**/m_*.erl"; const models = await vscode.workspace.findFiles(modelsPattern); return models.reduce((arr, file) => { - const modelRe = /(?<=\bm_).*?(?=.erl)/ - const modelMatch = modelRe.exec(file.fsPath) + const modelRe = /(?<=\bm_).*?(?=.erl)/; + const modelMatch = modelRe.exec(file.fsPath); if (!modelMatch || !modelMatch.length) { - return arr + return arr; } - const model = modelMatch[0] - const modelExpressionsFinder = (m: string) => m_get(findFile, m) - const snippet = new vscode.CompletionItem(model) - snippet.insertText = new vscode.SnippetString(model) + const model = modelMatch[0]; + const modelExpressionsFinder = (m: string) => mGetExpressions(findFile, m); + const snippet = new vscode.CompletionItem(model); + snippet.insertText = new vscode.SnippetString(model); snippet.command = { command: "tpl.snippet.pick", title: "m_get", arguments: [model, modelExpressionsFinder] - } - arr.push(snippet) - return arr - }, new Array()) + }; + arr.push(snippet); + return arr; + }, new Array()); } - const variableRe = /(?<=\[).*?(?=\])|(?<={).*?(?=})|(?<=:).*?(?=}|,)|(?<==).*?(?=(}|,|%}))/ - const variableRange = document.getWordRangeAtPosition(position, variableRe) + const variableRe = /(?<=\[).*?(?=\])|(?<={).*?(?=})|(?<=:).*?(?=}|,)|(?<==).*?(?=(}|,|%}))/; + const variableRange = document.getWordRangeAtPosition(position, variableRe); if (!!variableRange) { // TODO: Variables snippets. // It will be awesome if variables can pop up as suggestion. - return + return; } - const mSnippet = new vscode.CompletionItem("m") - mSnippet.insertText = new vscode.SnippetString("m") + const mSnippet = new vscode.CompletionItem("m"); + mSnippet.insertText = new vscode.SnippetString("m"); return [ mSnippet - ] + ]; } - // }) - }, ".", "[", "{", "|") + }, ".", "[", "{", "|"); - context.subscriptions.push(completionProvider) + context.subscriptions.push(completionProvider); vscode.commands.registerCommand('tpl.snippet.pick', async (model, modelExpressionsFinder) => { - const expressions: Array = await modelExpressionsFinder(model) + const expressions: Array = await modelExpressionsFinder(model); if (expressions instanceof Error) { - await vscode.window.showErrorMessage(expressions.message) - return undefined + await vscode.window.showErrorMessage(expressions.message); + return undefined; } const quickPick = vscode.window.createQuickPick(); quickPick.items = expressions.map(({ expression: label }) => ({ label })); quickPick.onDidChangeSelection(async ([{ label }]) => { - const token = expressions.find(token => token.expression === label) + const token = expressions.find(token => token.expression === label); if (!token) { - throw (new Error(`Unexpected no token match in quick pick with label '${label}'`)) + throw (new Error(`Unexpected no token match in quick pick with label '${label}'`)); } - await vscode.commands.executeCommand("tpl.snippet.insert", token.snippet) + await vscode.commands.executeCommand("tpl.snippet.insert", token.snippet); quickPick.hide(); }); quickPick.show(); - }) + }); vscode.commands.registerTextEditorCommand('tpl.snippet.insert', (editor, _edit, snippet) => { return editor.insertSnippet( new vscode.SnippetString(snippet), ); - }) + }); } // this method is called when your extension is deactivated @@ -190,5 +189,5 @@ const findFile: FindFile = async (pattern, ignorePattern = null) => { const files = await vscode.workspace.findFiles(pattern, ignorePattern, 1); return files.length ? files[0].fsPath - : undefined -} + : undefined; +}; diff --git a/examples/test.tpl b/src/test/test.tpl similarity index 100% rename from examples/test.tpl rename to src/test/test.tpl diff --git a/src/utils/docs.ts b/src/utils/docs.ts index d2bb18b..063c556 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -4,31 +4,31 @@ export type Doc = { pattern: RegExp, genUri: (token: string) => string, tokens?: string[] -} +}; -export type Docs = { [key: string]: Doc } +export type Docs = { [key: string]: Doc }; // API export default function (docs: Docs, language: string, version: string, branch: string) { return function (patternMatch: (pattern: RegExp) => string | undefined) { - const uri = maybeGenDocUri(docs, patternMatch) + const uri = maybeGenDocUri(docs, patternMatch); return { uri: maybeGenDocUri(docs, patternMatch), url: uri ? genDocUrl(language, version, uri) : undefined, raw: uri ? genDocRawUrl(branch, uri) : undefined, - } - } + }; + }; } // Internal functions function genDocUrl(language: string, version: string, uri = "/") { - return `http://docs.zotonic.com/${language}/${version}${uri}` + return `http://docs.zotonic.com/${language}/${version}${uri}`; } function genDocRawUrl(branch: string, uri: string) { - return `https://raw.githubusercontent.com/zotonic/zotonic/${branch}/doc${uri}.rst` + return `https://raw.githubusercontent.com/zotonic/zotonic/${branch}/doc${uri}.rst`; } function maybeGenDocUri( @@ -36,8 +36,8 @@ function maybeGenDocUri( patternMatch: (pattern: RegExp) => string | undefined ) { for (const doc of Object.values(docs)) { - const token = patternMatch(doc.pattern) - if (!token || (doc.tokens?.length && !doc.tokens.includes(token))) continue - return doc.genUri(token) + const token = patternMatch(doc.pattern); + if (!token || (doc.tokens?.length && !doc.tokens.includes(token))) { continue; }; + return doc.genUri(token); } } diff --git a/src/utils/snippets.ts b/src/utils/snippets.ts index 94353b1..8cb1080 100644 --- a/src/utils/snippets.ts +++ b/src/utils/snippets.ts @@ -1,4 +1,4 @@ -import * as fs from 'fs' +import * as fs from 'fs'; // Defs @@ -7,80 +7,62 @@ export type Token = { editable?: boolean, prefix?: string, suffix?: string -} +}; export type Expression = { expression: string, tokens: Array, snippet: string -} +}; -export type FindFile = (pattern: string) => Promise +export type FindFile = (pattern: string) => Promise; -type Behaviour = "m_get" | "m_post" | "m_delete" +type Behaviour = "m_get" | "m_post" | "m_delete"; // API -export function behaviourFactory(findFile: FindFile) { - return (behaviour: Behaviour) => { - switch(behaviour) { - case "m_get": - return m_get - default: - throw(new Error(`Behaviour '${behaviour}' not implemented.`)) - } - } -} - -export async function m_get(findFile: FindFile, model: string) { - const re = /(?<=m_get\s*\(\s*\[\s*)(\w|<|\{).*?(?=\s*(\||\]))/g - return await getFileTokens("m_get", re, findFile, model) +export async function mGetExpressions(findFile: FindFile, model: string) { + const re = /(?<=m_get\s*\(\s*\[\s*)(\w|<|\{).*?(?=\s*(\||\]))/g; + return await getFileExpressions("m_get", re, findFile, model); } // Internal functions -async function getFileTokens( +async function getFileExpressions( behaviour: Behaviour, re: RegExp, findFile: FindFile, model: string, ) { - // const files = await vscode.workspace.findFiles(`{apps,apps_user}/**/src/models/**/m_${model}.erl`, null, 1); - // if (!files.length) { - // return new Error(`Could not find the model '${model}'.`) - // } - - // const filePath = files[0].fsPath - - const filePath = await findFile(`{apps,apps_user}/**/src/models/**/m_${model}.erl`) + const filePath = await findFile(`{apps,apps_user}/**/src/models/**/m_${model}.erl`); if (!filePath) { - return new Error(`Could not find the model '${model}'.`) + return new Error(`Could not find the model '${model}'.`); } - const data = await fs.promises.readFile(filePath, { encoding: "utf8" }) - const matches = data.match(re) + const data = await fs.promises.readFile(filePath, { encoding: "utf8" }); + const matches = data.match(re); if (!matches) { - return new Error(`The model '${model}' have no match for '${behaviour}'.`) + return new Error(`The model '${model}' have no match for '${behaviour}'.`); } - return matches.map(parseExpression) + return matches.map(parseExpression); } function parseExpression(expression: string) { - const re = /\s*,\s*(?=(?:[^{]*{[^}]*})*[^}]*$)/ - const tokens = expression.split(re).flatMap(parseExpressionToken) - const snippet = tokensToSnippet(tokens) + const re = /\s*,\s*(?=(?:[^{]*{[^}]*})*[^}]*$)/; + const tokens = expression.split(re).flatMap(parseExpressionToken); + const snippet = tokensToSnippet(tokens); return { expression, tokens, snippet - } + }; } function parseExpressionToken(data: string) { - const constantRe = /(?<=<<\").*?(?=\">>)/ - const constantMatch = constantRe.exec(data) + const constantRe = /(?<=<<\").*?(?=\">>)/; + const constantMatch = constantRe.exec(data); if (constantMatch && constantMatch.length === 1) { return [ @@ -88,10 +70,10 @@ function parseExpressionToken(data: string) { token: constantMatch[0], prefix: "." } - ] + ]; } else { - const propsRe = /(?<=\{\s*)(\w+)(?:\s*,\s*)(\w+)(?=\s*\})/ - const propsMatch = propsRe.exec(data) + const propsRe = /(?<=\{\s*)(\w+)(?:\s*,\s*)(\w+)(?=\s*\})/; + const propsMatch = propsRe.exec(data); if (propsMatch && propsMatch.length === 3) { return [ { @@ -110,10 +92,10 @@ function parseExpressionToken(data: string) { editable: true, suffix: "}]", } - ] + ]; } else { - const paramRe = /\w+/ - const paramMatch = paramRe.exec(data) + const paramRe = /\w+/; + const paramMatch = paramRe.exec(data); if (paramMatch && paramMatch.length === 1) { return [ { @@ -122,19 +104,19 @@ function parseExpressionToken(data: string) { prefix: "[", suffix: "]", } - ] + ]; } } } - throw(new Error(`Unexpected no match in expression token parser for data '${data}'`)) + throw (new Error(`Unexpected no match in expression token parser for data '${data}'`)); } function tokensToSnippet(tokens: Array) { return tokens.reduce( (snippet, { token, editable, prefix = "", suffix = "" }, i) => { - const acc = `${prefix}${editable ? `\${${i + 1}:${token}}` : token}${suffix}` - return snippet + acc + const acc = `${prefix}${editable ? `\${${i + 1}:${token}}` : token}${suffix}`; + return snippet + acc; }, "" - ) + ); } diff --git a/syntaxes/html-tpl.tmLanguage.json b/syntaxes/html-tpl.tmLanguage.json index 46bc62e..001b237 100644 --- a/syntaxes/html-tpl.tmLanguage.json +++ b/syntaxes/html-tpl.tmLanguage.json @@ -7,4 +7,4 @@ "include": "text.html.basic" } ] -} +} \ No newline at end of file diff --git a/syntaxes/js-tpl.tmLanguage.json b/syntaxes/js-tpl.tmLanguage.json index ebe3fe2..f04135d 100644 --- a/syntaxes/js-tpl.tmLanguage.json +++ b/syntaxes/js-tpl.tmLanguage.json @@ -7,4 +7,4 @@ "include": "source.js" } ] -} +} \ No newline at end of file diff --git a/syntaxes/tpl.tmLanguage.json b/syntaxes/tpl.tmLanguage.json index 0be50a6..55f31c1 100644 --- a/syntaxes/tpl.tmLanguage.json +++ b/syntaxes/tpl.tmLanguage.json @@ -626,4 +626,4 @@ } } } -} +} \ No newline at end of file