├── .python-version ├── LSP-terraform.sublime-commands ├── LSP-terraform.sublime-settings ├── LICENSE ├── README.md ├── plugin.py └── sublime-package.json /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /LSP-terraform.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences: LSP-terraform Settings", 4 | "command": "edit_settings", 5 | "args": { 6 | "base_file": "${packages}/LSP-terraform/LSP-terraform.sublime-settings", 7 | "default": "// Settings in here override those in \"LSP-terraform/LSP-terraform.sublime-settings\"\n\n{\n\t$0\n}\n", 8 | }, 9 | }, 10 | ] 11 | -------------------------------------------------------------------------------- /LSP-terraform.sublime-settings: -------------------------------------------------------------------------------- 1 | // Packages/User/LSP-terraform.sublime-settings 2 | { 3 | "initializationOptions": { 4 | "experimentalFeatures.validateOnSave": false, 5 | "experimentalFeatures.prefillRequiredFields": false, 6 | "ignoreSingleFileWarning": false, 7 | "indexing.ignoreDirectoryNames": [], 8 | "indexing.ignorePaths": [], 9 | "terraform.logFilePath": "", 10 | "validation.enableEnhancedValidation": true, 11 | }, 12 | "command": [ 13 | "${storage_path}/LSP-terraform/terraform-ls", 14 | "serve" 15 | ], 16 | "selector": "source.terraform | source.terraform-vars", 17 | // File watching functionality only works with "LSP-file-watcher-chokidar" package installed. 18 | "file_watcher": { 19 | "patterns": [ 20 | "**/*.tf", 21 | "**/*.tfvars" 22 | ], 23 | "events": [ 24 | "create", 25 | "change", 26 | "delete" 27 | ] 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SublimeLSP 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 | # LSP-terraform 2 | 3 | A convenience package to take advantage of the [Terraform Language Server](https://github.com/hashicorp/terraform-ls). 4 | 5 | ## Installation 6 | 7 | 1. Install [LSP](https://packagecontrol.io/packages/LSP), [LSP-terraform](https://packagecontrol.io/packages/LSP-terraform) and [Terraform Syntax](https://packagecontrol.io/packages/Terraform) from Package Control. 8 | 2. Restart Sublime Text. 9 | 3. (Optional but recommended) Install the [LSP-file-watcher-chokidar](https://github.com/sublimelsp/LSP-file-watcher-chokidar) via Package Control to enable functionality to notify the server about new files. 10 | 11 | Optionally install the [Terraform CLI](https://learn.hashicorp.com/tutorials/terraform/install-cli) for the `validateOnSave` and formatting functionality. 12 | 13 | ## Configuration 14 | 15 | You may edit the default settings by running `Preferences: LSP-terraform Settings` from the _Command Palette_. 16 | 17 | Optionally you can view `terraform-ls` settings at Hashicorps [repo](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md) 18 | 19 | ## Formatting 20 | 21 | The server supports formatting for the terraform files (equivalent to running `terraform fmt`). You can either trigger it manually from the _Command Palette_ using the `LSP: Format File` command or automatically run it on saving the file. To enable formatting on save, open `Preferences: LSP Settings` from the _Command Palette_ and add or modify the following setting: 22 | 23 | ```js 24 | { 25 | "lsp_code_actions_on_save": { 26 | "source.formatAll.terraform": true, 27 | }, 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | # Packages/LSP-terraform/plugin.py 2 | 3 | import sublime 4 | 5 | from LSP.plugin import AbstractPlugin, register_plugin, unregister_plugin 6 | from LSP.plugin.core.typing import cast, Any, List, Optional 7 | 8 | import os 9 | import sys 10 | import shutil 11 | import zipfile 12 | import urllib.request 13 | import platform 14 | import hashlib 15 | 16 | USER_AGENT = 'Sublime Text LSP' 17 | 18 | TAG = '0.38.3' 19 | 20 | # GitHub releases page: https://github.com/hashicorp/terraform-ls/releases 21 | HASHICORP_RELEASES_BASE = 'https://releases.hashicorp.com/terraform-ls/{tag}/terraform-ls_{tag}_{platform}_{arch}.zip' 22 | HASHICORP_SHA256_BASE = 'https://releases.hashicorp.com/terraform-ls/{tag}/terraform-ls_{tag}_SHA256SUMS' 23 | HASHICORP_FILENAME_BASE = 'terraform-ls_{tag}_{platform}_{arch}.zip' 24 | 25 | 26 | def plat() -> Optional[str]: 27 | """Return the user friendly platform version that sublime is running on.""" 28 | if sublime.platform() == 'osx': 29 | return 'darwin' 30 | if sublime.platform() == 'windows': 31 | return 'windows' 32 | if sublime.platform() == 'linux': 33 | if platform.system() == 'Linux': 34 | return 'linux' 35 | if sys.platform.startswith('freebsd'): 36 | return 'freebsd' 37 | if sys.platform.startswith('openbsd'): 38 | return 'openbsd' 39 | return None 40 | 41 | 42 | def arch() -> Optional[str]: 43 | """Return the user friendly architecture version that sublime is running on.""" 44 | if sublime.arch() == "x32": 45 | return "386" 46 | elif sublime.arch() == "x64": 47 | return "amd64" 48 | elif sublime.arch() == "arm64": 49 | return "arm64" 50 | else: 51 | return None 52 | 53 | 54 | class Terraform(AbstractPlugin): 55 | """AbstractPlugin implementation acts as a helper package for the Terraform Language Server (terraform-ls).""" 56 | 57 | @classmethod 58 | def name(cls) -> str: 59 | return "terraform" 60 | 61 | @classmethod 62 | def basedir(cls) -> str: 63 | return os.path.join(cls.storage_path(), __package__) 64 | 65 | @classmethod 66 | def server_version(cls) -> str: 67 | return TAG 68 | 69 | @classmethod 70 | def current_server_version(cls) -> Optional[str]: 71 | try: 72 | with open(os.path.join(cls.basedir(), "VERSION")) as fp: 73 | return fp.read() 74 | except: 75 | return None 76 | 77 | @classmethod 78 | def _is_terraform_ls_installed(cls) -> bool: 79 | return bool(cls._get_terraform_ls_path()) 80 | 81 | @classmethod 82 | def _get_terraform_ls_path(cls) -> Optional[str]: 83 | terraform_ls_binary = cast(List[str], get_setting('command', [os.path.join(cls.basedir(), 'terraform-ls')])) 84 | return shutil.which(terraform_ls_binary[0]) if len(terraform_ls_binary) else None 85 | 86 | @classmethod 87 | def needs_update_or_installation(cls) -> bool: 88 | return not cls._is_terraform_ls_installed() or (cls.current_server_version() != cls.server_version()) 89 | 90 | @classmethod 91 | def install_or_update(cls) -> None: 92 | if plat() is None: 93 | raise ValueError('System platform not detected or supported') 94 | 95 | if arch() is None: 96 | raise ValueError('System architecture not detected or supported') 97 | 98 | terraform_ls_path = cls._get_terraform_ls_path() 99 | if terraform_ls_path: 100 | os.remove(terraform_ls_path) 101 | 102 | os.makedirs(cls.basedir(), exist_ok=True) 103 | 104 | zip_url = HASHICORP_RELEASES_BASE.format( 105 | tag=cls.server_version(), arch=arch(), platform=plat()) 106 | zip_file = os.path.join(cls.basedir(), HASHICORP_FILENAME_BASE.format( 107 | tag=cls.server_version(), platform=plat(), arch=arch())) 108 | sha_url = HASHICORP_SHA256_BASE.format(tag=cls.server_version()) 109 | 110 | sha_file = os.path.join(cls.basedir(), 'terraform-ls.sha') 111 | 112 | req = urllib.request.Request( 113 | zip_url, 114 | data=None, 115 | headers={ 116 | 'User-Agent': USER_AGENT 117 | } 118 | ) 119 | with urllib.request.urlopen(req) as fp: 120 | with open(zip_file, "wb") as f: 121 | f.write(fp.read()) 122 | 123 | req = urllib.request.Request( 124 | sha_url, 125 | data=None, 126 | headers={ 127 | 'User-Agent': USER_AGENT 128 | } 129 | ) 130 | with urllib.request.urlopen(req) as fp: 131 | with open(sha_file, "wb") as f: 132 | f.write(fp.read()) 133 | 134 | sha256_hash_computed = None 135 | with open(zip_file, "rb") as f: 136 | file_bytes = f.read() 137 | sha256_hash_computed = hashlib.sha256(file_bytes).hexdigest() 138 | 139 | with open(sha_file) as fp: 140 | for line in fp: 141 | sha256sum, filename = line.split(' ') 142 | if filename.strip() != HASHICORP_FILENAME_BASE.format(tag=TAG, platform=plat(), arch=arch()): 143 | continue 144 | 145 | if sha256sum.strip() != sha256_hash_computed: 146 | raise ValueError( 147 | 'sha256 mismatch', 'original hash:', sha256sum, 'computed hash:', sha256_hash_computed) 148 | break 149 | 150 | with zipfile.ZipFile(zip_file, 'r') as zip_ref: 151 | zip_ref.extractall(cls.basedir()) 152 | 153 | os.remove(zip_file) 154 | os.remove(sha_file) 155 | 156 | terraform_ls = 'terraform-ls' if plat() != 'windows' else 'terraform-ls.exe' 157 | os.chmod(os.path.join(cls.basedir(), terraform_ls), 0o700) 158 | 159 | with open(os.path.join(cls.basedir(), 'VERSION'), 'w') as fp: 160 | fp.write(cls.server_version()) 161 | 162 | 163 | def get_setting(key: str, default=None) -> Any: 164 | settings = sublime.load_settings( 165 | 'LSP-terraform.sublime-settings').get("settings", {}) 166 | return settings.get(key, default) 167 | 168 | 169 | def plugin_loaded(): 170 | register_plugin(Terraform) 171 | 172 | 173 | def plugin_unloaded(): 174 | unregister_plugin(Terraform) 175 | -------------------------------------------------------------------------------- /sublime-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributions": { 3 | "settings": [ 4 | { 5 | "file_patterns": [ 6 | "/LSP-terraform.sublime-settings" 7 | ], 8 | "schema": { 9 | "$id": "sublime://settings/LSP-terraform", 10 | "definitions": { 11 | "PluginConfig": { 12 | "properties": { 13 | "initializationOptions": { 14 | "additionalProperties": false, 15 | "type": "object", 16 | "properties": { 17 | "terraformLogFilePath": { 18 | "deprecationMessage": "Deprecated in favour of `terraform.logFilePath` and is now ignored.", 19 | "default": "", 20 | "type": "string" 21 | }, 22 | "terraform.logFilePath": { 23 | "markdownDescription": "Path to a file for Terraform executions to be logged into (`TF_LOG_PATH`) with support for variables (e.g. Timestamp, Pid, Ppid) via Go template syntax `{{.VarName}}`.", 24 | "default": "", 25 | "type": "string" 26 | }, 27 | "terraformExecTimeout": { 28 | "deprecationMessage": "Deprecated in favour of `terraform.timeout` and is now ignored.", 29 | "default": "", 30 | "type": "string" 31 | }, 32 | "terraform.timeout": { 33 | "markdownDescription": "Overrides Terraform execution timeout in [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration) compatible format (e.g. `30s`)", 34 | "default": "", 35 | "type": "string" 36 | }, 37 | "terraformExecPath": { 38 | "deprecationMessage": "Deprecated in favour of `terraform.path` and is now ignored.", 39 | "default": "", 40 | "type": "string" 41 | }, 42 | "terraform.path": { 43 | "markdownDescription": "Path to the Terraform binary.\n\nThis is usually looked up automatically from `$PATH` and should not need to be specified in majority of cases. Use this to override the automatic lookup.", 44 | "default": "", 45 | "type": "string" 46 | }, 47 | "rootModulePaths": { 48 | "deprecationMessage": "Deprecated and is ignored. Users should instead leverage the workspace LSP API and add the folder to a workspace, if they wish it to be indexed.", 49 | "default": [], 50 | "items": { 51 | "type": "string" 52 | }, 53 | "type": "array" 54 | }, 55 | "ignoreDirectoryNames": { 56 | "deprecationMessage": "Deprecated in favour of `indexing.ignoreDirectoryNames` - https://github.com/hashicorp/terraform-ls/blob/v0.29.0/docs/SETTINGS.md#ignoredirectorynames-string.", 57 | "default": [], 58 | "items": { 59 | "type": "string" 60 | }, 61 | "type": "array" 62 | }, 63 | "excludeModulePaths": { 64 | "deprecationMessage": "Deprecated in favour of `indexing.ignorePaths` and is now ignored.", 65 | "default": [], 66 | "items": { 67 | "type": "string" 68 | }, 69 | "type": "array" 70 | }, 71 | "ignoreSingleFileWarning": { 72 | "default": false, 73 | "markdownDescription": "Controls whether a warning is raised about opening a single Terraform file instead of a Terraform folder.", 74 | "type": "boolean" 75 | }, 76 | "indexing.ignoreDirectoryNames": { 77 | "markdownDescription": "Allows excluding directories from being indexed upon initialization by passing a list of directory names. [documentation](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md#ignoredirectorynames-string)", 78 | "default": [], 79 | "items": { 80 | "type": "string" 81 | }, 82 | "type": "array" 83 | }, 84 | "indexing.ignorePaths": { 85 | "markdownDescription": "Paths to ignore when indexing the workspace on initialization. [documentation](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md#ignorepaths-string)", 86 | "default": [], 87 | "items": { 88 | "type": "string" 89 | }, 90 | "type": "array" 91 | }, 92 | "experimentalFeatures.validateOnSave": { 93 | "markdownDescription": "Enabling this feature will run terraform validate within the folder of the file saved. This comes with some user experience caveats.\n\n- Validation is not run on file open, only once it's saved.\n\n- When editing a module file, validation is not run due to not knowing which \"rootmodule\" to run validation from (there could be multiple). This creates an awkward workflow where when saving a file in a rootmodule, a diagnostic is raised in a module file. Editing the module file will not clear the diagnostic for the reason mentioned above, it will only clear once a file is saved back in the original \"rootmodule\". We will continue to attempt improve this user experience.", 94 | "default": false, 95 | "type": "boolean" 96 | }, 97 | "experimentalFeatures.prefillRequiredFields": { 98 | "markdownDescription": "Enables advanced completion for `provider`, `resource`, and `data` blocks where any required fields for that block are pre-filled. All such attributes and blocks are sorted alphabetically to ensure consistent ordering.\n\nWhen disabled (unset or set to `false`), completion only provides the label name.\n\nFor example, when completing the `aws_appmesh_route` resource the `mesh_name`, `name`, `virtual_router_name` attributes and the `spec` block will fill and prompt you for appropriate values.", 99 | "default": false, 100 | "type": "boolean" 101 | }, 102 | "validation.enableEnhancedValidation": { 103 | "markdownDescription": "Controls whether enhanced validation of Terraform files is enabled", 104 | "default": true, 105 | "type": "boolean" 106 | }, 107 | } 108 | }, 109 | "additionalProperties": false 110 | } 111 | } 112 | }, 113 | "allOf": [ 114 | { 115 | "$ref": "sublime://settings/LSP-plugin-base" 116 | }, 117 | { 118 | "$ref": "sublime://settings/LSP-terraform#/definitions/PluginConfig" 119 | } 120 | ] 121 | } 122 | }, 123 | { 124 | "file_patterns": [ 125 | "/*.sublime-project" 126 | ], 127 | "schema": { 128 | "properties": { 129 | "settings": { 130 | "properties": { 131 | "LSP": { 132 | "properties": { 133 | "terraform": { 134 | "$ref": "sublime://settings/LSP-terraform#/definitions/PluginConfig" 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | { 144 | "file_patterns": [ 145 | "LSP.sublime-settings" 146 | ], 147 | "schema": { 148 | "properties": { 149 | "lsp_code_actions_on_save": { 150 | "properties": { 151 | "source.formatAll.terraform": { 152 | "type": "boolean" 153 | }, 154 | } 155 | } 156 | } 157 | } 158 | } 159 | ] 160 | } 161 | } 162 | --------------------------------------------------------------------------------