├── .github └── workflows │ └── go.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum └── main.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.20' 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v4 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | flags: 18 | - -v 19 | - -trimpath 20 | ldflags: 21 | - -s 22 | - -w 23 | 24 | archives: 25 | - format: tar.gz 26 | # this name template makes the OS and Arch compatible with the results of uname. 27 | name_template: >- 28 | {{ .ProjectName }}_ 29 | {{- title .Os }}_ 30 | {{- if eq .Arch "amd64" }}x86_64 31 | {{- else if eq .Arch "386" }}i386 32 | {{- else }}{{ .Arch }}{{ end }} 33 | {{- if .Arm }}v{{ .Arm }}{{ end }} 34 | # use zip for windows archives 35 | format_overrides: 36 | - goos: windows 37 | format: zip 38 | checksum: 39 | name_template: 'checksums.txt' 40 | snapshot: 41 | name_template: "{{ incpatch .Version }}-next" 42 | changelog: 43 | sort: asc 44 | filters: 45 | exclude: 46 | - '^docs:' 47 | - '^test:' 48 | 49 | # The lines beneath this are called `modelines`. See `:help modeline` 50 | # Feel free to remove those if you don't want/use them. 51 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 52 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 53 | 54 | brews: 55 | - repository: 56 | owner: quantonganh 57 | name: homebrew-tap 58 | folder: Formula 59 | homepage: https://github.com/quantonganh/snippets-ls 60 | description: >- 61 | A simple language server to just insert snippets into Helix. 62 | license: MIT -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 quantonganh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snippets-ls 2 | A simple language server to just insert snippets into Helix (and other text editor, IDE) 3 | 4 | https://quantonganh.com/2023/07/31/create-snippets-in-helix 5 | 6 | ## Installation 7 | 8 | You can download the latest binary from the [release page](https://github.com/quantonganh/snippets-ls/releases). 9 | 10 | ### Install via homebrew 11 | 12 | ``` 13 | brew install quantonganh/tap/snippets-ls 14 | ``` 15 | 16 | ### Install via go 17 | 18 | ```sh 19 | $ go install github.com/quantonganh/snippets-ls@latest 20 | ``` 21 | 22 | Don't forget to append `~/go/bin` to your `$PATH`. 23 | 24 | ### Install via [nix flake](https://nixos.wiki/wiki/Flakes) with [home-manager](https://nix-community.github.io/home-manager/index.xhtml#ch-nix-flakes) 25 | 26 | Include the following in the `inputs` of `flake.nix` for `home-manager`: 27 | ```nix 28 | inputs = { 29 | # load compatible nixpkgs and home-mananger repos; they need not be 23.05 30 | nixpkgs = { url = "github:nixos/nixpkgs/nixos-23.05"; }; 31 | home-manager = { 32 | url = "github:nix-community/home-manager/release-23.05"; 33 | inputs.nixpkgs.follows = "nixpkgs"; 34 | }; 35 | # ... 36 | snippets-ls = { 37 | url = "github:quantonganh/snippets-ls"; 38 | inputs.nixpkgs.follows = "nixpkgs"; 39 | }; 40 | }; 41 | ``` 42 | There are in turn many suitable ways to configure the `outputs` of `flake.nix`. One option is to use an overlay with something like 43 | ```nix 44 | outputs = inputs: 45 | with inputs; 46 | let 47 | system = "x86_64-linux"; # or other system as applicable (such as "x86_64-darwin") 48 | pkgs = import nixpkgs { 49 | inherit system; 50 | overlays = [ pkgs_overlay ]; 51 | }; 52 | # ... 53 | pkgs_overlay = final: prev: { 54 | # ... 55 | external.snippets-ls = snippets-ls.packages.${prev.system}.snippets-ls; 56 | }; 57 | in { 58 | homeConfigurations.YOUR-USER-NAME-HERE = home-manager.lib.homeManagerConfiguration { 59 | inherit pkgs; 60 | modules = [ ./home.nix ]; 61 | }; 62 | }; 63 | }; 64 | ``` 65 | The use of this overlay allows you to call this packages with `pkgs.external.snippets-ls`, such the list of packages in `home.nix` can look something like 66 | ```nix 67 | home.packages = with pkgs; [ 68 | # ... 69 | external.snippets-ls 70 | ]; 71 | ``` 72 | 73 | 74 | ## Usage 75 | 76 | Create your own snippets follow [VSCode syntax](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets). Alternatively, you can make use of [pre-existing](https://github.com/microsoft/vscode-go/blob/master/snippets/go.json) [sample](https://github.com/rust-lang/vscode-rust/blob/master/snippets/rust.json) for various programming languages. 77 | 78 | Update your configuration file located at `~/.config/helix/languages.toml`: 79 | 80 | ```toml 81 | [[language]] 82 | name = "go" 83 | formatter = { command = "goimports"} 84 | language-servers = ["gopls", "snippets-ls"] 85 | 86 | [language-server.snippets-ls] 87 | command = "snippets-ls" 88 | args = ["-config", "/Users/quantong/.config/helix/go-snippets.json"] 89 | ``` 90 | 91 | Subsequently, as you start working on your file, input a snippet prefix to observe the suggestion. 92 | If it does not work, take a look at `~/.cache/helix/helix.log` for additional insights. 93 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1718447546, 6 | "narHash": "sha256-JHuXsrC9pr4kA4n7LuuPfWFJUVlDBVJ1TXDVpHEuUgM=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "842253bf992c3a7157b67600c2857193f126563a", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixos-23.11", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Snippet LSP for helix"; 3 | 4 | # nixpkgs version to use 5 | inputs.nixpkgs.url = "nixpkgs/nixos-23.11"; 6 | 7 | outputs = { self, nixpkgs }: 8 | let 9 | # work with older version of flakes 10 | lastModifiedDate = 11 | self.lastModifiedDate or self.lastModified or "19700101"; 12 | 13 | # user-friendly version number 14 | version = builtins.substring 0 8 lastModifiedDate; 15 | 16 | # system types to support 17 | supportedSystems = 18 | [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 19 | 20 | # helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }' 21 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 22 | 23 | # nixpkgs instantiated for supported system types 24 | nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); 25 | 26 | in { 27 | # provide some binary packages for selected system types 28 | packages = forAllSystems (system: 29 | let pkgs = nixpkgsFor.${system}; 30 | in { 31 | snippets-ls = pkgs.buildGoModule { 32 | pname = "snippets-ls"; 33 | inherit version; 34 | src = ./.; 35 | 36 | # This hash locks the dependencies of this package. It is 37 | # necessary because of how Go requires network access to resolve 38 | # VCS. See https://www.tweag.io/blog/2021-03-04-gomod2nix/ for 39 | # details. Normally one can build with a fake sha256 and rely on native Go 40 | # mechanisms to tell you what the hash should be or determine what 41 | # it should be "out-of-band" with other tooling (eg. gomod2nix). 42 | # To begin with it is recommended to set this, but one must 43 | # remember to bump this hash when your dependencies change. 44 | vendorHash = 45 | "sha256-0FGBtSYKaSjaJlxr8mpXyRKG88ThJCSL3Qutf8gkllw="; 46 | 47 | 48 | meta = with pkgs.lib; { 49 | description = "A simple language server to insert snippets into Helix"; 50 | homepage = "https://github.com/quantonganh/snippets-ls"; 51 | license = licenses.mit; 52 | maintainers = with maintainers; [bddvlpr]; 53 | mainProgram = "snippets-ls"; 54 | }; 55 | }; 56 | }); 57 | 58 | # add dependencies that are only needed for development 59 | devShells = forAllSystems (system: 60 | let pkgs = nixpkgsFor.${system}; 61 | in { 62 | default = pkgs.mkShell { 63 | buildInputs = with pkgs; [ go gopls gotools go-tools ]; 64 | }; 65 | }); 66 | 67 | defaultPackage = 68 | forAllSystems (system: self.packages.${system}.snippets-ls); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quantonganh/snippets-ls 2 | 3 | go 1.20 4 | 5 | require github.com/TobiasYin/go-lsp v0.0.0-20231106040121-c84e66f01aa4 6 | 7 | require ( 8 | github.com/json-iterator/go v1.1.12 // indirect 9 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 10 | github.com/modern-go/reflect2 v1.0.2 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/TobiasYin/go-lsp v0.0.0-20231106040121-c84e66f01aa4 h1:FZOnnAwcy8nr77stDFINHVgTTkpbyZNTw/ZOhJrWA7I= 2 | github.com/TobiasYin/go-lsp v0.0.0-20231106040121-c84e66f01aa4/go.mod h1:wcol2p6rdHzKIPn6+I4LL9jv6BBkBklZGZsrZ15wkgU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 7 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 8 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 9 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 10 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 11 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 12 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/TobiasYin/go-lsp/logs" 14 | "github.com/TobiasYin/go-lsp/lsp" 15 | "github.com/TobiasYin/go-lsp/lsp/defines" 16 | ) 17 | 18 | type Snippets map[string]Snippet 19 | 20 | type Snippet struct { 21 | Prefix Prefix `json:"prefix"` 22 | Body Body `json:"body"` 23 | Descripttion string `json:"description"` 24 | } 25 | 26 | type Prefix struct { 27 | Value interface{} 28 | } 29 | 30 | func (p *Prefix) UnmarshalJSON(data []byte) error { 31 | var singleWord string 32 | if err := json.Unmarshal(data, &singleWord); err == nil { 33 | p.Value = singleWord 34 | return nil 35 | } 36 | 37 | var multipleWords []string 38 | if err := json.Unmarshal(data, &multipleWords); err == nil { 39 | p.Value = multipleWords 40 | return nil 41 | } 42 | 43 | return fmt.Errorf("Cannot unmarshal snippet body: %s", data) 44 | } 45 | 46 | func (p Prefix) ToStringSlice() []string { 47 | switch v := p.Value.(type) { 48 | case string: 49 | return []string{v} 50 | case []string: 51 | return v 52 | default: 53 | return nil 54 | } 55 | } 56 | 57 | type Body struct { 58 | Value interface{} 59 | } 60 | 61 | func (b *Body) UnmarshalJSON(data []byte) error { 62 | var singleLine string 63 | if err := json.Unmarshal(data, &singleLine); err == nil { 64 | b.Value = singleLine 65 | return nil 66 | } 67 | 68 | var multipleLines []string 69 | if err := json.Unmarshal(data, &multipleLines); err == nil { 70 | b.Value = multipleLines 71 | return nil 72 | } 73 | 74 | return fmt.Errorf("Cannot unmarshal snippet body: %s", data) 75 | } 76 | 77 | func (b Body) String() string { 78 | switch v := b.Value.(type) { 79 | case string: 80 | return v 81 | case []string: 82 | return strings.Join(v, "\n") 83 | default: 84 | return "" 85 | } 86 | } 87 | 88 | func main() { 89 | configPath := flag.String("config", "snippets.json", "Path to the json snippets file") 90 | flag.Parse() 91 | 92 | data, err := ioutil.ReadFile(*configPath) 93 | if err != nil { 94 | log.Fatalf("Error reading config file: %v", err) 95 | } 96 | 97 | var snippets Snippets 98 | if err := json.Unmarshal(data, &snippets); err != nil { 99 | log.Fatalf("Error unmarshalling config data: %v", err) 100 | } 101 | 102 | logger := log.New(os.Stdout, "snippets-ls: ", log.LstdFlags) 103 | logs.Init(logger) 104 | 105 | server := lsp.NewServer(&lsp.Options{CompletionProvider: &defines.CompletionOptions{ 106 | TriggerCharacters: &[]string{"."}, 107 | }}) 108 | 109 | items := make([]defines.CompletionItem, 0) 110 | k := defines.CompletionItemKindSnippet 111 | for _, snippet := range snippets { 112 | for _, prefix := range snippet.Prefix.ToStringSlice() { 113 | item := defines.CompletionItem{ 114 | Kind: &k, 115 | Label: prefix, 116 | InsertText: strPtr(fmt.Sprintf("%s", snippet.Body)), 117 | } 118 | items = append(items, item) 119 | } 120 | } 121 | 122 | server.OnCompletion(func(ctx context.Context, req *defines.CompletionParams) (result *[]defines.CompletionItem, err error) { 123 | logs.Println(req) 124 | return &items, nil 125 | }) 126 | 127 | server.Run() 128 | } 129 | 130 | func strPtr(s string) *string { 131 | return &s 132 | } 133 | --------------------------------------------------------------------------------