├── .gitignore ├── README.md ├── helix-snippets-ls.gif ├── out ├── server.js └── server.js.map ├── package.json ├── pnpm-lock.yaml ├── src └── server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helix-snippets-ls 2 | 3 | [![npm version](http://img.shields.io/npm/v/helix-snippets-ls.svg?style=flat)](https://npmjs.org/package/helix-snippets-ls 'View this project on npm') 4 | 5 | Language server for snippets in Helix 6 | 7 | This package allows you to add snippets to the [Helix](https://helix-editor.com) editor 8 | using a LSP server for autocompleting snippets. 9 | 10 | You can define your snippets in a TOML file and use it in Helix. 11 | 12 | ![helix snippets demo](helix-snippets-ls.gif) 13 | 14 | 15 | ## Install 16 | ``` 17 | npm i -g helix-snippets-ls 18 | ``` 19 | 20 | 21 | ### Usage 22 | Create an `snippets.toml` file inside your Helix config dir `~/.config/helix/snippets.toml` 23 | 24 | The snippets follow the [VSCode snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets) standard. 25 | Add snippets inside it like: 26 | 27 | ```toml 28 | li = "
  • $0
  • " 29 | img = "\"$2\"" 30 | inbx = "$0" 31 | 32 | # Multiline snippets 33 | for = """for await (const ${1:iterator} of ${2:object}) { 34 | \t$0 35 | }""" 36 | sim = "setImmediate(() => {\n\t${0}\n})" 37 | 38 | ``` 39 | 40 | ### Config 41 | Config your `languages.toml` to use this Language server 42 | 43 | ```toml 44 | [[language]] 45 | name = "handlebars" 46 | roots = ["package.json"] 47 | file-types = ["hbs"] 48 | scope = "source.hbs" 49 | language-server = { command = "helix-snippets-ls" } 50 | ``` 51 | 52 | ### Known limitations 53 | - The language server can be only used for one language at a time 54 | -------------------------------------------------------------------------------- /helix-snippets-ls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajasegar/helix-snippets-ls/dc03c431e1aeff2fa827a009c6cbe5b67a0f7a79/helix-snippets-ls.gif -------------------------------------------------------------------------------- /out/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | const fs = require("node:fs"); 5 | const toml = require("toml"); 6 | const os = require("node:os"); 7 | const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); 8 | const node_1 = require("vscode-languageserver/node"); 9 | console.log(`Inside lsp: ${new Date().toTimeString()}`); 10 | const contents = fs.readFileSync(`${os.homedir()}/.config/helix/snippets.toml`, "utf8"); 11 | const snippets = toml.parse(contents); 12 | const connection = (0, node_1.createConnection)(process.stdin, process.stdout); 13 | //Create a simple text document manager 14 | const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument); 15 | let hasConfigurationCapability = false; 16 | let hasWorkspaceFolderCapability = false; 17 | connection.onInitialize((params) => { 18 | const capabilities = params.capabilities; 19 | console.log("Initializing unity frontend lsp..."); 20 | // Does the client support the `workspace/configuration` request? 21 | // If not, we fall back using global settings. 22 | hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration); 23 | hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders); 24 | const triggerCharacters = Object.keys(snippets); 25 | const result = { 26 | capabilities: { 27 | textDocumentSync: node_1.TextDocumentSyncKind.Incremental, 28 | // Tell the client that this server supports code completion. 29 | completionProvider: { 30 | resolveProvider: true, 31 | triggerCharacters: triggerCharacters, 32 | }, 33 | }, 34 | }; 35 | if (hasWorkspaceFolderCapability) { 36 | result.capabilities.workspace = { 37 | workspaceFolders: { 38 | supported: true, 39 | }, 40 | }; 41 | } 42 | console.log(result); 43 | return result; 44 | }); 45 | connection.onInitialized(() => { 46 | if (hasConfigurationCapability) { 47 | // Register for all configuration changes. 48 | connection.client.register(node_1.DidChangeConfigurationNotification.type, undefined); 49 | } 50 | if (hasWorkspaceFolderCapability) { 51 | connection.workspace.onDidChangeWorkspaceFolders((_event) => { 52 | connection.console.log("Workspace folder change event received."); 53 | }); 54 | } 55 | }); 56 | // This handler provides the initial list of the completion items. 57 | connection.onCompletion((textDocumentPosition) => { 58 | try { 59 | const docs = documents.get(textDocumentPosition.textDocument.uri); 60 | if (!docs) 61 | throw "failed to find document"; 62 | const languageId = docs.languageId; 63 | const content = docs.getText(); 64 | const linenr = textDocumentPosition.position.line; 65 | const line = String(content.split(/\r?\n/g)[linenr]); 66 | const character = textDocumentPosition.position.character; 67 | return Object.keys(snippets).map((key, idx) => { 68 | const label = snippets[key]; 69 | return { 70 | label, 71 | kind: node_1.CompletionItemKind.Snippet, 72 | data: idx + 1 73 | }; 74 | }); 75 | } 76 | catch (error) { 77 | connection.console.log(`ERR: ${error}`); 78 | } 79 | return []; 80 | }); 81 | // This handler resolve additional information for the item selected in 82 | // the completion list. 83 | connection.onCompletionResolve((item) => { 84 | /* 85 | if (item.data === 1) { 86 | item.detail = 'TypeScript details', 87 | item.documentation = 'TypeScript documentation' 88 | } else if (item.data === 2) { 89 | item.detail = 'JavaScript details', 90 | item.documentation = 'JavaScript documentation' 91 | } 92 | */ 93 | return item; 94 | }); 95 | documents.listen(connection); 96 | connection.listen(); 97 | //# sourceMappingURL=server.js.map -------------------------------------------------------------------------------- /out/server.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helix-snippets-ls", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "npm run build && npm link", 9 | "build": "tsc" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@types/node": "^20.4.0", 16 | "toml": "^3.0.0", 17 | "typescript": "^5.1.6", 18 | "vscode-languageserver": "^8.1.0", 19 | "vscode-languageserver-textdocument": "^1.0.8" 20 | }, 21 | "bin": { 22 | "helix-snippets-ls": "./out/server.js" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | '@types/node': 5 | specifier: ^20.4.0 6 | version: 20.4.0 7 | toml: 8 | specifier: ^3.0.0 9 | version: 3.0.0 10 | typescript: 11 | specifier: ^5.1.6 12 | version: 5.1.6 13 | vscode-languageserver: 14 | specifier: ^8.1.0 15 | version: 8.1.0 16 | vscode-languageserver-textdocument: 17 | specifier: ^1.0.8 18 | version: 1.0.8 19 | 20 | packages: 21 | 22 | /@types/node@20.4.0: 23 | resolution: {integrity: sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==} 24 | dev: false 25 | 26 | /toml@3.0.0: 27 | resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} 28 | dev: false 29 | 30 | /typescript@5.1.6: 31 | resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} 32 | engines: {node: '>=14.17'} 33 | hasBin: true 34 | dev: false 35 | 36 | /vscode-jsonrpc@8.1.0: 37 | resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==} 38 | engines: {node: '>=14.0.0'} 39 | dev: false 40 | 41 | /vscode-languageserver-protocol@3.17.3: 42 | resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==} 43 | dependencies: 44 | vscode-jsonrpc: 8.1.0 45 | vscode-languageserver-types: 3.17.3 46 | dev: false 47 | 48 | /vscode-languageserver-textdocument@1.0.8: 49 | resolution: {integrity: sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==} 50 | dev: false 51 | 52 | /vscode-languageserver-types@3.17.3: 53 | resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==} 54 | dev: false 55 | 56 | /vscode-languageserver@8.1.0: 57 | resolution: {integrity: sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==} 58 | hasBin: true 59 | dependencies: 60 | vscode-languageserver-protocol: 3.17.3 61 | dev: false 62 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as fs from 'node:fs'; 4 | import * as toml from 'toml'; 5 | import * as os from 'node:os'; 6 | import { TextDocument } from "vscode-languageserver-textdocument"; 7 | import { 8 | CompletionItem, 9 | CompletionItemKind, 10 | createConnection, 11 | DidChangeConfigurationNotification, 12 | InitializeParams, 13 | InitializeResult, 14 | TextDocumentPositionParams, 15 | TextDocuments, 16 | TextDocumentSyncKind 17 | 18 | } from "vscode-languageserver/node"; 19 | 20 | 21 | 22 | const contents = fs.readFileSync(`${os.homedir()}/.config/helix/snippets.toml`,"utf8"); 23 | const snippets = toml.parse(contents); 24 | 25 | const connection = createConnection(process.stdin, process.stdout); 26 | connection.console.log(`Inside lsp: ${new Date().toTimeString()}`); 27 | 28 | //Create a simple text document manager 29 | const documents: TextDocuments = new TextDocuments(TextDocument); 30 | 31 | let hasConfigurationCapability: boolean = false; 32 | let hasWorkspaceFolderCapability: boolean = false; 33 | 34 | connection.onInitialize((params: InitializeParams) => { 35 | const capabilities = params.capabilities; 36 | 37 | connection.console.log("Initializing helix-snippets lsp..."); 38 | // Does the client support the `workspace/configuration` request? 39 | // If not, we fall back using global settings. 40 | hasConfigurationCapability = !!( 41 | capabilities.workspace && !!capabilities.workspace.configuration 42 | ); 43 | hasWorkspaceFolderCapability = !!( 44 | capabilities.workspace && !!capabilities.workspace.workspaceFolders 45 | ); 46 | 47 | 48 | const triggerCharacters = Object.keys(snippets); 49 | 50 | const result: InitializeResult = { 51 | capabilities: { 52 | textDocumentSync: TextDocumentSyncKind.Incremental, 53 | // Tell the client that this server supports code completion. 54 | completionProvider: { 55 | resolveProvider: true, 56 | triggerCharacters: triggerCharacters, 57 | }, 58 | }, 59 | }; 60 | if (hasWorkspaceFolderCapability) { 61 | result.capabilities.workspace = { 62 | workspaceFolders: { 63 | supported: true, 64 | }, 65 | }; 66 | } 67 | console.log(result); 68 | return result; 69 | }); 70 | 71 | connection.onInitialized(() => { 72 | if (hasConfigurationCapability) { 73 | // Register for all configuration changes. 74 | connection.client.register( 75 | DidChangeConfigurationNotification.type, 76 | undefined 77 | ); 78 | } 79 | if (hasWorkspaceFolderCapability) { 80 | connection.workspace.onDidChangeWorkspaceFolders((_event) => { 81 | connection.console.log("Workspace folder change event received."); 82 | }); 83 | } 84 | }); 85 | 86 | // This handler provides the initial list of the completion items. 87 | connection.onCompletion((textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { 88 | try { 89 | const docs = documents.get(textDocumentPosition.textDocument.uri); 90 | if (!docs) throw "failed to find document"; 91 | const languageId = docs.languageId; 92 | const content = docs.getText(); 93 | const linenr = textDocumentPosition.position.line; 94 | const line = String(content.split(/\r?\n/g)[linenr]); 95 | const character = textDocumentPosition.position.character; 96 | 97 | return Object.keys(snippets).map((key,idx) => { 98 | const label = snippets[key]; 99 | return { 100 | label, 101 | kind: CompletionItemKind.Snippet, 102 | data: idx + 1 103 | }; 104 | }); 105 | 106 | } catch (error) { 107 | connection.console.log(`ERR: ${error}`); 108 | } 109 | 110 | return []; 111 | }); 112 | 113 | // This handler resolve additional information for the item selected in 114 | // the completion list. 115 | connection.onCompletionResolve((item: CompletionItem): CompletionItem => { 116 | /* 117 | if (item.data === 1) { 118 | item.detail = 'TypeScript details', 119 | item.documentation = 'TypeScript documentation' 120 | } else if (item.data === 2) { 121 | item.detail = 'JavaScript details', 122 | item.documentation = 'JavaScript documentation' 123 | } 124 | */ 125 | return item; 126 | }); 127 | 128 | 129 | 130 | 131 | 132 | documents.listen(connection); 133 | 134 | connection.listen(); 135 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": [ 5 | "ES2019" 6 | ], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "strict": true, 11 | "outDir": "out", 12 | "rootDir": "src", 13 | "types": [ 14 | "node" 15 | ] 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test" 23 | ] 24 | } --------------------------------------------------------------------------------