diff --git a/README.md b/README.md index bd3b0cf..9aedc2d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ endsnippet - vscode, [Visual Studio Code](https://code.visualstudio.com/docs/editor/userdefinedsnippets) - atom, [Atom](https://flight-manual.atom.io/using-atom/sections/snippets/) - sublime , [Sublime Text](http://www.sublimetext.info/docs/en/extensibility/snippets.html) +- jetbrains , [JetBrains live template](https://www.jetbrains.com/help/idea/using-live-templates.html) ## Usage diff --git a/packages/jetbrains/__tests__/index.spec.ts b/packages/jetbrains/__tests__/index.spec.ts new file mode 100644 index 0000000..1d8ecd6 --- /dev/null +++ b/packages/jetbrains/__tests__/index.spec.ts @@ -0,0 +1,4 @@ +describe('jetbrains tests', () => { + it('setup done', () => { + }) +}) \ No newline at end of file diff --git a/packages/jetbrains/package.json b/packages/jetbrains/package.json new file mode 100644 index 0000000..755214b --- /dev/null +++ b/packages/jetbrains/package.json @@ -0,0 +1,38 @@ +{ + "name": "@unisnips/jetbrains", + "version": "0.5.1-alpha.0", + "description": "Utilities for generating JetBrains live templates in unisnips project", + "keywords": [ + "unisnips", + "snippets", + "live-template", + "jetbrains" + ], + "author": "hikperig ", + "license": "MIT", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "files": [ + "lib", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "git@github.com:hikerpig/unisnips.git" + }, + "scripts": { + "build": "tsc --pretty", + "dev": "tsc --watch", + "test": "jest" + }, + "dependencies": { + }, + "devDependencies": { + "jest": "*", + "typescript": "~3.7.2" + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/jetbrains/src/generate.ts b/packages/jetbrains/src/generate.ts new file mode 100644 index 0000000..c8350f2 --- /dev/null +++ b/packages/jetbrains/src/generate.ts @@ -0,0 +1,232 @@ +import path from 'path' +import { + SnippetDefinition, + GenerateOptions, + SnippetPlaceholder, + PlaceholderReplacement, + UnisnipsGenerator, + applyReplacements, +} from '@unisnips/core' + +type ResultPair = { + variable: JetBrainsVariable + replacement: PlaceholderReplacement +} + +function detectVariableReplacements(placeholders: SnippetPlaceholder[]): ResultPair[] { + const resultPairs: ResultPair[] = [] + placeholders.forEach(placeholder => { + const { valueType, variable, description, index } = placeholder + let newDesc: string + let jbVariable: JetBrainsVariable + if (valueType === 'positional') { + // TODO: transform ? + if (placeholder.transform) { + const transform = placeholder.transform + const transformStr = ['', transform.search, transform.replace, transform.options].join('/') + newDesc = `$\{${index}${transformStr}\}` + } else { + newDesc = `$${index}$` + jbVariable = { + name: index.toString(), + defaultValue: index.toString(), + alwaysStopAt: true, + } + } + } else if (valueType === 'variable') { + // if (variable.type === 'builtin') { + // newDesc = variable.name + // } + } else if (valueType === 'script') { + console.warn('[jetbrains] script placeholder is not supported') + } + if (jbVariable) { + const replacement: PlaceholderReplacement = { + type: 'string', + placeholder, + replaceContent: newDesc, + } + resultPairs.push({ + variable: jbVariable, + replacement, + }) + } + }) + return resultPairs +} + +type JetBrainsVariable = { + name: string + expression?: string + defaultValue: string + alwaysStopAt?: boolean +} + +type JetBrainsSnippetItem = { + name: string + value: string + description: string + variables?: JetBrainsVariable[] + contexts?: string[] +} + +type XMLAttribute = { + name: string + value: string | boolean | number +} + +type XMLNode = { + tagName: string + attributes: XMLAttribute[] + children?: XMLNode[] +} + +function indent(str: string, cols: number) { + let prefix = '' + for (let i = 0; i < cols; i++) { + prefix += ' ' + } + return str + .split('\n') + .map(l => { + return prefix + l + }) + .join('\n') +} + +function nodeTreeToXml(root: XMLNode, level = 0): string { + const attrStr = root.attributes + .filter(attr => attr.value !== undefined) + .map(attr => { + return `${attr.name}="${attr.value.toString()}"` + }) + .join(' ') + const childrenStr = root.children + ? root.children + .map(child => { + return nodeTreeToXml(child, 1) + }) + .join('\n') + : '' + + if (childrenStr) { + return indent( + `<${root.tagName} ${attrStr}> +${childrenStr} +`, + level * 2, + ) + } else { + return indent(`<${root.tagName} ${attrStr} />`, level * 2) + } +} + +const JB_LANG_MAP: { [key: string]: string } = { + other: 'OTHER', + sh: 'SHELL_SCRIPT', + bash: 'SHELL_SCRIPT', + javascript: 'JAVA_SCRIPT', + typescript: 'TypeScript', + css: 'CSS', + python: 'Python', + xml: 'XML', + html: 'HTML', + sql: 'SQL', +} + +function attr(name: string, value: string | boolean) { + return { name, value } +} + +function makeTag(name: string, attrs: XMLAttribute[], children?: XMLNode[]): XMLNode { + return { + tagName: name, + attributes: attrs, + children, + } +} + +/* eslint-disable prettier/prettier */ +function snippetItemToXml(item: JetBrainsSnippetItem) { + const uniqueVariableMap: {[key: string]: JetBrainsVariable} = {} + if (item.variables) { + item.variables.forEach((variable) => { + const k = [variable.name, variable.defaultValue, variable.expression].join('_') + if (!uniqueVariableMap[k]) { + uniqueVariableMap[k] = variable + } + }) + } + + const variableNodes = Object.values(uniqueVariableMap).map(variable => { + return makeTag('variable', [ + attr('name', variable.name), + attr('expression', variable.expression), + attr('defaultValue', variable.defaultValue), + attr('alwaysStopAt', variable.alwaysStopAt), + ]) + }) + + const contextNodes = (item.contexts || []).map(contextName => { + return makeTag( + 'context', + [], + [makeTag('option', [attr('name', contextName), attr('value', 'true')])], + ) + }) + + const escapedValue = item.value.replace(/\n/g, ' ') + const template = makeTag( + 'template', + [attr('name', item.name), attr('description', item.description), attr('value', escapedValue)], + [...variableNodes, ...contextNodes], + ) + return nodeTreeToXml(template) +} +/* eslint-enable prettier/prettier */ + +export const generateSnippets: UnisnipsGenerator['generateSnippets'] = ( + defs: SnippetDefinition[], + opts: GenerateOptions = {}, +) => { + const segs: string[] = [] + let langName: string + let groupName = 'unisnips' + if (opts.snippetsFilePath) { + langName = path.basename(opts.snippetsFilePath).replace(/\..*/, '') + groupName = `unisnips-${langName}` + } + + defs.forEach(def => { + const pairs = detectVariableReplacements(def.placeholders) + const replacements = pairs.map(o => o.replacement) + const variables: JetBrainsVariable[] = [] + pairs.forEach(({ variable }) => { + variables.push(variable) + }) + const newBody = applyReplacements(def, replacements) + const item: JetBrainsSnippetItem = { + name: def.trigger, + value: newBody, + description: def.description, + variables, + } + let jbContextName = JB_LANG_MAP.other + if (langName) { + jbContextName = JB_LANG_MAP[langName] + } + if (jbContextName) { + item.contexts = [jbContextName] + } + segs.push(snippetItemToXml(item)) + }) + const snippetsContent = segs.join('\n') + const content = [ + ``, + `${indent(snippetsContent, 2)}`, + '', + ].join('\n') + return { + content, + } +} diff --git a/packages/jetbrains/src/index.ts b/packages/jetbrains/src/index.ts new file mode 100644 index 0000000..cf97b23 --- /dev/null +++ b/packages/jetbrains/src/index.ts @@ -0,0 +1,9 @@ +import { UnisnipsGenerator } from '@unisnips/core' + +import { generateSnippets } from './generate' + +const PLUGIN_JETBRAINS: UnisnipsGenerator = { + generateSnippets, +} + +export default PLUGIN_JETBRAINS diff --git a/packages/jetbrains/tsconfig.json b/packages/jetbrains/tsconfig.json new file mode 100644 index 0000000..60ba5f7 --- /dev/null +++ b/packages/jetbrains/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/unisnips/__tests__/to-jetbrains.spec.ts b/packages/unisnips/__tests__/to-jetbrains.spec.ts new file mode 100644 index 0000000..4700fc2 --- /dev/null +++ b/packages/unisnips/__tests__/to-jetbrains.spec.ts @@ -0,0 +1,31 @@ +import outdent from 'outdent' + +import { ULTI_SNIPPETS } from '../../../tools/test-tool/src/ultisnips' + +import { convert } from '../src/index' + +import { ParseOptions } from '@unisnips/core' + +describe('convert to jetbrains live template', () => { + const convertToJetBrains = (inputContent: string, opts: ParseOptions = {}) => { + return convert({ + target: 'jetbrains', + inputContent, + ...opts, + }) + } + + it('generate right placeholder', () => { + const { content } = convertToJetBrains(ULTI_SNIPPETS.SIMPLE) + // console.log(content) + expect(content).toEqual(outdent` + + + `) + }) +}) diff --git a/packages/unisnips/package.json b/packages/unisnips/package.json index d57fdb0..4c16855 100644 --- a/packages/unisnips/package.json +++ b/packages/unisnips/package.json @@ -27,6 +27,7 @@ "dependencies": { "@unisnips/atom": "^0.5.1-alpha.0", "@unisnips/core": "^0.5.1-alpha.0", + "@unisnips/jetbrains": "^0.5.1-alpha.0", "@unisnips/sublime": "^0.5.1-alpha.0", "@unisnips/ultisnips": "^0.5.1-alpha.0", "@unisnips/vscode": "^0.5.1-alpha.0", diff --git a/packages/unisnips/src/services/convert.ts b/packages/unisnips/src/services/convert.ts index 354773f..62e07c5 100644 --- a/packages/unisnips/src/services/convert.ts +++ b/packages/unisnips/src/services/convert.ts @@ -7,9 +7,11 @@ import { } from '@unisnips/core' import PLUGIN_ULTISNIPS from '@unisnips/ultisnips' + import PLUGIN_VSCODE from '@unisnips/vscode' import PLUGIN_ATOM from '@unisnips/atom' import PLUGIN_SUBLIME from '@unisnips/sublime' +import PLUGIN_JETBRAINS from '@unisnips/jetbrains' const UNISNIPS_SUPPORTED_SOURCES = { ultisnips: 'ultisnips', @@ -19,6 +21,7 @@ const UNISNIPS_SUPPORTED_TARGETS = { vscode: 'vscode', atom: 'atom', sublime: 'sublime', + jetbrains: 'jetbrains', } class PluginManager { @@ -52,6 +55,7 @@ pluginManager.registerParser(UNISNIPS_SUPPORTED_SOURCES.ultisnips, PLUGIN_ULTISN pluginManager.registerGenerator(UNISNIPS_SUPPORTED_TARGETS.vscode, PLUGIN_VSCODE) pluginManager.registerGenerator(UNISNIPS_SUPPORTED_TARGETS.atom, PLUGIN_ATOM) pluginManager.registerGenerator(UNISNIPS_SUPPORTED_TARGETS.sublime, PLUGIN_SUBLIME) +pluginManager.registerGenerator(UNISNIPS_SUPPORTED_TARGETS.jetbrains, PLUGIN_JETBRAINS) // ----------------end Register plugins ------------------- type UnisnipsParseOptions = ParseOptions & {