Skip to content

Commit

Permalink
Merge pull request #27 from williamthome/feat/model_snippet
Browse files Browse the repository at this point in the history
Feat/model snippet
  • Loading branch information
williamthome authored Jun 13, 2022
2 parents d5bdfc6 + af0089d commit d718f4e
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 15 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Also some special completions are provided, like the atoms `true`, `false` and `

![Snippets](images/snippets.gif)

A great help is the models snippets. Typing `m.` all models are listed and picking one shows all `m_get` possibilities.

![m_get snippets](images/m_get_snippets.gif)

### Go to definition

Navigate to files in the `.tpl` by pressing <kbd>Ctrl</kbd> + <kbd>Click</kbd> over file names.
Expand Down Expand Up @@ -66,7 +70,7 @@ npm install
code .
```

Make sure you have [TSlint](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) extension installed.
Make sure you have [ESlint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) extension installed.

### Debugging the extension

Expand Down
Binary file added images/m_get_snippets.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 5 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@
"onStartupFinished"
],
"capabilities": {
"definitionProvider": "true"
"definitionProvider": "true",
"hoverProvider": "true",
"completionProvider": {
"triggerCharacters": [ ".", "[", "{", "|" ]
}
},
"main": "./out/extension.js",
"contributes": {
"configurationDefaults": {
"editor.snippetSuggestions": "top"
},
"languages": [
{
"id": "tpl",
Expand Down Expand Up @@ -93,10 +94,6 @@
"language": "tpl",
"path": "./snippets/tpl-global-vars.code-snippets"
},
{
"language": "tpl",
"path": "./snippets/tpl-models.code-snippets"
},
{
"language": "tpl",
"path": "./snippets/tpl-validators.code-snippets"
Expand Down
6 changes: 0 additions & 6 deletions snippets/tpl-global-vars.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@
"prefix": "now",
"body": "now"
},
"m": {
"scope": "tpl",
"description": "m",
"prefix": "m.",
"body": "m.${1|acl,acl_rule,acl_user_group,admin,admin_blocks,admin_config,admin_identity,admin_menu,admin_status,auth2fa,authentication,backup,backup_revision,category,client_local_storage,client_session_storage,comment,config,content_group,custom_redirect,development,edge,editor_tinymce,email_dkim,email_receive_recipient,email_status,facebook,filestore,fileuploader,hierarchy,identity,image_edit,import_csv_data,l10n,linkedin,log,log_email,log_ui,mailinglist,media,microsoft,modules,mqtt_ticket,oauth2,oauth2_consumer,oauth2_service,predicate,ratelimit,req,rsc,rsc_gone,search,seo,seo_sitemap,server_storage,signup,site,site_update,ssl_letsencrypt,survey,sysconfig,template,tkvstore,translation,twitter|}"
},
"true": {
"scope": "tpl",
"description": "true",
Expand Down
90 changes: 90 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as vscode from 'vscode';
import axios from "axios";
import config from "./config";
import { m_get, 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
Expand Down Expand Up @@ -98,7 +99,96 @@ export function activate(context: vscode.ExtensionContext) {
});

context.subscriptions.push(hoverProvider);

const completionProvider = vscode.languages.registerCompletionItemProvider('tpl', {
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
_token: vscode.CancellationToken,
_context: vscode.CompletionContext
) {
const modelNameRe = /\bm\.(\w+)?/
const modelNameRange = document.getWordRangeAtPosition(position, modelNameRe)
if (!!modelNameRange && !modelNameRange.isEmpty) {
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)
if (!modelMatch || !modelMatch.length) {
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)
snippet.command = {
command: "tpl.snippet.pick",
title: "m_get",
arguments: [model, modelExpressionsFinder]
}
arr.push(snippet)
return arr
}, new Array<vscode.CompletionItem>())
}

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
}

const mSnippet = new vscode.CompletionItem("m")
mSnippet.insertText = new vscode.SnippetString("m")

return [
mSnippet
]
}
// })
}, ".", "[", "{", "|")

context.subscriptions.push(completionProvider)

vscode.commands.registerCommand('tpl.snippet.pick', async (model, modelExpressionsFinder) => {
const expressions: Array<Expression> = await modelExpressionsFinder(model)
if (expressions instanceof Error) {
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)
if (!token) {
throw (new Error(`Unexpected no token match in quick pick with label '${label}'`))
}

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
export function deactivate() { }

// Internal functions

const findFile: FindFile = async (pattern, ignorePattern = null) => {
const files = await vscode.workspace.findFiles(pattern, ignorePattern, 1);
return files.length
? files[0].fsPath
: undefined
}
140 changes: 140 additions & 0 deletions src/utils/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as fs from 'fs'

// Defs

export type Token = {
token: string,
editable?: boolean,
prefix?: string,
suffix?: string
}

export type Expression = {
expression: string,
tokens: Array<Token>,
snippet: string
}

export type FindFile = (pattern: string) => Promise<string | undefined>

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)
}

// Internal functions

async function getFileTokens(
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`)
if (!filePath) {
return new Error(`Could not find the model '${model}'.`)
}

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 matches.map(parseExpression)
}

function parseExpression(expression: string) {
const re = /\s*,\s*(?=(?:[^{]*{[^}]*})*[^}]*$)/
const tokens = expression.split(re).flatMap<Token>(parseExpressionToken)
const snippet = tokensToSnippet(tokens)

return {
expression,
tokens,
snippet
}
}

function parseExpressionToken(data: string) {
const constantRe = /(?<=<<\").*?(?=\">>)/
const constantMatch = constantRe.exec(data)

if (constantMatch && constantMatch.length === 1) {
return [
{
token: constantMatch[0],
prefix: "."
}
]
} else {
const propsRe = /(?<=\{\s*)(\w+)(?:\s*,\s*)(\w+)(?=\s*\})/
const propsMatch = propsRe.exec(data)
if (propsMatch && propsMatch.length === 3) {
return [
{
token: propsMatch[1],
editable: true,
prefix: "[{",
},
{
token: "Prop",
editable: true,
prefix: " ",
suffix: "="
},
{
token: "Value",
editable: true,
suffix: "}]",
}
]
} else {
const paramRe = /\w+/
const paramMatch = paramRe.exec(data)
if (paramMatch && paramMatch.length === 1) {
return [
{
token: data,
editable: true,
prefix: "[",
suffix: "]",
}
]
}
}
}
throw(new Error(`Unexpected no match in expression token parser for data '${data}'`))
}

function tokensToSnippet(tokens: Array<Token>) {
return tokens.reduce(
(snippet, { token, editable, prefix = "", suffix = "" }, i) => {
const acc = `${prefix}${editable ? `\${${i + 1}:${token}}` : token}${suffix}`
return snippet + acc
},
""
)
}

0 comments on commit d718f4e

Please sign in to comment.