├── .python-version ├── dependencies.json ├── LSP-elixir.sublime-commands ├── Main.sublime-menu ├── CHANGELOG.md ├── LICENSE ├── .github └── workflows │ └── auto-update.yml ├── plugin.py ├── README.md ├── LSP-elixir.sublime-settings └── server_zip_resource.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | "*": [ 4 | "lsp_utils", 5 | "sublime_lib" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LSP-elixir.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences: LSP-elixir Settings", 4 | "command": "edit_settings", 5 | "args": { 6 | "base_file": "${packages}/LSP-elixir/LSP-elixir.sublime-settings", 7 | "default": "// Settings in here override those in \"LSP-elixir/LSP-elixir.sublime-settings\"\n\n{\n\t$0\n}\n" 8 | } 9 | }, 10 | ] 11 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": [ 5 | { 6 | "caption": "Package Settings", 7 | "mnemonic": "P", 8 | "id": "package-settings", 9 | "children": [ 10 | { 11 | "caption": "LSP", 12 | "id": "lsp-settings", 13 | "children": [ 14 | { 15 | "caption": "Servers", 16 | "id": "lsp-servers", 17 | "children": [ 18 | { 19 | "caption": "LSP-elixir", 20 | "command": "edit_settings", 21 | "args": { 22 | "base_file": "${packages}/LSP-elixir/LSP-elixir.sublime-settings", 23 | "default": "// Settings in here override those in \"LSP-elixir/LSP-elixir.sublime-settings\",\n\n{\n\t$0\n}\n", 24 | } 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | LSP-Elixir Changelog 2 | ==================== 3 | 4 | 0.4.1 (2021-04-08) 5 | ------------------ 6 | 7 | * Upgrade elixir-lsp to 0.7.0 (thanks @optikfluffel) 8 | 9 | 0.4.0 (2021-03-18) 10 | ------------------ 11 | 12 | * Use lsp_utils for server handling (thanks @princemaple) 13 | * Upgrade elixir-lsp to 0.6.5 (thanks @optikfluffel) 14 | 15 | 0.3.2 (2021-02-02) 16 | ------------------ 17 | 18 | * Upgrade elixir-lsp to 0.6.4 (thanks @optikfluffel) 19 | 20 | 0.3.1 (2021-02-01) 21 | ------------------ 22 | 23 | * Upgrade elixir-lsp to 0.6.3 (thanks @princemaple) 24 | 25 | 0.3.0 (2020-10-04) 26 | ------------------ 27 | 28 | * Upgrade elixir-lsp to 0.6.0 (thanks @princemaple) 29 | 30 | 0.2.0 (2020-06-29) 31 | ------------------ 32 | 33 | * Upgrade elixir-lsp to 0.5.0 (thanks @princemaple) 34 | * Add available settings to `LSP-elixir.sublime-settings` 35 | 36 | 0.1.0 (2020-05-23) 37 | ------------------ 38 | 39 | * Initial release 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ivan Habunek 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 | -------------------------------------------------------------------------------- /.github/workflows/auto-update.yml: -------------------------------------------------------------------------------- 1 | name: Auto Update 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0 * * * 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v6 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: '3.14' 19 | 20 | - name: Run script to check for changes 21 | id: check 22 | run: yes | python scripts/update.py 23 | 24 | - name: Check for code diffs 25 | id: diff 26 | run: echo "diff=$(git diff --name-only)" >> "$GITHUB_OUTPUT" 27 | 28 | - name: Create pull request 29 | if: steps.diff.outputs.diff != '' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | VERSION: ${{ steps.check.outputs.version }} 33 | run: | 34 | git add -A 35 | git checkout -b "elixir-ls-$VERSION" 36 | 37 | git config user.name 'Po Chen' 38 | git config user.email 'princemaple@users.noreply.github.com' 39 | 40 | git commit -m "Bump elixir-ls to $VERSION" 41 | git push origin "elixir-ls-$VERSION" 42 | 43 | gh pr create --fill 44 | -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from lsp_utils import GenericClientHandler, ServerResourceInterface 4 | from LSP.plugin.core.typing import Optional 5 | 6 | from .server_zip_resource import ServerZipResource 7 | 8 | SERVER_VERSION = "0.30.0" 9 | SERVER_URL = "https://github.com/elixir-lsp/elixir-ls/releases/download/v0.30.0/elixir-ls-v0.30.0.zip" 10 | SERVER_SHA256 = "924c3d44c9d04ba332e4613697ef0f91ff7a57c1205d4e9fcc1395bd0e69b042" 11 | 12 | SERVER_EXECUTABLES = ["language_server.sh", "launch.sh"] 13 | BINARY_PATH = ( 14 | "language_server.bat" if sublime.platform() == "windows" else "language_server.sh" 15 | ) 16 | 17 | 18 | def plugin_loaded() -> None: 19 | LspElixirPlugin.setup() 20 | 21 | 22 | def plugin_unloaded() -> None: 23 | LspElixirPlugin.cleanup() 24 | 25 | 26 | class LspElixirPlugin(GenericClientHandler): 27 | package_name = __package__ 28 | __server = None 29 | 30 | @classmethod 31 | def get_displayed_name(cls) -> str: 32 | return "lsp-elixir" 33 | 34 | @classmethod 35 | def manages_server(cls) -> bool: 36 | return True 37 | 38 | @classmethod 39 | def get_server(cls) -> Optional[ServerResourceInterface]: 40 | if not cls.__server: 41 | cls.__server = ServerZipResource( 42 | cls.storage_path(), 43 | cls.package_name, 44 | BINARY_PATH, 45 | SERVER_URL, 46 | SERVER_VERSION, 47 | asset_hash=SERVER_SHA256, 48 | executables=SERVER_EXECUTABLES, 49 | ) 50 | return cls.__server 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LSP-elixir 2 | 3 | Elixir support for Sublime Text's LSP plugin provided through 4 | [ElixirLS](https://github.com/elixir-lsp/elixir-ls/). 5 | 6 | Requires **Elixir 1.14.0+** and **OTP 25+**. A good way of installing these is 7 | using the [ASDF package manager](https://github.com/asdf-vm/asdf) with the 8 | [asdf-elixir plugin](https://github.com/asdf-vm/asdf-elixir). On Windows, 9 | [Scoop](https://scoop.sh/) is a solid option. `scoop install elixir` or 10 | `scoop install elixir@1.18.2-otp-27`. 11 | 12 | ## Installation 13 | 14 | Install the following packages in Sublime Text: 15 | 16 | * [Elixir](https://packagecontrol.io/packages/Elixir) or [Elixir Sublime Syntax](https://packagecontrol.io/packages/ElixirSyntax) - Elixir syntax support 17 | * [LSP](https://packagecontrol.io/packages/LSP) - Base LSP package 18 | * [LSP-elixir](https://packagecontrol.io/packages/LSP-elixir) - This plugin 19 | 20 | ## Configuration 21 | 22 | Defaults can be edited by selecting `Preferences: LSP-elixir Settings` from the 23 | command palette. 24 | 25 | ### Format on save 26 | 27 | To format your code on save add the following setting to your syntax-specific settings (Elixir in this case) and/or project files: 28 | 29 | ```json 30 | { 31 | "lsp_format_on_save": true 32 | } 33 | ``` 34 | 35 | ## Upgrade to latest version 36 | 37 | For developers! To upgrade this library to use the latest version of elixir-ls, 38 | run: 39 | 40 | ``` 41 | ./scripts/update.py 42 | ``` 43 | 44 | This will update the server info in `plugin.py` to match the latest [release of 45 | elixir-ls](https://github.com/elixir-lsp/elixir-ls/releases). 46 | -------------------------------------------------------------------------------- /LSP-elixir.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "initializationOptions": {}, 3 | "settings": { 4 | "elixirLS": { 5 | // Run ElixirLS's rapid Dialyzer when code is saved 6 | "dialyzerEnabled": true, 7 | 8 | // Dialyzer options to enable or disable warnings. 9 | // 10 | // See Dialyzer's documentation for explanation of each option: 11 | // http://erlang.org/doc/man/dialyzer.html 12 | // 13 | // Note that the `race_conditions` option is unsupported. 14 | "dialyzerWarnOpts": [ 15 | // "error_handling", 16 | // "no_behaviours", 17 | // "no_contracts", 18 | // "no_fail_call", 19 | // "no_fun_app", 20 | // "no_improper_lists", 21 | // "no_match", 22 | // "no_missing_calls", 23 | // "no_opaque", 24 | // "no_return", 25 | // "no_undefined_callbacks", 26 | // "no_unused", 27 | // "overspecs", 28 | // "specdiffs", 29 | // "underspecs", 30 | // "unknown", 31 | // "unmatched_returns", 32 | ], 33 | 34 | // Formatter to use for Dialyzer warnings 35 | // Options are: 36 | // * dialyzer - Original Dialyzer format 37 | // * dialyxir_short - Same as `mix dialyzer --format short` 38 | // * dialyxir_long - Same as `mix dialyzer --format long` 39 | "dialyzerFormat": "dialyxir_long", 40 | 41 | // Mix environment to use for compilation 42 | "mixEnv": "test", 43 | 44 | // Mix target to use for compilation (requires Elixir >= 1.8) 45 | "mixTarget": null, 46 | 47 | // Subdirectory containing Mix project if not in the project root 48 | "projectDir": null, 49 | 50 | // Automatically fetch project dependencies when compiling 51 | "fetchDeps": true, 52 | } 53 | }, 54 | "command": ["$server_path"], 55 | "selector": "source.elixir" 56 | } 57 | -------------------------------------------------------------------------------- /server_zip_resource.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import sublime 4 | 5 | from zipfile import ZipFile 6 | from urllib.request import urlretrieve 7 | 8 | from lsp_utils import ServerResourceInterface, ServerStatus 9 | from sublime_lib import ActivityIndicator 10 | from LSP.plugin.core.typing import List 11 | 12 | __all__ = ['ServerZipResource'] 13 | 14 | 15 | class ServerZipResource(ServerResourceInterface): 16 | def __init__(self, 17 | storage_path: str, 18 | package_name: str, 19 | binary_path: str, 20 | asset_url: str, 21 | asset_version: str, 22 | *, 23 | asset_hash: str = None, 24 | executables: List[str] = []) -> None: 25 | self._storage_path = storage_path 26 | self._package_name = package_name 27 | self._binary_path = binary_path 28 | self._url = asset_url 29 | self._version = asset_version 30 | self._status = ServerStatus.UNINITIALIZED 31 | 32 | self._hash = asset_hash 33 | self._executables = executables 34 | 35 | def get_server_dir(self) -> str: 36 | return os.path.join(self._storage_path, self._package_name, "server", self._version) 37 | 38 | def get_server_exec(self) -> str: 39 | return os.path.join(self.get_server_dir(), self._binary_path) 40 | 41 | def is_server_downloaded(self): 42 | return os.path.exists(self.get_server_exec()) 43 | 44 | def is_valid_hash(self, path, expected) -> bool: 45 | if not self._hash: 46 | return True 47 | 48 | with open(path, "rb") as f: 49 | calculated = hashlib.sha256(f.read()).hexdigest() 50 | return calculated == expected 51 | 52 | def unpack_server(self, zip_file) -> None: 53 | if not self.is_valid_hash(zip_file, self._hash): 54 | return 55 | 56 | target_dir = self.get_server_dir() 57 | os.makedirs(target_dir, exist_ok=True) 58 | 59 | with ZipFile(zip_file, "r") as f: 60 | f.extractall(target_dir) 61 | 62 | # ZipFile removes permissions, make server executable 63 | if self._executables and not sublime.platform() == 'windows': 64 | for executable in self._executables: 65 | os.chmod(os.path.join(target_dir, executable), 0o755) 66 | 67 | def download_server(self) -> None: 68 | target = sublime.active_window() 69 | label = "Downloading zip file..." 70 | 71 | with ActivityIndicator(target, label): 72 | tmp_file, _ = urlretrieve(self._url) 73 | self.unpack_server(tmp_file) 74 | os.unlink(tmp_file) 75 | 76 | # ServerResourceInterface 77 | 78 | def get_status(self) -> int: 79 | return self._status 80 | 81 | def needs_installation(self) -> bool: 82 | if self.is_server_downloaded(): 83 | self._status = ServerStatus.READY 84 | return False 85 | return True 86 | 87 | def install_or_update(self) -> None: 88 | self.download_server() 89 | self._status = ServerStatus.READY 90 | 91 | @property 92 | def binary_path(self) -> str: 93 | return self.get_server_exec() 94 | --------------------------------------------------------------------------------