├── .changes ├── 0.1.0.md ├── header.tpl.md ├── unreleased │ └── ENHANCEMENTS-20240911-122605.yaml ├── v0.1.1.md ├── v0.2.0-beta1.md ├── v0.2.0.md └── v0.2.1.md ├── .changie.yaml ├── .copywrite.hcl ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── documentation.yml │ ├── feature_request.yml │ └── performance.yml ├── SUPPORT.md ├── dependabot.yml ├── release.yml ├── renovate.json └── workflows │ ├── check-changelog.yml │ ├── deploy.yml │ ├── issue-comment-created.yml │ ├── lock.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── assets └── icons │ ├── opentofu.svg │ ├── opentofu_logo_universal.png │ ├── running.woff │ ├── terraform_stacks.svg │ └── vs_code_opentofu.svg ├── build ├── downloader.ts └── preview.sh ├── docs ├── code_lens.png ├── intellisense1.png ├── intellisense2.png ├── intellisense3.png ├── module_calls.png ├── module_calls_doc_link.png ├── module_providers.png ├── pin_version.png ├── pre-fill.png ├── syntax.png ├── validation-cli-command.png ├── validation-cli-diagnostic.png ├── validation-rule-hcl.png ├── validation-rule-invalid-ref.png └── validation-rule-missing-attribute.png ├── esbuild.js ├── language-configuration.json ├── package-lock.json ├── package.json ├── snippets └── opentofu.json ├── src ├── api │ └── terraform │ │ └── terraform.ts ├── commands │ ├── generateBugReport.ts │ ├── terraform.ts │ └── terraformls.ts ├── extension.ts ├── features │ ├── languageStatus.ts │ ├── moduleCalls.ts │ ├── moduleProviders.ts │ ├── semanticTokens.ts │ ├── showReferences.ts │ ├── terraformVersion.ts │ └── types.ts ├── providers │ └── terraform │ │ ├── moduleCalls.ts │ │ └── moduleProviders.ts ├── settings.ts ├── status │ ├── installedVersion.ts │ ├── language.ts │ └── requiredVersion.ts ├── test │ ├── e2e │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── specs │ │ │ ├── extension.e2e.ts │ │ │ ├── language │ │ │ │ └── terraform.e2e..ts │ │ │ └── views │ │ │ │ ├── terraform.e2e.ts │ │ │ │ └── tfc.e2e.ts │ │ ├── tsconfig.json │ │ └── wdio.conf.ts │ ├── fixtures │ │ ├── actions.tf │ │ ├── ai │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ ├── compute │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── empty.tf │ │ ├── main.tf │ │ ├── sample.tf │ │ ├── terraform.tfvars │ │ └── variables.tf │ ├── helper.ts │ └── integration │ │ ├── README.md │ │ ├── basics │ │ ├── codeAction.test.ts │ │ ├── completion.test.ts │ │ ├── definition.test.ts │ │ ├── hover.test.ts │ │ ├── references.test.ts │ │ ├── symbols.test.ts │ │ └── workspace │ │ │ ├── actions.tf │ │ │ ├── ai │ │ │ ├── main.tf │ │ │ └── variables.tf │ │ │ ├── compute │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ │ ├── empty.tf │ │ │ ├── main.tf │ │ │ ├── registry_module.tf │ │ │ ├── sample.tf │ │ │ ├── terraform.tfvars │ │ │ └── variables.tf │ │ ├── init │ │ ├── init.test.ts │ │ └── workspace │ │ │ ├── .vscode │ │ │ └── settings.json │ │ │ ├── git_module.tf │ │ │ └── main.tf │ │ └── stacks │ │ └── workspace │ │ ├── .terraform-version │ │ ├── .terraform.lock.hcl │ │ ├── .vscode │ │ └── settings.json │ │ ├── README.md │ │ ├── api-gateway │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ │ ├── components.tfstack.hcl │ │ ├── deployments.tfdeploy.hcl │ │ ├── lambda │ │ ├── hello-world │ │ │ └── hello.rb │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ │ ├── providers.tfstack.hcl │ │ ├── s3 │ │ ├── main.tf │ │ └── outputs.tf │ │ └── variables.tfstack.hcl ├── utils │ ├── clientHelpers.ts │ ├── serverPath.ts │ └── vscode.ts └── web │ └── extension.ts ├── test ├── fixtures │ ├── .vscode │ │ └── settings.json │ ├── actions.tf │ ├── ai │ │ ├── main.tf │ │ └── variables.tf │ ├── compute │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── empty.tf │ ├── main.tf │ ├── sample.tf │ ├── terraform.tfvars │ └── variables.tf ├── package-lock.json ├── package.json ├── specs │ ├── extension.ts │ ├── language │ │ └── terraform.ts │ └── views │ │ ├── terraform.ts │ │ └── tfc.ts ├── tsconfig.json └── wdio.conf.ts ├── tsconfig.json └── webpack.config.js /.changes/0.1.0.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2024-09-11) 2 | 3 | -------------------------------------------------------------------------------- /.changes/header.tpl.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | -------------------------------------------------------------------------------- /.changes/unreleased/ENHANCEMENTS-20240911-122605.yaml: -------------------------------------------------------------------------------- 1 | kind: ENHANCEMENTS 2 | body: update opentofu ls 0.2.0 3 | time: 2024-09-11T12:26:05.5663+05:30 4 | custom: 5 | Issue: "97" 6 | Repository: opentofu-ls 7 | -------------------------------------------------------------------------------- /.changes/v0.1.1.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 (2024-09-11) 2 | 3 | -------------------------------------------------------------------------------- /.changes/v0.2.0-beta1.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0-beta1 (2024-09-11) 2 | 3 | -------------------------------------------------------------------------------- /.changes/v0.2.0.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (2024-09-11) 2 | 3 | -------------------------------------------------------------------------------- /.changes/v0.2.1.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 (2024-09-11) 2 | 3 | -------------------------------------------------------------------------------- /.changie.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | changesDir: .changes 5 | unreleasedDir: unreleased 6 | headerPath: header.tpl.md 7 | changelogPath: CHANGELOG.md 8 | versionExt: md 9 | versionFormat: '## {{.VersionNoPrefix}} ({{.Time.Format "2006-01-02"}})' 10 | kindFormat: '{{.Kind}}:' 11 | changeFormat: |- 12 | * {{.Body}} ([{{- if not (eq .Custom.Repository "vscode-opentofu")}}{{.Custom.Repository}}{{- end}}#{{.Custom.Issue}}](https://github.com/hashicorp/{{.Custom.Repository}}/issues/{{.Custom.Issue}})) 13 | custom: 14 | - key: Repository 15 | label: Repository 16 | type: enum 17 | enumOptions: 18 | - vscode-opentofu 19 | - opentofu-ls 20 | - terraform-schema 21 | - hcl-lang 22 | - key: Issue 23 | label: Issue/PR Number 24 | type: int 25 | minInt: 1 26 | kinds: 27 | - label: ENHANCEMENTS 28 | auto: minor 29 | - label: BUG FIXES 30 | auto: patch 31 | - label: INTERNAL 32 | auto: patch 33 | - label: NOTES 34 | auto: patch 35 | - label: BREAKING CHANGES 36 | auto: minor 37 | newlines: 38 | afterKind: 1 39 | beforeKind: 1 40 | endOfVersion: 2 41 | envPrefix: CHANGIE_ 42 | replacements: 43 | - path: package.json 44 | find: ' "version": ".*",' 45 | replace: ' "version": "{{.VersionNoPrefix}}",' 46 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | 6 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 7 | # Supports doublestar glob patterns for more flexibility in defining which 8 | # files or folders should be ignored 9 | header_ignore = [ 10 | ".changes/**", 11 | ".github/ISSUE_TEMPLATE/**", 12 | ".vscode-test/**", 13 | ".wdio-vscode-service/**", 14 | "**/node_modules/**", 15 | "out/**", 16 | "dist/**", 17 | "src/test/fixtures/**", 18 | "src/test/integration/*/workspace/**", 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{diff,md}] 16 | trim_trailing_whitespace = false 17 | insert_final_newline = false 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | .wdio-vscode-service 3 | **/*.d.ts 4 | **/*/__mocks__ 5 | node_modules 6 | dist 7 | out 8 | src/test/fixtures 9 | src/test/integration/*/workspace/** 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended"], 4 | "env": { 5 | "browser": true, 6 | "es2021": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": false 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "curly": "warn", 17 | "eqeqeq": "warn", 18 | "no-throw-literal": "warn", 19 | "semi": "off" 20 | }, 21 | "overrides": [ 22 | { 23 | "files": ["**/*.ts"], 24 | "parser": "@typescript-eslint/parser", 25 | "parserOptions": { 26 | "ecmaVersion": 6, 27 | "sourceType": "module" 28 | }, 29 | "plugins": ["@typescript-eslint", "prettier"], 30 | "extends": [ 31 | "eslint:recommended", 32 | "plugin:@typescript-eslint/eslint-recommended", 33 | "plugin:@typescript-eslint/recommended", 34 | "plugin:prettier/recommended", 35 | "prettier" 36 | ], 37 | "rules": { 38 | "@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }], 39 | "@typescript-eslint/naming-convention": "off", 40 | "@typescript-eslint/semi": "warn", 41 | "curly": "warn", 42 | "eqeqeq": "warn", 43 | "no-throw-literal": "warn", 44 | "semi": "off" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure line endings are always Unix-style for known text files 2 | # GitHub Actions CI doesn't have "native" solution for this 3 | # See https://github.com/actions/checkout/issues/226 4 | *.ts text eol=lf 5 | language-configuration.json linguist-language=jsonc 6 | .vscode/**.json linguist-language=jsonc 7 | *.json text eol=lf 8 | *.md text eol=lf 9 | *.tf text eol=lf 10 | *.txt text eol=lf 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/.github/CODEOWNERS -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the OpenTofu Extension for VS Code 2 | 3 | ## Reporting Feedback 4 | 5 | The OpenTofu Extension for VS Code is an open source project and we appreciate 6 | contributions of various kinds, including bug reports and fixes, 7 | enhancement proposals, documentation updates, and user experience feedback. 8 | 9 | To record a bug report, enhancement proposal, or give any other product 10 | feedback, please [open a GitHub issue](https://github.com/gamunu/vscode-opentofu/issues/new/choose) 11 | using the most appropriate issue template. Please do fill in all of the 12 | information the issue templates request, because we've seen from experience that 13 | this will maximize the chance that we'll be able to act on your feedback. 14 | 15 | 16 | ## Development 17 | 18 | Please see [DEVELOPMENT.md](../DEVELOPMENT.md). 19 | 20 | ## Releasing 21 | 22 | Releases are made on a reasonably regular basis by the maintainers, using our internal tooling. The following notes are only relevant to maintainers. 23 | 24 | Release Process: 25 | 26 | 1. Create a release branch/PR with the following changes: 27 | 28 | - Bump the language server version in `package.json` if the release should include a LS update 29 | - Update the [CHANGELOG.md](../CHANGELOG.md) with the recent changes 30 | - Bump the version in `package.json` (and `package-lock.json`) by using `npm version 2.X.Y` 31 | 32 | 1. Review & merge the branch and wait for the [Test Workflow](https://github.com/gamunu/vscode-opentofu/actions/workflows/test.yml) on `main` to complete 33 | 1. Run the [Deploy Workflow](https://github.com/gamunu/vscode-opentofu/actions/workflows/deploy.yml) from `main` for a `stable` release 34 | 1. Wait for publishing & processing to complete 35 | 1. Create a new release & tag on GitHub for the version `v2.X.Y` and copy over the changelog 36 | 1. Optional: Wait ~10 minutes and run the [Deploy Workflow](https://github.com/gamunu/vscode-opentofu/actions/workflows/deploy.yml) from `main` for a `prerelease` release to also update the preview extension 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | blank_issues_enabled: false 5 | contact_links: 6 | - name: Terraform Language Features / Bugs 7 | url: https://github.com/hashicorp/terraform-ls/issues 8 | about: >- 9 | Most of the language features of the VS Code extension are supplied by the Terraform 10 | Language Server. Issues with language features in many cases may need to be filed on 11 | that repository. 12 | - name: Discussion and Questions 13 | url: https://github.com/gamunu/vscode-opentofu/discussions 14 | about: Please use the OpenTofu extension Github discussions for questions and discussions. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: "Documentation issue or request" 2 | description: "Report an issue with our docs or Marketplace listing, or suggest additions and improvements to our documentation" 3 | labels: [documentation] 4 | assignees: [] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We use GitHub issues for tracking bugs and enhancements. For questions, please use [the github discussions](https://github.com/gamunu/vscode-opentofu/discussions) where there are more people ready to help. 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: A clear and concise description of the issue in plain English. 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: links 20 | attributes: 21 | label: Links 22 | description: | 23 | Include links to affected or related documentation page(s) or issues. 24 | Guide to referencing Github issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests 25 | placeholder: | 26 | - https://github.com/gamunu/vscode-opentofu/blob/main/README.md#formatting 27 | - https://github.com/gamunu/vscode-opentofu/blob/main/docs/settings-migration.md 28 | - #123 29 | - #456 30 | validations: 31 | required: true 32 | 33 | - type: checkboxes 34 | id: contribution 35 | attributes: 36 | label: Help Wanted 37 | description: Is this something you're able to or interested in helping out with? This is not required but a helpful way to signal to us that you're planning to open a PR with a fix. 38 | options: 39 | - label: I'm interested in contributing a fix myself 40 | required: false 41 | 42 | - type: textarea 43 | id: community 44 | attributes: 45 | label: Community Note 46 | description: Please do not remove, edit, or change the following note for our community. Just leave everything in this textbox as-is. 47 | value: | 48 | - Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 49 | - Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 50 | - If you are interested in working on this issue or have submitted a pull request, please leave a comment 51 | validations: 52 | required: true 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature request" 2 | description: "Suggest a new feature or other enhancement." 3 | labels: [enhancement] 4 | assignees: [] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We use GitHub issues for tracking bugs and enhancements. For questions, please use [the github discussions](https://github.com/gamunu/vscode-opentofu/discussions) where there are more people ready to help. 9 | 10 | - type: input 11 | id: version 12 | attributes: 13 | label: Extension Version 14 | description: | 15 | Find this in the VS Code UI: Extensions Pane -> Installed -> OpenTofu 16 | placeholder: v0.1.0 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: problem 22 | attributes: 23 | label: Problem Statement 24 | description: | 25 | In order to properly evaluate a feature request, it is necessary to understand the use cases for it. 26 | Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. 27 | Please keep this section focused on the problem and not on the suggested solution. We'll get to that in a moment, below! 28 | 29 | If you've already tried to solve the problem with existing features and found a limitation that prevented you from succeeding, please describe it below in as much detail as possible. 30 | Ideally, this would include real configuration snippets that you tried, actions you performed (e.g. autocompletion in a particular position in that snippet), and what results you got in each case. 31 | Please remove any sensitive information such as passwords before sharing configuration snippets. 32 | placeholder: | 33 | (I can do ... today but ...) 34 | 35 | When I do ... then ... 36 | It would be helpful if ... 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: ux 42 | attributes: 43 | label: Expected User Experience 44 | description: | 45 | If you already have an idea of what this feature might look like in the editor and can produce screenshots, drawings or ASCII art - please attach it here. 46 | placeholder: | 47 | I would be able to see ... when I click/type ... 48 | validations: 49 | required: false 50 | 51 | - type: textarea 52 | id: proposal 53 | attributes: 54 | label: Proposal 55 | description: | 56 | If you have an idea for a way to address the problem via a change to existing features, please describe it below. 57 | In this section, it's helpful to include specific examples of how what you are suggesting might look in configuration files, or in the UI, since that allows us to understand the full picture of what you are proposing. 58 | If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Language Server and VS Code. 59 | validations: 60 | required: false 61 | 62 | - type: textarea 63 | id: references 64 | attributes: 65 | label: References 66 | description: | 67 | Are there any other GitHub issues (open or closed) or pull requests that relate to this request? Or links to documentation pages? 68 | Guide to referencing Github issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests 69 | placeholder: | 70 | - #123 71 | - #456 72 | - opentofu/opentofu#1 73 | - https://opentofu.org/docs/language/expressions/dynamic-blocks/ 74 | validations: 75 | required: false 76 | 77 | - type: checkboxes 78 | id: contribution 79 | attributes: 80 | label: Help Wanted 81 | description: Is this something you're able to or interested in helping out with? This is not required but a helpful way to signal to us that you're planning to open a PR with a fix. 82 | options: 83 | - label: I'm interested in contributing a fix myself 84 | required: false 85 | 86 | - type: textarea 87 | id: community 88 | attributes: 89 | label: Community Note 90 | description: Please do not remove, edit, or change the following note for our community. Just leave everything in this textbox as-is. 91 | value: | 92 | - Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 93 | - Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 94 | - If you are interested in working on this issue or have submitted a pull request, please leave a comment 95 | validations: 96 | required: true 97 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.yml: -------------------------------------------------------------------------------- 1 | name: "Performance issue report" 2 | description: "Let us know about issues with performance, such as slow speed or abnormally high CPU or memory usage." 3 | labels: [performance] 4 | assignees: [] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We use GitHub issues for tracking bugs and enhancements. For questions, please use [the github discussions](https://github.com/gamunu/vscode-opentofu/discussions) where there are more people ready to help. 9 | 10 | - type: input 11 | id: version 12 | attributes: 13 | label: Extension Version 14 | description: | 15 | Find this in the VS Code UI: Extensions Pane -> Installed -> OpenTofu 16 | placeholder: v0.1.0 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: vscode 22 | attributes: 23 | label: VS Code Version 24 | description: | 25 | Copy this from VS Code: 26 | - Windows/Linux: Help -> About 27 | - macOS: Code -> About Visual Studio Code 28 | placeholder: | 29 | Version: 1.78.2 (Universal) 30 | Commit: b3e4e68a0bc097f0ae7907b217c1119af9e03435 31 | Date: 2023-05-10T14:44:45.204Z 32 | Electron: 22.5.2 33 | Chromium: 108.0.5359.215 34 | Node.js: 16.17.1 35 | V8: 10.8.168.25-electron.0 36 | OS: Darwin arm64 21.6.0 37 | Sandboxed: Yes 38 | validations: 39 | required: true 40 | 41 | - type: input 42 | id: os 43 | attributes: 44 | label: Operating System 45 | description: | 46 | Find version and build (32-bit or 64-bit) of your OS 47 | - macOS: Apple logo -> About This Mac 48 | - Windows: right-click on Windows logo -> Settings -> Device and Windows specifications 49 | - Linux: `uname -a` 50 | - Ubuntu: `cat /etc/issue` 51 | Also note whether you use WSL (Windows Subsystem for Linux) when on Windows. 52 | placeholder: macOS Monterey Version 12.6.5 (21G531) 53 | validations: 54 | required: true 55 | 56 | - type: input 57 | id: terraform 58 | attributes: 59 | label: Terraform Version 60 | description: | 61 | Output of `terraform version` 62 | placeholder: Terraform v1.4.6 on darwin_arm64 63 | validations: 64 | required: true 65 | 66 | - type: textarea 67 | id: behavior 68 | attributes: 69 | label: Behavior 70 | description: | 71 | What happens? What symptoms of poor performance are you seeing? Please give a clear and concise description in plain English. 72 | validations: 73 | required: true 74 | 75 | - type: textarea 76 | id: steps 77 | attributes: 78 | label: Steps to Reproduce 79 | description: | 80 | Please list the steps required to reproduce the issue. If applicable, add screenshots to help explain your problem. 81 | placeholder: | 82 | 1. Go to '...' 83 | 2. Type '...' 84 | 3. See error 85 | validations: 86 | required: true 87 | 88 | - type: textarea 89 | id: configuration 90 | attributes: 91 | label: Terraform Configuration 92 | description: | 93 | Include any relevant Terraform configuration that might be helpful for reproducing your issue. 94 | Please remove any sensitive information such as passwords before sharing configuration snippets. 95 | placeholder: | 96 | resource "github_repository" "test" { 97 | name = "vscode-terraform" 98 | } 99 | 100 | # etc... 101 | render: terraform 102 | validations: 103 | required: false 104 | 105 | - type: textarea 106 | id: tree 107 | attributes: 108 | label: Project Structure 109 | description: | 110 | Optionally, use `tree` to output ASCII-based hierarchy of your project. 111 | placeholder: | 112 | . 113 | ├── LICENSE 114 | ├── README.md 115 | ├── aks 116 | │ ├── main.tf 117 | │ ├── outputs.tf 118 | │ ├── terraform.tfvars.example 119 | │ ├── variables.tf 120 | │ └── versions.tf 121 | ├── consul 122 | │ ├── dc1.yaml 123 | │ ├── dc2.yaml 124 | │ ├── main.tf 125 | │ ├── proxy_defaults.tf 126 | │ └── versions.tf 127 | ├── counting-service 128 | │ ├── main.tf 129 | │ └── versions.tf 130 | └── eks 131 | ├── kubernetes.tf 132 | ├── main.tf 133 | ├── outputs.tf 134 | ├── variables.tf 135 | └── versions.tf 136 | render: sh 137 | validations: 138 | required: false 139 | 140 | - type: input 141 | id: gist 142 | attributes: 143 | label: Gist 144 | description: | 145 | If possible, please provide a link to a [GitHub Gist](https://gist.github.com/) containing a larger code sample and/or the [log output](https://user-images.githubusercontent.com/45985/239918316-8ad0a91b-c724-4f89-ae8f-1a992385bd77.png). 146 | Please remove any sensitive information such as passwords before sharing configuration files. 147 | placeholder: | 148 | https://gist.github.com/gdb/b6365e79be6052e7531e7ba6ea8caf23 149 | validations: 150 | required: false 151 | 152 | - type: textarea 153 | id: miscellaneous 154 | attributes: 155 | label: Anything Else? 156 | description: | 157 | Is there anything else we should know? For example, do you you use any tools for managing Terraform version/execution (e.g. `tfenv`) or any credentials helpers? 158 | Do you have any other Terraform extensions installed? 159 | validations: 160 | required: false 161 | 162 | - type: textarea 163 | id: references 164 | attributes: 165 | label: References 166 | description: | 167 | Are there any other GitHub issues (open or closed) or pull requests that relate to this issue? Or links to documentation pages? 168 | Guide to referencing Github issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests 169 | placeholder: | 170 | - #123 171 | - #456 172 | - hashicorp/terraform#789 173 | - https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks 174 | validations: 175 | required: false 176 | 177 | - type: textarea 178 | id: community 179 | attributes: 180 | label: Community Note 181 | description: Please do not remove, edit, or change the following note for our community. Just leave everything in this textbox as-is. 182 | value: | 183 | - Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 184 | - Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 185 | - If you are interested in working on this issue or have submitted a pull request, please leave a comment 186 | validations: 187 | required: true 188 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | If you have questions about OpenTofu VS Code extension usage, please feel free to create a topic on [the GitHub discussions](https://github.com/gamunu/vscode-opentofu/discussions). -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | versioning-strategy: lockfile-only 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: ["dependencies"] 9 | ignore: 10 | - dependency-name: "@types/*" 11 | update-types: 12 | ["version-update:semver-minor", "version-update:semver-patch"] 13 | - dependency-name: "@typescript-eslint/*" 14 | update-types: 15 | ["version-update:semver-minor", "version-update:semver-patch"] 16 | # Dependabot only updates hashicorp GHAs, external GHAs are managed by internal tooling (tsccr) 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | allow: 22 | - dependency-name: "hashicorp/*" 23 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | changelog: 5 | categories: 6 | - title: BREAKING CHANGES 7 | labels: 8 | - breaking 9 | - title: NOTES 10 | labels: 11 | - note 12 | - documentation 13 | - title: ENHANCEMENTS 14 | labels: 15 | - enhancement 16 | - title: BUG FIXES 17 | labels: 18 | - bug 19 | - title: INTERNAL 20 | labels: 21 | - dependencies 22 | - ci 23 | - "*" 24 | exclude: 25 | labels: 26 | - duplicate 27 | - question 28 | - needs-reproduction 29 | - needs-research 30 | - stale 31 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/check-changelog.yml: -------------------------------------------------------------------------------- 1 | # Checks if a file has been committed under the .changes/unreleased directory 2 | # 3 | # Skip PRs labeled with 'dependencies' 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - main 9 | - pre-release 10 | 11 | name: Check if changelog entry exists 12 | 13 | jobs: 14 | changelog_existence: 15 | name: Check if changelog entry exists 16 | if: "!contains(github.event.pull_request.labels.*.name, 'dependencies')" 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Check if changelog file was added 21 | # https://github.com/marketplace/actions/paths-changes-filter 22 | # For each filter, it sets output variable named by the filter to the text: 23 | # 'true' - if any of changed files matches any of filter rules 24 | # 'false' - if none of changed files matches any of filter rules 25 | uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 26 | id: changelog_check 27 | with: 28 | filters: | 29 | exists: 30 | - '.changes/unreleased/**.yaml' 31 | 32 | - name: Fail job if changelog entry is missing and required 33 | if: steps.changelog_check.outputs.exists == 'false' 34 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 35 | with: 36 | script: core.setFailed('Changelog entry required to merge.') 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish Extension release 3 | 4 | permissions: 5 | contents: write # for uploading release artifacts 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | deploy_type: 11 | description: 'Deploy a stable or prerelease extension' 12 | type: choice 13 | required: true 14 | default: stable 15 | options: 16 | - stable 17 | - prerelease 18 | langserver: 19 | description: 'The terraform-ls version to use. If not specified will use version in package.json' 20 | required: false 21 | type: string 22 | 23 | jobs: 24 | build: 25 | name: Package 26 | strategy: 27 | matrix: 28 | include: 29 | - vsce_target: web 30 | ls_target: web_noop 31 | npm_config_arch: x64 32 | - vsce_target: win32-x64 33 | ls_target: windows_amd64 34 | npm_config_arch: x64 35 | - vsce_target: win32-arm64 36 | ls_target: windows_arm64 37 | npm_config_arch: arm 38 | - vsce_target: linux-x64 39 | ls_target: linux_amd64 40 | npm_config_arch: x64 41 | - vsce_target: linux-arm64 42 | ls_target: linux_arm64 43 | npm_config_arch: arm64 44 | - vsce_target: darwin-x64 45 | ls_target: darwin_amd64 46 | npm_config_arch: x64 47 | - vsce_target: darwin-arm64 48 | ls_target: darwin_arm64 49 | npm_config_arch: arm64 50 | runs-on: 'ubuntu-latest' 51 | steps: 52 | - name: Check out repository 53 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 54 | 55 | - name: Setup Node 56 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 57 | with: 58 | node-version-file: '.nvmrc' 59 | cache: npm 60 | 61 | - name: Install dependencies 62 | run: npm ci 63 | env: 64 | npm_config_arch: ${{ matrix.npm_config_arch }} 65 | ls_target: ${{ matrix.ls_target }} 66 | 67 | - name: Package PreRelease VSIX 68 | if: inputs.deploy_type == 'prerelease' 69 | run: npm run package -- --githubBranch $GITHUB_REF_NAME --pre-release --target=${{ matrix.vsce_target }} 70 | 71 | - name: Package Stable VSIX 72 | if: inputs.deploy_type == 'stable' 73 | run: npm run package -- --target=${{ matrix.vsce_target }} 74 | 75 | - name: Upload vsix as artifact 76 | uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 77 | with: 78 | name: ${{ matrix.vsce_target }} 79 | path: '*.vsix' 80 | 81 | publish-pre-marketplace: 82 | name: Publish Prerelease to Marketplace 83 | runs-on: ubuntu-latest 84 | needs: build 85 | if: success() && inputs.deploy_type == 'prerelease' 86 | steps: 87 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 88 | - name: Publish Prerelease to Marketplace 89 | run: npx vsce publish --pre-release --no-git-tag-version --packagePath $(find . -iname *.vsix) 90 | env: 91 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 92 | 93 | publish-stable-marketplace: 94 | name: Publish Marketplace 95 | runs-on: ubuntu-latest 96 | needs: build 97 | if: success() && inputs.deploy_type == 'stable' 98 | steps: 99 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 100 | - name: Publish Stable to Marketplace 101 | run: npx vsce publish --no-git-tag-version --packagePath $(find . -iname *.vsix) 102 | env: 103 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 104 | 105 | publish-stable-openvsx: 106 | name: Publish OpenVSX 107 | runs-on: ubuntu-latest 108 | needs: build 109 | if: success() && inputs.deploy_type == 'stable' 110 | steps: 111 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 112 | - name: Publish Stable to OpenVSX 113 | run: npx ovsx publish --packagePath $(find . -iname *.vsix) 114 | env: 115 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 116 | -------------------------------------------------------------------------------- /.github/workflows/issue-comment-created.yml: -------------------------------------------------------------------------------- 1 | name: Issue Comment Created Triage 2 | 3 | permissions: 4 | issues: write 5 | pull-requests: write 6 | 7 | on: 8 | issue_comment: 9 | types: [created] 10 | 11 | jobs: 12 | issue_comment_triage: 13 | # Only run this job for issue comments 14 | if: ${{ !github.event.issue.pull_request }} 15 | runs-on: ubuntu-latest 16 | env: 17 | GH_TOKEN: ${{ github.token }} 18 | steps: 19 | - name: Remove stale and waiting-response labels 20 | run: gh issue edit ${{ github.event.issue.html_url }} --remove-label stale,waiting-response 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | permissions: 4 | issues: write 5 | pull-requests: write 6 | 7 | on: 8 | schedule: 9 | - cron: '0 3 * * *' 10 | 11 | jobs: 12 | lock: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 16 | with: 17 | issue-comment: > 18 | I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. 19 | 20 | If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. 21 | issue-inactive-days: '30' 22 | pr-comment: > 23 | I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. 24 | 25 | If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. 26 | pr-inactive-days: '30' 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale issues and pull requests" 2 | 3 | permissions: 4 | issues: write 5 | pull-requests: write 6 | 7 | on: 8 | schedule: 9 | - cron: "10 3 * * *" 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 16 | with: 17 | only-labels: 'waiting-response' 18 | 19 | days-before-stale: 30 20 | stale-issue-label: 'stale' 21 | stale-issue-message: | 22 | Marking this issue as stale due to inactivity over the last 30 days. This helps our maintainers find and focus on the active issues. If this issue receives no comments in the next **30 days** it will automatically be closed. Maintainers can also remove the stale label. 23 | 24 | Thank you for understanding. 25 | stale-pr-label: 'stale' 26 | stale-pr-message: | 27 | Marking this pull request as stale due to inactivity over the last 30 days. This helps our maintainers find and focus on the active pull requests. If this pull request receives no comments in the next **30 days** it will automatically be closed. Maintainers can also remove the stale label. 28 | 29 | Thank you for understanding. 30 | 31 | days-before-close: 30 32 | close-issue-message: | 33 | Closing this issue due to its staleness. 34 | 35 | If the issue was automatically closed and you feel it should be reopened, we encourage creating a new one linking back to this one for added context. 36 | 37 | Thank you! 38 | close-pr-message: | 39 | Closing this pull request due to its staleness. 40 | 41 | If the pull request was automatically closed and you feel it should be reopened, we encourage creating a new one linking back to this one for added context. 42 | 43 | Thank you! 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - pre-release 9 | paths-ignore: 10 | - 'syntaxes/**' 11 | - 'tests/**' 12 | - '**.md' 13 | pull_request: 14 | branches: 15 | - main 16 | - pre-release 17 | paths-ignore: 18 | - 'syntaxes/**' 19 | - 'tests/**' 20 | - '**.md' 21 | 22 | jobs: 23 | lint: 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 3 26 | steps: 27 | - name: Checkout Repo 28 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 29 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 30 | with: 31 | node-version-file: '.nvmrc' 32 | cache: npm 33 | - name: npm install 34 | run: npm ci 35 | - name: lint 36 | run: npm run lint 37 | - name: format 38 | run: npm run check-format 39 | 40 | test: 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | vscode: 45 | - '1.88.0' 46 | - 'insiders' 47 | - 'stable' 48 | os: 49 | - windows-latest 50 | - macos-latest 51 | - ubuntu-latest 52 | runs-on: ${{ matrix.os }} 53 | timeout-minutes: 10 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | steps: 57 | - name: Checkout Repo 58 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 59 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 60 | with: 61 | node-version-file: '.nvmrc' 62 | - name: Set up Xvfb (Ubuntu) 63 | run: | 64 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 65 | echo ">>> Started xvfb" 66 | if: matrix.os == 'ubuntu-latest' 67 | - name: Install OpenTofu 68 | uses: opentofu/setup-opentofu@v1 69 | with: 70 | tofu_wrapper: false 71 | tofu_version: 1.8.0 72 | - name: Tofu version 73 | run: tofu version 74 | - name: Clean Install Dependencies 75 | run: npm ci 76 | - name: Run Tests 77 | run: xvfb-run -a npm test 78 | if: runner.os == 'Linux' 79 | - name: Run Tests 80 | run: npm test 81 | if: runner.os != 'Linux' 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .terraform 3 | .vscode-test 4 | .vscode-test-web 5 | .wdio-vscode-service 6 | *.vsix 7 | bin 8 | node_modules 9 | npm-debug.log 10 | dist 11 | out 12 | syntaxes 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | .vscode-test-web 3 | .wdio-vscode-service 4 | language-configuration.json 5 | node_modules 6 | dist 7 | out 8 | package-lock.json 9 | snippets 10 | src/test/fixtures 11 | src/test/integration/*/workspace/** 12 | syntaxes 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 120, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { defineConfig } from '@vscode/test-cli'; 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | 10 | // Discover test suite folders in src/test/integration 11 | const BASE_SRC_PATH = './src/test/integration'; 12 | const BASE_OUT_PATH = './out/test/integration'; 13 | 14 | const testSuiteFolderNames = fs.readdirSync(BASE_SRC_PATH, { withFileTypes: true }) 15 | .filter(entry => entry.isDirectory()) // only directories ... 16 | .filter(entry => fs.existsSync(path.join(BASE_SRC_PATH, entry.name, "workspace"))) // ... that contain a workspace folder are valid 17 | .map(entry => entry.name); 18 | 19 | const configs = testSuiteFolderNames.map(folderName => ({ 20 | version: process.env['VSCODE_VERSION'] ?? 'stable', 21 | workspaceFolder: process.env['VSCODE_WORKSPACE_FOLDER'] ?? path.join(BASE_SRC_PATH, folderName, "workspace"), 22 | launchArgs: ['--disable-extensions', '--disable-workspace-trust'], 23 | files: `${BASE_OUT_PATH}/${folderName}/*.test.js`, 24 | mocha: { 25 | ui: 'tdd', 26 | color: true, 27 | timeout: 100000, 28 | // require: ['./out/test/mockSetup.js'], // mocks are shared for all test suites, but not needed for opentofu 29 | }, 30 | })); 31 | 32 | const config = defineConfig(configs); 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "amodio.tsl-problem-matcher", 4 | "connor4312.esbuild-problem-matchers", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "ms-vscode.extension-test-runner", 8 | "redhat.vscode-yaml" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": ["--profile-temp", "--extensionDevelopmentPath=${workspaceFolder}"], 9 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 10 | "preLaunchTask": "${defaultBuildTask}" 11 | }, 12 | { 13 | "name": "Develop Extension", 14 | "type": "extensionHost", 15 | "request": "launch", 16 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 17 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Run Web Extension ", 22 | "type": "pwa-extensionHost", 23 | "debugWebWorkerHost": true, 24 | "request": "launch", 25 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web"], 26 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 27 | "preLaunchTask": "npm: watch" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.tabSize": 2, 4 | 5 | "files.encoding": "utf8", 6 | "files.eol": "\n", 7 | "files.insertFinalNewline": true, 8 | "files.trimFinalNewlines": true, 9 | "files.trimTrailingWhitespace": true, 10 | 11 | "files.exclude": { 12 | "out": false 13 | }, 14 | "search.exclude": { 15 | "node_modules": true, 16 | "out": true 17 | }, 18 | 19 | "typescript.format.enable": false, 20 | "typescript.tsc.autoDetect": "off", 21 | 22 | "[javascript]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[json]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[jsonc]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[markdown]": { 32 | "editor.wordWrapColumn": 80, 33 | "editor.wordWrap": "wordWrapColumn", 34 | "editor.quickSuggestions": { 35 | "comments": "off", 36 | "strings": "off", 37 | "other": "off" 38 | }, 39 | "files.trimTrailingWhitespace": false, 40 | "files.insertFinalNewline": false, 41 | "files.trimFinalNewlines": false 42 | }, 43 | "[typescript]": { 44 | "editor.codeActionsOnSave": { 45 | "source.fixAll": "explicit", 46 | "source.organizeImports": "never" 47 | }, 48 | "editor.defaultFormatter": "esbenp.prettier-vscode" 49 | }, 50 | 51 | "json.schemas": [ 52 | { 53 | "fileMatch": ["*.tmGrammar.json"], 54 | "url": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json" 55 | } 56 | ], 57 | "yaml.schemas": { 58 | "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json": "*.tmGrammar.yml" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": ["npm: watch:tsc", "npm: watch:esbuild"], 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "watch:esbuild", 20 | "group": "build", 21 | "problemMatcher": "$esbuild-watch", 22 | "isBackground": true, 23 | "label": "npm: watch:esbuild", 24 | "presentation": { 25 | "group": "watch", 26 | "reveal": "never" 27 | } 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "watch:tsc", 32 | "group": "build", 33 | "problemMatcher": "$tsc-watch", 34 | "isBackground": true, 35 | "label": "npm: watch:tsc", 36 | "presentation": { 37 | "group": "watch", 38 | "reveal": "never" 39 | } 40 | }, 41 | { 42 | "type": "npm", 43 | "script": "watch-tests", 44 | "problemMatcher": "$tsc-watch", 45 | "isBackground": true, 46 | "presentation": { 47 | "reveal": "never", 48 | "group": "watchers" 49 | }, 50 | "group": "build" 51 | }, 52 | { 53 | "label": "tasks: watch-tests", 54 | "dependsOn": ["npm: watch", "npm: watch-tests"], 55 | "problemMatcher": [] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .changes 2 | .changie.yaml 3 | .copywrite.hcl 4 | .editorconfig 5 | .eslintignore 6 | .eslintrc.json 7 | .gitattributes 8 | .github 9 | .gitignore 10 | .npmrc 11 | .nvmrc 12 | .prettierignore 13 | .prettierrc 14 | .vscode 15 | .vscode-test 16 | .vscode-test-web 17 | .vscode-test.mjs 18 | .vscodeignore 19 | .wdio-vscode-service 20 | **/__mocks__ 21 | build/ 22 | DEVELOPMENT.md 23 | docs/ 24 | node_modules/ 25 | ROADMAP.md 26 | src/ 27 | out/ 28 | esbuild.js 29 | tsconfig.json 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.1 (2024-09-11) 4 | 5 | ## 0.2.0 (2024-09-11) 6 | 7 | ## 0.2.0-beta1 (2024-09-11) 8 | 9 | ## 0.1.1 (2024-09-11) 10 | 11 | ## 0.1.0 (2024-09-11) 12 | 13 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | We are an open source project on GitHub and would enjoy your contributions! Please [open a new issue](https://github.com/gamunu/terraform-opentofu/issues) before working on a PR that requires significant effort. This will allow us to make sure the work is in line with the project's goals. 4 | 5 | ## Building 6 | 7 | The extension makes use of the [VSCode Language Server](https://github.com/Microsoft/vscode-languageserver-node) client package to integrate with [terraform-ls](https://github.com/hashicorp/terraform-ls) for [language features](https://code.visualstudio.com/api/language-extensions/programmatic-language-features). The directions below cover how to build and package the extension; please see the [`terraform-ls`](https://github.com/hashicorp/terraform-ls) documentation for how to build the language server executable. 8 | 9 | ### Requirements 10 | 11 | - VSCode >= 1.61 12 | - Node >= 16.13.2 13 | - npm >= 8.x 14 | 15 | ### Getting the code 16 | 17 | ``` 18 | git clone https://github.com/gamunu/vscode-opentofu 19 | ``` 20 | 21 | ### Dependencies 22 | 23 | After cloning the repo, run `npm install` to install dependencies. There's an included build task to compile the TypeScript files to JavaScript; you can run it directly with `npm run compile`. 24 | 25 | ``` 26 | > npm install 27 | > npm run compile 28 | ``` 29 | 30 | > In order to use an independently built or installed version of terraform-ls, you will need to set `opentofu.languageServer.path` to the correct executable path. 31 | 32 | ## Running the Extension 33 | 34 | The extension can be run in a development mode via the launch task called `Run Extension`. This will open a new VS Code window with the extension loaded, and from there you can open any files or folders you want to check against. This extension development window can also be used to run commands or any other feature the extension provides. 35 | 36 | > New to VS Code development? You can get started [here](https://code.visualstudio.com/api/get-started/your-first-extension) 37 | 38 | ## Running the Web Extension 39 | 40 | ### Run in VS Code on the desktop 41 | 42 | The extension can be run in a development mode via the launch task called `Run Web Extension`. This will open a new VS Code window with the extension loaded in 'desktop' context, which is close to what a Github Codespace operates as. 43 | 44 | ### Run in VS Code in the browser locally 45 | 46 | 47 | The extension can be run in a browser like github.dev or vscode.dev, except locally by running: 48 | 49 | ``` 50 | npm run web 51 | ``` 52 | 53 | ### Run in VS Code in vscode.dev 54 | 55 | The extension can be run in vscode.dev directly by sideloading the extension. This is the closet to running the extension in production as you can get. To sideload the extension: 56 | 57 | ``` 58 | npm run web:serve 59 | npm run web:tunnel 60 | ``` 61 | 62 | Then follow https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension-in-on-vscode.dev 63 | 64 | ## Tests 65 | 66 | Automated `unit` and `integration` tests can be written using [mocha](https://mochajs.org) and live inside `./src/test` with file pattern `*.test.ts`. 67 | 68 | > It is *required* that `tofu` is available on `$PATH` to run the tests. 69 | 70 | To run the `unit tests` from the command-line run: 71 | 72 | ```bash 73 | > `npm test:unit` 74 | ``` 75 | 76 | To run the `integration tests` from the command-line without any configuration, run `npm test`. By default, `npm test` will test against VS Code Stable. If you want to test against a different VS Code version, or if you want to have VS Code remain open, use an environment variable to indicate which version of VS Code to test against: 77 | 78 | ```bash 79 | # VS Code Stable is open, use Insiders: 80 | > VSCODE_VERSION='insiders' npm test 81 | 82 | # VS Code Insiders is open, use Stable: 83 | > VSCODE_VERSION='stable' npm test 84 | 85 | # Test against VS Code v1.55.8: 86 | > VSCODE_VERSION='1.55.8' npm test 87 | ``` 88 | 89 | To run the `integration` tests in PowerShell, set the environment variable accordingly: 90 | 91 | ```powershell 92 | # VS Code Stable is open, use Insiders: 93 | > $env:VSCODE_VERSION ='insiders' 94 | > npm test 95 | ``` 96 | 97 | The tests can also be run within VSCode itself, using the launch task `Run Extension Tests`. This will open a new VS Code window, run the test suite, and exit with the test results. 98 | 99 | ### Acceptance Tests 100 | 101 | End to end acceptance tests with the extension running against the language server are a work in progress. An example can be seen in [`./src/test/integration/symbols.test.ts`](src/test/integration/symbols.test.ts). 102 | 103 | Unfortunately automated user input does not appear to be possible (keypresses, cursor clicks) at the moment, but some integration testing can be performed by using the vscode API to open/edit files, and triggering events/commands such as language server requests and verifying the responses. 104 | 105 | The `tofu init` command runs automatically when tests are executed. 106 | 107 | ### Web Extension Tests 108 | 109 | 110 | To run unit tests for the web: 111 | 112 | ``` 113 | npm run test:unit:web 114 | ``` 115 | 116 | ### Test Fixture Data 117 | 118 | Sample files for tests should be added to the [`./testFixture`](testFixture/) folder, this is the folder vscode will open during tests. Starting from this folder will prevent the language server from walking other folders that typically exist such as `node_modules`. 119 | 120 | 121 | ## Packaging 122 | 123 | To package the extension into a [`platform specific extension`](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions) VSIX ready for testing run the following command: 124 | 125 | ``` 126 | npm run package -- --target=win32-x64 127 | ``` 128 | 129 | Replace `target` with the platform/architecture combination that is on the supported matrix list. 130 | 131 | platform | terraform-ls | extension | vs code 132 | -- | -- | -- | -- 133 | macOS | darwin_amd64 | darwin_x64 | ✅ 134 | macOS | darwin_arm64 | darwin_arm64 | ✅ 135 | Linux | linux_amd64 | linux_x64 | ✅ 136 | Linux | linux_arm | linux_armhf | ✅ 137 | Linux | linux_arm64 | linux_arm64 | ✅ 138 | Windows | windows_amd64 | win32_x64 | ✅ 139 | Windows | windows_arm64 | win32_arm64 | ✅ 140 | 141 | This will run several chained commands which will download the specified version of terraform-ls, minify the extension using Webpack, and package the extension using vsce into a VSIX. 142 | 143 | > You can run `npm run package` without paramaters, but this will not produce a platform specific extension. 144 | -------------------------------------------------------------------------------- /assets/icons/opentofu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/icons/opentofu_logo_universal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/assets/icons/opentofu_logo_universal.png -------------------------------------------------------------------------------- /assets/icons/running.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/assets/icons/running.woff -------------------------------------------------------------------------------- /assets/icons/terraform_stacks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/icons/vs_code_opentofu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/downloader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | import axios from 'axios'; 9 | 10 | async function fileFromUrl(url: string): Promise { 11 | const response = await axios.get(url, { responseType: 'arraybuffer' }); 12 | return Buffer.from(response.data, 'binary'); 13 | } 14 | 15 | export interface Release { 16 | repository: string; 17 | package: string; 18 | destination: string; 19 | fileName: string; 20 | version: string; 21 | extract: boolean; 22 | } 23 | 24 | function getPlatform(platform: string) { 25 | if (platform === 'win32') { 26 | return 'windows'; 27 | } 28 | if (platform === 'sunos') { 29 | return 'solaris'; 30 | } 31 | return platform; 32 | } 33 | 34 | function getArch(arch: string) { 35 | // platform | terraform-ls | extension platform | vs code editor 36 | // -- | -- | -- | -- 37 | // macOS | darwin_amd64 | darwin_x64 | ✅ 38 | // macOS | darwin_arm64 | darwin_arm64 | ✅ 39 | // Linux | linux_amd64 | linux_x64 | ✅ 40 | // Linux | linux_arm | linux_armhf | ✅ 41 | // Linux | linux_arm64 | linux_arm64 | ✅ 42 | // Windows | windows_amd64 | win32_x64 | ✅ 43 | // Windows | windows_arm64 | win32_arm64 | ✅ 44 | if (arch === 'x64') { 45 | return 'amd64'; 46 | } 47 | if (arch === 'armhf') { 48 | return 'arm'; 49 | } 50 | return arch; 51 | } 52 | 53 | interface ExtensionInfo { 54 | name: string; 55 | extensionVersion: string; 56 | languageServerVersion: string; 57 | preview: false; 58 | syntaxVersion: string; 59 | } 60 | 61 | function getExtensionInfo(): ExtensionInfo { 62 | // eslint-disable-next-line @typescript-eslint/no-var-requires 63 | const pjson = require('../package.json'); 64 | return { 65 | name: 'opentofu', 66 | extensionVersion: pjson.version, 67 | languageServerVersion: pjson.langServer.version, 68 | syntaxVersion: pjson.syntax.version, 69 | preview: pjson.preview, 70 | }; 71 | } 72 | 73 | async function downloadLanguageServer(platform: string, architecture: string, extInfo: ExtensionInfo) { 74 | const cwd = path.resolve(__dirname); 75 | 76 | const os = getPlatform(platform); 77 | const arch = getArch(architecture); 78 | 79 | const buildDir = path.basename(cwd); 80 | const repoDir = cwd.replace(buildDir, ''); 81 | const installPath = path.join(repoDir, 'bin'); 82 | const filename = os === 'windows' ? 'opentofu-ls.exe' : 'opentofu-ls'; 83 | const packageName = 84 | os === 'windows' 85 | ? `opentofu-ls_${extInfo.languageServerVersion}_${os}_${arch}.exe` 86 | : `opentofu-ls_${extInfo.languageServerVersion}_${os}_${arch}`; 87 | const filePath = path.join(installPath, filename); 88 | if (fs.existsSync(filePath)) { 89 | if (process.env.downloader_log === 'true') { 90 | console.log(`OpenTofu LS exists at ${filePath}. Exiting`); 91 | } 92 | return; 93 | } 94 | 95 | fs.mkdirSync(installPath); 96 | 97 | await fetchVersion({ 98 | repository: 'gamunu/opentofu-ls', 99 | package: packageName, 100 | destination: installPath, 101 | fileName: filename, 102 | version: extInfo.languageServerVersion, 103 | extract: false, 104 | }); 105 | } 106 | 107 | async function downloadFile(url: string, installPath: string) { 108 | if (process.env.downloader_log === 'true') { 109 | console.log(`Downloading: ${url}`); 110 | } 111 | 112 | const buffer = await fileFromUrl(url); 113 | fs.writeFileSync(installPath, buffer); 114 | if (process.env.downloader_log === 'true') { 115 | console.log(`Download completed: ${installPath}`); 116 | } 117 | } 118 | 119 | async function downloadSyntax(info: ExtensionInfo) { 120 | const release = `v${info.syntaxVersion}`; 121 | info.name = 'terraform'; 122 | 123 | const cwd = path.resolve(__dirname); 124 | const buildDir = path.basename(cwd); 125 | const repoDir = cwd.replace(buildDir, ''); 126 | const installPath = path.join(repoDir, 'syntaxes'); 127 | 128 | if (fs.existsSync(installPath)) { 129 | if (process.env.downloader_log === 'true') { 130 | console.log(`Syntax path exists at ${installPath}. Removing`); 131 | } 132 | fs.rmSync(installPath, { recursive: true }); 133 | } 134 | 135 | fs.mkdirSync(installPath); 136 | 137 | const productName = info.name.replace('-preview', ''); 138 | const terraformSyntaxFile = `${productName}.tmGrammar.json`; 139 | const hclSyntaxFile = `hcl.tmGrammar.json`; 140 | 141 | let url = `https://github.com/hashicorp/syntax/releases/download/${release}/${terraformSyntaxFile}`; 142 | await downloadFile(url, path.join(installPath, terraformSyntaxFile)); 143 | 144 | url = `https://github.com/hashicorp/syntax/releases/download/${release}/${hclSyntaxFile}`; 145 | await downloadFile(url, path.join(installPath, hclSyntaxFile)); 146 | } 147 | 148 | export async function fetchVersion(release: Release): Promise { 149 | validateRelease(release); 150 | await downloadBinary(release); 151 | } 152 | 153 | async function downloadBinary(release: Release) { 154 | const url = `https://github.com/${release.repository}/releases/download/v${release.version}/${release.package}`; 155 | 156 | const fpath = path.join(release.destination, release.fileName); 157 | 158 | try { 159 | //fs.mkdirSync(release.destination); 160 | 161 | const buffer = await fileFromUrl(url); 162 | fs.writeFileSync(fpath, buffer); 163 | 164 | if (os !== 'windows' && fpath) { 165 | fs.chmodSync(fpath, 0o777); 166 | } 167 | 168 | if (process.env.downloader_log === 'true') { 169 | console.log(`Download completed`); 170 | } 171 | } catch (error) { 172 | console.log(error); 173 | throw new Error(`Release download failed version: ${release.version}, fileName: ${release.fileName}`); 174 | } 175 | } 176 | 177 | function validateRelease(release: Release) { 178 | if (!release.repository) { 179 | throw new Error('Missing release repository'); 180 | } 181 | 182 | if (!release.package) { 183 | throw new Error('Missing release package name'); 184 | } 185 | 186 | if (!release.destination) { 187 | throw new Error('Missing release destination'); 188 | } 189 | 190 | if (!release.version) { 191 | throw new Error('Missing release version'); 192 | } 193 | } 194 | 195 | async function run(platform: string, architecture: string) { 196 | const extInfo = getExtensionInfo(); 197 | if (process.env.downloader_log === 'true') { 198 | console.log(extInfo); 199 | } 200 | 201 | await downloadSyntax(extInfo); 202 | 203 | // we don't download ls for web platforms 204 | if (os === 'web') { 205 | return; 206 | } 207 | 208 | await downloadLanguageServer(platform, architecture, extInfo); 209 | } 210 | 211 | let os = process.platform.toString(); 212 | let arch = process.arch; 213 | 214 | // ls_target=linux_amd64 npm install 215 | // or 216 | // ls_target=web_noop npm run download:artifacts 217 | const lsTarget = process.env.ls_target; 218 | if (lsTarget !== undefined) { 219 | const tgt = lsTarget.split('_'); 220 | os = tgt[0]; 221 | 222 | arch = tgt[1] as NodeJS.Architecture; 223 | } 224 | 225 | run(os, arch); 226 | -------------------------------------------------------------------------------- /build/preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | set -eEuo pipefail 6 | 7 | SCRIPT_RELATIVE_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | ROOT_RELATIVE_DIR=$(dirname "${SCRIPT_RELATIVE_DIR}") 9 | 10 | cd $ROOT_RELATIVE_DIR 11 | 12 | # Get current version info 13 | VERSION=$(cat package.json | jq -r '.version') # e.g. 2.26.0 14 | MAJOR=$(echo $VERSION | cut -d. -f1) 15 | MINOR=$(echo $VERSION | cut -d. -f2) 16 | # Build new version 17 | # 18 | # For the pre-release build, we keep the major and minor versions 19 | # and add the timestamp of the last commit as a patch. 20 | NEW_PATCH=`git log -1 --format=%cd --date="format:%Y%m%d%H"` # e.g. 2023050312 21 | VER="$MAJOR.$MINOR.$NEW_PATCH" 22 | 23 | npm version $VER --no-git-tag-version --no-commit-hooks 24 | 25 | changie batch "v$VER" 26 | changie merge 27 | -------------------------------------------------------------------------------- /docs/code_lens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/code_lens.png -------------------------------------------------------------------------------- /docs/intellisense1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/intellisense1.png -------------------------------------------------------------------------------- /docs/intellisense2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/intellisense2.png -------------------------------------------------------------------------------- /docs/intellisense3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/intellisense3.png -------------------------------------------------------------------------------- /docs/module_calls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/module_calls.png -------------------------------------------------------------------------------- /docs/module_calls_doc_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/module_calls_doc_link.png -------------------------------------------------------------------------------- /docs/module_providers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/module_providers.png -------------------------------------------------------------------------------- /docs/pin_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/pin_version.png -------------------------------------------------------------------------------- /docs/pre-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/pre-fill.png -------------------------------------------------------------------------------- /docs/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/syntax.png -------------------------------------------------------------------------------- /docs/validation-cli-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/validation-cli-command.png -------------------------------------------------------------------------------- /docs/validation-cli-diagnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/validation-cli-diagnostic.png -------------------------------------------------------------------------------- /docs/validation-rule-hcl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/validation-rule-hcl.png -------------------------------------------------------------------------------- /docs/validation-rule-invalid-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/validation-rule-invalid-ref.png -------------------------------------------------------------------------------- /docs/validation-rule-missing-attribute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/docs/validation-rule-missing-attribute.png -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | /* eslint-disable no-undef */ 7 | const esbuild = require('esbuild'); 8 | const glob = require('glob'); 9 | const path = require('path'); 10 | const polyfill = require('@esbuild-plugins/node-globals-polyfill'); 11 | 12 | const production = process.argv.includes('--production'); 13 | const watch = process.argv.includes('--watch'); 14 | 15 | /** 16 | * @type {import('esbuild').Plugin} 17 | */ 18 | const esbuildProblemMatcherPlugin = { 19 | name: 'esbuild-problem-matcher', 20 | 21 | setup(build) { 22 | build.onStart(() => { 23 | console.log('[watch] build started'); 24 | }); 25 | build.onEnd((result) => { 26 | result.errors.forEach(({ text, location }) => { 27 | console.error(`✘ [ERROR] ${text}`); 28 | console.error(` ${location.file}:${location.line}:${location.column}:`); 29 | }); 30 | console.log('[watch] build finished'); 31 | }); 32 | }, 33 | }; 34 | 35 | /** 36 | * For web extension, all tests, including the test runner, need to be bundled into 37 | * a single module that has a exported `run` function . 38 | * This plugin bundles implements a virtual file extensionTests.ts that bundles all these together. 39 | * @type {import('esbuild').Plugin} 40 | */ 41 | const testBundlePlugin = { 42 | name: 'testBundlePlugin', 43 | setup(build) { 44 | build.onResolve({ filter: /[/\\]extensionTests\.ts$/ }, (args) => { 45 | if (args.kind === 'entry-point') { 46 | return { path: path.resolve(args.path) }; 47 | } 48 | }); 49 | build.onLoad({ filter: /[/\\]extensionTests\.ts$/ }, async () => { 50 | const testsRoot = path.join(__dirname, 'src/web/test/suite'); 51 | const files = await glob.glob('*.test.{ts,tsx}', { cwd: testsRoot, posix: true }); 52 | return { 53 | contents: `export { run } from './mochaTestRunner.ts';` + files.map((f) => `import('./${f}');`).join(''), 54 | watchDirs: files.map((f) => path.dirname(path.resolve(testsRoot, f))), 55 | watchFiles: files.map((f) => path.resolve(testsRoot, f)), 56 | }; 57 | }); 58 | }, 59 | }; 60 | 61 | async function main() { 62 | const defaults = { 63 | bundle: true, 64 | format: 'cjs', 65 | minify: production, 66 | sourcemap: !production, 67 | sourcesContent: false, 68 | external: ['vscode'], 69 | logLevel: 'silent', 70 | }; 71 | 72 | const desktop = { 73 | entryPoints: ['src/extension.ts'], 74 | outfile: 'dist/extension.js', 75 | platform: 'node', 76 | plugins: [ 77 | /* add to the end of plugins array */ 78 | esbuildProblemMatcherPlugin, 79 | ], 80 | ...defaults, 81 | }; 82 | 83 | const web = { 84 | entryPoints: ['src/web/extension.ts'], 85 | outdir: 'dist/web', 86 | platform: 'browser', 87 | // Node.js global to browser globalThis 88 | define: { 89 | global: 'globalThis', 90 | }, 91 | plugins: [ 92 | polyfill.NodeGlobalsPolyfillPlugin({ 93 | process: true, 94 | buffer: true, 95 | }), 96 | testBundlePlugin, 97 | esbuildProblemMatcherPlugin /* add to the end of plugins array */, 98 | ], 99 | ...defaults, 100 | }; 101 | 102 | const contexts = [await esbuild.context(desktop), await esbuild.context(web)]; 103 | 104 | try { 105 | const promises = []; 106 | if (watch) { 107 | // Watch 108 | for (const context of contexts) { 109 | promises.push(context.watch()); 110 | } 111 | return await Promise.all(promises); 112 | } 113 | 114 | // Build once 115 | for (const context of contexts) { 116 | promises.push(context.rebuild()); 117 | } 118 | await Promise.all(promises); 119 | for (const context of contexts) { 120 | await context.dispose(); 121 | } 122 | } catch (error) { 123 | console.error(error); 124 | process.exit(1); 125 | } 126 | } 127 | 128 | main(); 129 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#", 4 | "blockComment": [ 5 | "/*", 6 | "*/" 7 | ] 8 | }, 9 | "brackets": [ 10 | [ 11 | "{", 12 | "}" 13 | ], 14 | [ 15 | "[", 16 | "]" 17 | ], 18 | [ 19 | "(", 20 | ")" 21 | ] 22 | ], 23 | "autoClosingPairs": [ 24 | { 25 | "open": "{", 26 | "close": "}" 27 | }, 28 | { 29 | "open": "[", 30 | "close": "]" 31 | }, 32 | { 33 | "open": "(", 34 | "close": ")" 35 | }, 36 | { 37 | "open": "\"", 38 | "close": "\"", 39 | "notIn": [ 40 | "string" 41 | ] 42 | }, 43 | { 44 | "open": "'", 45 | "close": "'", 46 | "notIn": [ 47 | "string", 48 | "comment" 49 | ] 50 | } 51 | ], 52 | "autoCloseBefore": ";:.,=}])> \n\t\"", 53 | "surroundingPairs": [ 54 | [ 55 | "{", 56 | "}" 57 | ], 58 | [ 59 | "[", 60 | "]" 61 | ], 62 | [ 63 | "(", 64 | ")" 65 | ], 66 | [ 67 | "\"", 68 | "\"" 69 | ], 70 | [ 71 | "'", 72 | "'" 73 | ] 74 | ], 75 | "folding": { 76 | "markers": { 77 | "start": "^\\s*#region", 78 | "end": "^\\s*#endregion" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /snippets/opentofu.json: -------------------------------------------------------------------------------- 1 | { 2 | "For Each": { 3 | "prefix": "fore", 4 | "body": [ 5 | "for_each = {", 6 | "\t${1:key} = \"${2:value}\"", 7 | "}" 8 | ], 9 | "description": "The for_each meta-argument accepts a map or a set of strings, and creates an instance for each item in that map or set." 10 | }, 11 | "Module": { 12 | "prefix": "module", 13 | "body": [ 14 | "module \"${1:name}\" {", 15 | "\tsource = \"$2\"", 16 | "\t$3", 17 | "}" 18 | ] 19 | }, 20 | "Output": { 21 | "prefix": "output", 22 | "body": [ 23 | "output \"${1:name}\" {", 24 | "\tvalue = \"$2\"", 25 | "}" 26 | ] 27 | }, 28 | "Provisioner": { 29 | "prefix": "provisioner", 30 | "body": [ 31 | "provisioner \"${1:name}\" {", 32 | "${2}", 33 | "}" 34 | ], 35 | "description": "Provisioners can be used to model specific actions on the local machine or on a remote machine in order to prepare servers or other infrastructure objects for service." 36 | }, 37 | "Empty variable": { 38 | "prefix": "vare", 39 | "body": [ 40 | "variable \"${1:name}\" {", 41 | "\ttype = ${2|string,number,bool|}", 42 | "\t${3:description = \"${4:(optional) describe your variable}\"}", 43 | "}" 44 | ], 45 | "description": "Variable (empty)" 46 | }, 47 | "Map variable": { 48 | "prefix": "varm", 49 | "body": [ 50 | "variable \"${1:name}\" {", 51 | "\ttype = map(${2|string,number,bool|})", 52 | "\t${3:description = \"${4:(optional) describe your variable}\"}", 53 | "\tdefault = {", 54 | "\t\t${5:key1} = \"${6:val1}\"", 55 | "\t\t${7:key2} = \"${8:val2}\"", 56 | "\t}", 57 | "}" 58 | ], 59 | "description": "Variable (map)" 60 | } 61 | } -------------------------------------------------------------------------------- /src/commands/generateBugReport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import child_process = require('child_process'); 7 | import * as os from 'os'; 8 | import * as vscode from 'vscode'; 9 | 10 | interface TerraformInfo { 11 | version: string; 12 | platform: string; 13 | outdated: boolean; 14 | } 15 | 16 | interface VSCodeExtension { 17 | name: string; 18 | publisher: string; 19 | version: string; 20 | } 21 | 22 | export class GenerateBugReportCommand implements vscode.Disposable { 23 | constructor(private ctx: vscode.ExtensionContext) { 24 | this.ctx.subscriptions.push( 25 | vscode.commands.registerCommand('opentofu.generateBugReport', async () => { 26 | const problemText = await vscode.window.showInputBox({ 27 | title: 'Generate a Bug Report', 28 | prompt: 'Enter a short description of the problem or hit enter to submit now', 29 | placeHolder: "For example: I'm having trouble getting autocomplete to work when I...", 30 | }); 31 | 32 | const extensions = this.getExtensions(); 33 | const body = await this.generateBody(extensions, problemText); 34 | const encodedBody = encodeURIComponent(body); 35 | const fullUrl = `https://github.com/gamunu/vscode-opentofu/issues/new?body=${encodedBody}`; 36 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(fullUrl)); 37 | }), 38 | ); 39 | } 40 | 41 | dispose(): void { 42 | // throw new Error('Method not implemented.'); 43 | } 44 | 45 | async generateBody(extensions: VSCodeExtension[], problemText?: string): Promise { 46 | if (!problemText) { 47 | problemText = `Steps To Reproduce 48 | ===== 49 | 50 | Steps to reproduce the behavior: 51 | 52 | 1. Go to '...' 53 | 2. Type '...' 54 | 3. See error 55 | 56 | Include any relevant Terraform configuration or project structure: 57 | 58 | \`\`\`terraform 59 | resource "github_repository" "test" { 60 | name = "vscode-terraform" 61 | } 62 | \`\`\` 63 | 64 | 65 | You can use 'tree' to output ASCII-based hierarchy of your project. 66 | 67 | If applicable, add screenshots to help explain your problem. 68 | 69 | Expected Behavior 70 | ----- 71 | 72 | 73 | 74 | Actual Behavior 75 | ----- 76 | 77 | 78 | 79 | Additional context 80 | ----- 81 | 82 | 87 | `; 88 | } 89 | const body = `Issue Description 90 | ===== 91 | 92 | ${problemText} 93 | 94 | Environment Information 95 | ===== 96 | 97 | Terraform Information 98 | ----- 99 | 100 | ${this.generateRuntimeMarkdown(await this.getRuntimeInfo())} 101 | 102 | Visual Studio Code 103 | ----- 104 | 105 | | Name | Version | 106 | | --- | --- | 107 | | Operating System | ${os.type()} ${os.arch()} ${os.release()} | 108 | | VSCode | ${vscode.version}| 109 | 110 | Visual Studio Code Extensions 111 | ----- 112 | 113 |
Visual Studio Code Extensions(Click to Expand) 114 | 115 | ${this.generateExtesnionMarkdown(extensions)} 116 |
117 | 118 | Extension Logs 119 | ----- 120 | 121 | > Find this from the first few lines of the relevant Output pane: 122 | View -> Output -> 'HashiCorp Terraform' 123 | 124 | `; 125 | return body; 126 | } 127 | 128 | generateExtesnionMarkdown(extensions: VSCodeExtension[]): string { 129 | if (!extensions.length) { 130 | return 'none'; 131 | } 132 | 133 | const tableHeader = `|Extension|Author|Version|\n|---|---|---|`; 134 | const table = extensions 135 | .map((e) => { 136 | return `|${e.name}|${e.publisher}|${e.version}|`; 137 | }) 138 | .join('\n'); 139 | 140 | const extensionTable = `${tableHeader}\n${table}`; 141 | return extensionTable; 142 | } 143 | 144 | generateRuntimeMarkdown(info: TerraformInfo): string { 145 | const rows = ` 146 | Version:\t${info.version} 147 | Platform:\t${info.platform} 148 | Outdated:\t${info.outdated} 149 | `; 150 | return rows; 151 | } 152 | 153 | getExtensions(): VSCodeExtension[] { 154 | const extensions = vscode.extensions.all 155 | .filter((element) => element.packageJSON.isBuiltin === false) 156 | .sort((leftside, rightside): number => { 157 | if (leftside.packageJSON.name.toLowerCase() < rightside.packageJSON.name.toLowerCase()) { 158 | return -1; 159 | } 160 | if (leftside.packageJSON.name.toLowerCase() > rightside.packageJSON.name.toLowerCase()) { 161 | return 1; 162 | } 163 | return 0; 164 | }) 165 | .map((ext) => { 166 | return { 167 | name: ext.packageJSON.name, 168 | publisher: ext.packageJSON.publisher, 169 | version: ext.packageJSON.version, 170 | }; 171 | }); 172 | return extensions; 173 | } 174 | 175 | async getRuntimeInfo(): Promise { 176 | const terraformExe = 'tofu'; 177 | const spawn = child_process.spawnSync; 178 | 179 | // try to get version from a newer terraform binary 180 | const resultJson = spawn(terraformExe, ['version', '-json']); 181 | if (resultJson.error === undefined) { 182 | try { 183 | const response = resultJson.stdout.toString(); 184 | const j = JSON.parse(response); 185 | 186 | return { 187 | version: j.terraform_version, 188 | platform: j.platform, 189 | outdated: j.terraform_outdated, 190 | }; 191 | } catch { 192 | // fall through 193 | } 194 | } 195 | 196 | // try an older binary without the json flag 197 | const result = spawn(terraformExe, ['version']); 198 | if (result.error === undefined) { 199 | try { 200 | const response = result.stdout.toString() || result.stderr.toString(); 201 | const regex = new RegExp('v?(?[0-9]+(?:.[0-9]+)*(?:-[A-Za-z0-9.]+)?)'); 202 | const matches = regex.exec(response); 203 | 204 | const version = matches && matches.length > 1 ? matches[1] : 'Not found'; 205 | const platform = response.split('\n')[1].replace('on ', ''); 206 | 207 | return { 208 | version: version, 209 | platform: platform, 210 | outdated: true, 211 | }; 212 | } catch { 213 | // fall through 214 | } 215 | } 216 | 217 | return { 218 | version: 'Not found', 219 | platform: 'Not found', 220 | outdated: false, 221 | }; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/commands/terraform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { LanguageClient } from 'vscode-languageclient/node'; 8 | import * as terraform from '../api/terraform/terraform'; 9 | 10 | export class TerraformCommands implements vscode.Disposable { 11 | private commands: vscode.Disposable[]; 12 | 13 | constructor(private client: LanguageClient) { 14 | this.commands = [ 15 | vscode.commands.registerCommand('opentofu.init', async () => { 16 | await terraform.initAskUserCommand(this.client); 17 | }), 18 | vscode.commands.registerCommand('opentofu.initCurrent', async () => { 19 | await terraform.initCurrentOpenFileCommand(this.client); 20 | }), 21 | vscode.commands.registerCommand('opentofu.apply', async () => { 22 | await terraform.command('apply', this.client, true); 23 | }), 24 | vscode.commands.registerCommand('opentofu.plan', async () => { 25 | await terraform.command('plan', this.client, true); 26 | }), 27 | vscode.commands.registerCommand('opentofu.validate', async () => { 28 | await terraform.command('validate', this.client); 29 | }), 30 | ]; 31 | } 32 | 33 | dispose() { 34 | this.commands.forEach((c) => c.dispose()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/terraformls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { config, getScope } from '../utils/vscode'; 8 | 9 | export class TerraformLSCommands implements vscode.Disposable { 10 | private commands: vscode.Disposable[]; 11 | 12 | constructor() { 13 | this.commands = [ 14 | vscode.workspace.onDidChangeConfiguration(async (event: vscode.ConfigurationChangeEvent) => { 15 | if (event.affectsConfiguration('opentofu') || event.affectsConfiguration('terraform-ls')) { 16 | const reloadMsg = 'Reload VSCode window to apply language server changes'; 17 | const selected = await vscode.window.showInformationMessage(reloadMsg, 'Reload'); 18 | if (selected === 'Reload') { 19 | vscode.commands.executeCommand('workbench.action.reloadWindow'); 20 | } 21 | } 22 | }), 23 | vscode.commands.registerCommand('opentofu.enableLanguageServer', async () => { 24 | if (config('opentofu').get('languageServer.enable') === true) { 25 | return; 26 | } 27 | 28 | const scope: vscode.ConfigurationTarget = getScope('opentofu', 'languageServer.enable'); 29 | 30 | await config('opentofu').update('languageServer.enable', true, scope); 31 | }), 32 | vscode.commands.registerCommand('opentofu.disableLanguageServer', async () => { 33 | if (config('opentofu').get('languageServer.enable') === false) { 34 | return; 35 | } 36 | 37 | const scope: vscode.ConfigurationTarget = getScope('opentofu', 'languageServer.enable'); 38 | 39 | await config('opentofu').update('languageServer.enable', false, scope); 40 | }), 41 | vscode.commands.registerCommand('terraform.openSettingsJson', async () => { 42 | // this opens the default settings window (either UI or json) 43 | const s = await vscode.workspace.getConfiguration('workbench').get('settings.editor'); 44 | if (s === 'json') { 45 | return await vscode.commands.executeCommand('workbench.action.openSettingsJson', { 46 | revealSetting: { key: 'terraform.languageServer.enable', edit: true }, 47 | }); 48 | } else { 49 | return await vscode.commands.executeCommand('workbench.action.openSettings', { 50 | focusSearch: true, 51 | query: '@ext:hashicorp.terraform', 52 | }); 53 | } 54 | }), 55 | ]; 56 | } 57 | 58 | dispose() { 59 | this.commands.forEach((c) => c.dispose()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/features/languageStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { BaseLanguageClient, ClientCapabilities, FeatureState, StaticFeature } from 'vscode-languageclient'; 8 | 9 | import { ExperimentalClientCapabilities } from './types'; 10 | import * as lsStatus from '../status/language'; 11 | 12 | export class LanguageStatusFeature implements StaticFeature { 13 | private disposables: vscode.Disposable[] = []; 14 | 15 | constructor( 16 | private client: BaseLanguageClient, 17 | private outputChannel: vscode.OutputChannel, 18 | ) {} 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-empty-function 21 | clear(): void {} 22 | 23 | getState(): FeatureState { 24 | return { 25 | kind: 'static', 26 | }; 27 | } 28 | 29 | public fillClientCapabilities(capabilities: ClientCapabilities & ExperimentalClientCapabilities): void { 30 | if (!capabilities['experimental']) { 31 | capabilities['experimental'] = {}; 32 | } 33 | } 34 | 35 | public initialize(): void { 36 | this.outputChannel.appendLine('Started client'); 37 | 38 | const initializeResult = this.client.initializeResult; 39 | if (initializeResult === undefined) { 40 | return; 41 | } 42 | 43 | lsStatus.setVersion(initializeResult.serverInfo?.version ?? ''); 44 | } 45 | 46 | public dispose(): void { 47 | this.disposables.forEach((d: vscode.Disposable) => d.dispose()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/features/moduleCalls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { 8 | BaseLanguageClient, 9 | ClientCapabilities, 10 | FeatureState, 11 | ServerCapabilities, 12 | StaticFeature, 13 | } from 'vscode-languageclient'; 14 | import { ModuleCallsDataProvider } from '../providers/terraform/moduleCalls'; 15 | import { ExperimentalClientCapabilities } from './types'; 16 | 17 | const CLIENT_MODULE_CALLS_CMD_ID = 'client.refreshModuleCalls'; 18 | 19 | export class ModuleCallsFeature implements StaticFeature { 20 | private disposables: vscode.Disposable[] = []; 21 | 22 | constructor( 23 | private client: BaseLanguageClient, 24 | private view: ModuleCallsDataProvider, 25 | ) {} 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-empty-function 28 | clear(): void {} 29 | 30 | getState(): FeatureState { 31 | return { 32 | kind: 'static', 33 | }; 34 | } 35 | 36 | public fillClientCapabilities(capabilities: ClientCapabilities & ExperimentalClientCapabilities): void { 37 | if (!capabilities['experimental']) { 38 | capabilities['experimental'] = {}; 39 | } 40 | capabilities['experimental']['refreshModuleCallsCommandId'] = CLIENT_MODULE_CALLS_CMD_ID; 41 | } 42 | 43 | public async initialize(capabilities: ServerCapabilities): Promise { 44 | this.disposables.push(vscode.window.registerTreeDataProvider('opentofu.modules', this.view)); 45 | 46 | if (!capabilities.experimental?.refreshModuleCalls) { 47 | console.log('Server does not support client.refreshModuleCalls'); 48 | await vscode.commands.executeCommand('setContext', 'terraform.modules.supported', false); 49 | return; 50 | } 51 | 52 | await vscode.commands.executeCommand('setContext', 'terraform.modules.supported', true); 53 | 54 | const d = this.client.onRequest(CLIENT_MODULE_CALLS_CMD_ID, () => { 55 | this.view?.refresh(); 56 | }); 57 | this.disposables.push(d); 58 | } 59 | 60 | public dispose(): void { 61 | this.disposables.forEach((d: vscode.Disposable) => d.dispose()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/features/moduleProviders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { 8 | BaseLanguageClient, 9 | ClientCapabilities, 10 | FeatureState, 11 | ServerCapabilities, 12 | StaticFeature, 13 | } from 'vscode-languageclient'; 14 | import { ModuleProvidersDataProvider } from '../providers/terraform/moduleProviders'; 15 | import { ExperimentalClientCapabilities } from './types'; 16 | 17 | export const CLIENT_MODULE_PROVIDERS_CMD_ID = 'client.refreshModuleProviders'; 18 | 19 | export class ModuleProvidersFeature implements StaticFeature { 20 | private disposables: vscode.Disposable[] = []; 21 | 22 | constructor( 23 | private client: BaseLanguageClient, 24 | private view: ModuleProvidersDataProvider, 25 | ) {} 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-empty-function 28 | clear(): void {} 29 | 30 | getState(): FeatureState { 31 | return { 32 | kind: 'static', 33 | }; 34 | } 35 | 36 | public fillClientCapabilities(capabilities: ClientCapabilities & ExperimentalClientCapabilities): void { 37 | if (!capabilities['experimental']) { 38 | capabilities['experimental'] = {}; 39 | } 40 | capabilities['experimental']['refreshModuleProvidersCommandId'] = CLIENT_MODULE_PROVIDERS_CMD_ID; 41 | } 42 | 43 | public async initialize(capabilities: ServerCapabilities): Promise { 44 | this.disposables.push(vscode.window.registerTreeDataProvider('opentofu.providers', this.view)); 45 | 46 | if (!capabilities.experimental?.refreshModuleProviders) { 47 | console.log("Server doesn't support client.refreshModuleProviders"); 48 | await vscode.commands.executeCommand('setContext', 'terraform.providers.supported', false); 49 | return; 50 | } 51 | 52 | await vscode.commands.executeCommand('setContext', 'terraform.providers.supported', true); 53 | 54 | const d = this.client.onRequest(CLIENT_MODULE_PROVIDERS_CMD_ID, () => { 55 | this.view?.refresh(); 56 | }); 57 | this.disposables.push(d); 58 | } 59 | 60 | public dispose(): void { 61 | this.disposables.forEach((d: vscode.Disposable) => d.dispose()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/features/semanticTokens.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { 7 | BaseLanguageClient, 8 | ClientCapabilities, 9 | FeatureState, 10 | ServerCapabilities, 11 | StaticFeature, 12 | } from 'vscode-languageclient'; 13 | 14 | export interface PartialManifest { 15 | contributes: { 16 | semanticTokenTypes?: ObjectWithId[]; 17 | semanticTokenModifiers?: ObjectWithId[]; 18 | }; 19 | } 20 | 21 | interface ObjectWithId { 22 | id: string; 23 | } 24 | 25 | export class CustomSemanticTokens implements StaticFeature { 26 | constructor( 27 | private _client: BaseLanguageClient, 28 | private manifest: PartialManifest, 29 | ) {} 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-empty-function 32 | clear(): void {} 33 | 34 | getState(): FeatureState { 35 | return { 36 | kind: 'static', 37 | }; 38 | } 39 | 40 | public fillClientCapabilities(capabilities: ClientCapabilities): void { 41 | if (!capabilities.textDocument || !capabilities.textDocument.semanticTokens) { 42 | return; 43 | } 44 | 45 | const extSemanticTokenTypes = this.tokenTypesFromExtManifest(this.manifest); 46 | const extSemanticTokenModifiers = this.tokenModifiersFromExtManifest(this.manifest); 47 | 48 | const tokenTypes = capabilities.textDocument.semanticTokens.tokenTypes; 49 | capabilities.textDocument.semanticTokens.tokenTypes = tokenTypes.concat(extSemanticTokenTypes); 50 | 51 | const tokenModifiers = capabilities.textDocument.semanticTokens.tokenModifiers; 52 | capabilities.textDocument.semanticTokens.tokenModifiers = tokenModifiers.concat(extSemanticTokenModifiers); 53 | } 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 56 | public initialize(capabilities: ServerCapabilities): void { 57 | return; 58 | } 59 | 60 | public dispose(): void { 61 | return; 62 | } 63 | 64 | tokenTypesFromExtManifest(manifest: PartialManifest): string[] { 65 | if (!manifest.contributes.semanticTokenTypes) { 66 | return []; 67 | } 68 | return manifest.contributes.semanticTokenTypes.map((token: ObjectWithId) => token.id); 69 | } 70 | 71 | tokenModifiersFromExtManifest(manifest: PartialManifest): string[] { 72 | if (!manifest.contributes.semanticTokenModifiers) { 73 | return []; 74 | } 75 | 76 | return manifest.contributes.semanticTokenModifiers.map((modifier: ObjectWithId) => modifier.id); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/features/showReferences.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { 8 | BaseLanguageClient, 9 | ClientCapabilities, 10 | FeatureState, 11 | ReferenceContext, 12 | ReferencesRequest, 13 | ServerCapabilities, 14 | StaticFeature, 15 | } from 'vscode-languageclient'; 16 | import { config } from '../utils/vscode'; 17 | 18 | import { ExperimentalClientCapabilities } from './types'; 19 | 20 | type Position = { 21 | line: number; 22 | character: number; 23 | }; 24 | 25 | type RefContext = { 26 | includeDeclaration: boolean; 27 | }; 28 | 29 | const CLIENT_CMD_ID = 'client.showReferences'; 30 | const VSCODE_SHOW_REFERENCES = 'editor.action.showReferences'; 31 | 32 | export class ShowReferencesFeature implements StaticFeature { 33 | private registeredCommands: vscode.Disposable[] = []; 34 | private isEnabled = config('opentofu').get('codelens.referenceCount', false); 35 | 36 | constructor(private _client: BaseLanguageClient) {} 37 | 38 | // eslint-disable-next-line @typescript-eslint/no-empty-function 39 | clear(): void {} 40 | 41 | getState(): FeatureState { 42 | return { 43 | kind: 'static', 44 | }; 45 | } 46 | 47 | public fillClientCapabilities(capabilities: ClientCapabilities & ExperimentalClientCapabilities): void { 48 | if (this.isEnabled === false) { 49 | return; 50 | } 51 | if (!capabilities['experimental']) { 52 | capabilities['experimental'] = {}; 53 | } 54 | capabilities['experimental']['showReferencesCommandId'] = CLIENT_CMD_ID; 55 | } 56 | 57 | public initialize(capabilities: ServerCapabilities): void { 58 | if (!capabilities.experimental?.referenceCountCodeLens) { 59 | return; 60 | } 61 | 62 | if (this.isEnabled === false) { 63 | return; 64 | } 65 | 66 | const showRefs = vscode.commands.registerCommand(CLIENT_CMD_ID, async (pos: Position, refCtx: RefContext) => { 67 | const client = this._client; 68 | const doc = vscode.window?.activeTextEditor?.document; 69 | if (!doc) { 70 | return; 71 | } 72 | 73 | const position = new vscode.Position(pos.line, pos.character); 74 | const context: ReferenceContext = { includeDeclaration: refCtx.includeDeclaration }; 75 | 76 | const provider = client.getFeature(ReferencesRequest.method).getProvider(doc); 77 | if (!provider) { 78 | return; 79 | } 80 | 81 | const tokenSource = new vscode.CancellationTokenSource(); 82 | const locations = await provider.provideReferences(doc, position, context, tokenSource.token); 83 | 84 | await vscode.commands.executeCommand(VSCODE_SHOW_REFERENCES, doc.uri, position, locations); 85 | }); 86 | 87 | this.registeredCommands.push(showRefs); 88 | } 89 | 90 | public dispose(): void { 91 | this.registeredCommands.forEach(function (cmd, index, commands) { 92 | cmd.dispose(); 93 | commands.splice(index, 1); 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/features/terraformVersion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as terraform from '../api/terraform/terraform'; 8 | import { ClientCapabilities, FeatureState, ServerCapabilities, StaticFeature } from 'vscode-languageclient'; 9 | import { getActiveTextEditor } from '../utils/vscode'; 10 | import { ExperimentalClientCapabilities } from './types'; 11 | import { Utils } from 'vscode-uri'; 12 | import { LanguageClient } from 'vscode-languageclient/node'; 13 | import * as lsStatus from '../status/language'; 14 | import * as versionStatus from '../status/installedVersion'; 15 | import * as requiredVersionStatus from '../status/requiredVersion'; 16 | 17 | export class TerraformVersionFeature implements StaticFeature { 18 | private disposables: vscode.Disposable[] = []; 19 | 20 | private clientTerraformVersionCommandId = 'client.refreshTerraformVersion'; 21 | 22 | constructor( 23 | private client: LanguageClient, 24 | private outputChannel: vscode.OutputChannel, 25 | ) {} 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-empty-function 28 | clear(): void {} 29 | 30 | getState(): FeatureState { 31 | return { 32 | kind: 'static', 33 | }; 34 | } 35 | 36 | public fillClientCapabilities(capabilities: ClientCapabilities & ExperimentalClientCapabilities): void { 37 | capabilities.experimental = capabilities.experimental || {}; 38 | capabilities.experimental.refreshTerraformVersionCommandId = this.clientTerraformVersionCommandId; 39 | } 40 | 41 | public async initialize(capabilities: ServerCapabilities): Promise { 42 | if (!capabilities.experimental?.refreshTerraformVersion) { 43 | this.outputChannel.appendLine("Server doesn't support client.refreshTerraformVersion"); 44 | return; 45 | } 46 | 47 | const handler = this.client.onRequest(this.clientTerraformVersionCommandId, async () => { 48 | const editor = getActiveTextEditor(); 49 | if (editor === undefined) { 50 | return; 51 | } 52 | 53 | const moduleDir = Utils.dirname(editor.document.uri); 54 | 55 | try { 56 | versionStatus.setWaiting(); 57 | requiredVersionStatus.setWaiting(); 58 | 59 | lsStatus.setLanguageServerBusy(); 60 | 61 | const response = await terraform.terraformVersion(moduleDir.toString(), this.client); 62 | versionStatus.setVersion(response.discovered_version || 'unknown'); 63 | requiredVersionStatus.setVersion(response.required_version || 'any'); 64 | 65 | lsStatus.setLanguageServerRunning(); 66 | versionStatus.setReady(); 67 | requiredVersionStatus.setReady(); 68 | } catch (error) { 69 | let message = 'Unknown Error'; 70 | if (error instanceof Error) { 71 | message = error.message; 72 | } else if (typeof error === 'string') { 73 | message = error; 74 | } 75 | 76 | /* 77 | We do not want to pop an error window because the user cannot do anything 78 | at this point. An error here likely means we cannot communicate with the LS, 79 | which means it's already shut down. 80 | Instead we log to the outputchannel so when the user copies the log we can 81 | see this errored here. 82 | */ 83 | this.outputChannel.appendLine(message); 84 | 85 | lsStatus.setLanguageServerRunning(); 86 | versionStatus.setReady(); 87 | requiredVersionStatus.setReady(); 88 | } 89 | }); 90 | 91 | this.disposables.push(handler); 92 | } 93 | 94 | public dispose(): void { 95 | this.disposables.forEach((d: vscode.Disposable, index, things) => { 96 | d.dispose(); 97 | things.splice(index, 1); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/features/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | /** 7 | * Defines our experimental capabilities provided by the client. 8 | */ 9 | export interface ExperimentalClientCapabilities { 10 | experimental: { 11 | telemetryVersion?: number; 12 | showReferencesCommandId?: string; 13 | refreshModuleProvidersCommandId?: string; 14 | refreshModuleCallsCommandId?: string; 15 | refreshTerraformVersionCommandId?: string; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/terraform/moduleCalls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as path from 'path'; 7 | import * as terraform from '../../api/terraform/terraform'; 8 | import * as vscode from 'vscode'; 9 | import { LanguageClient } from 'vscode-languageclient/node'; 10 | import { Utils } from 'vscode-uri'; 11 | import { getActiveTextEditor, isTerraformFile } from '../../utils/vscode'; 12 | 13 | class ModuleCallItem extends vscode.TreeItem { 14 | constructor( 15 | public label: string, 16 | public sourceAddr: string, 17 | public version: string | undefined, 18 | public sourceType: string | undefined, 19 | public docsLink: string | undefined, 20 | public terraformIcon: string, 21 | public readonly children: ModuleCallItem[], 22 | ) { 23 | super( 24 | label, 25 | children.length >= 1 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, 26 | ); 27 | 28 | this.description = this.version ? `${this.version}` : ''; 29 | 30 | if (this.version === undefined) { 31 | this.tooltip = `${this.sourceAddr}`; 32 | } else { 33 | this.tooltip = `${this.sourceAddr}@${this.version}`; 34 | } 35 | 36 | this.iconPath = this.getIcon(this.sourceType); 37 | } 38 | 39 | // iconPath = this.getIcon(this.sourceType); 40 | 41 | getIcon(type: string | undefined) { 42 | switch (type) { 43 | case 'tfregistry': 44 | return { 45 | light: this.terraformIcon, 46 | dark: this.terraformIcon, 47 | }; 48 | case 'local': 49 | return new vscode.ThemeIcon('symbol-folder'); 50 | case 'github': 51 | return new vscode.ThemeIcon('github'); 52 | case 'git': 53 | return new vscode.ThemeIcon('git-branch'); 54 | default: 55 | return new vscode.ThemeIcon('extensions-view-icon'); 56 | } 57 | } 58 | } 59 | 60 | export class ModuleCallsDataProvider implements vscode.TreeDataProvider { 61 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 62 | ModuleCallItem | undefined | null | void 63 | >(); 64 | readonly onDidChangeTreeData: vscode.Event = 65 | this._onDidChangeTreeData.event; 66 | 67 | private svg = ''; 68 | 69 | constructor( 70 | ctx: vscode.ExtensionContext, 71 | public client: LanguageClient, 72 | ) { 73 | this.svg = ctx.asAbsolutePath(path.join('assets', 'icons', 'opentofu.svg')); 74 | 75 | ctx.subscriptions.push( 76 | vscode.commands.registerCommand('opentofu.modules.refreshList', () => this.refresh()), 77 | vscode.commands.registerCommand('opentofu.modules.openDocumentation', (module: ModuleCallItem) => { 78 | if (module.docsLink) { 79 | vscode.env.openExternal(vscode.Uri.parse(module.docsLink)); 80 | } 81 | }), 82 | vscode.window.onDidChangeActiveTextEditor(async () => { 83 | // most of the time this is called when the user switches tabs or closes the file 84 | // we already check for state inside the getModule function, so we can just call refresh here 85 | this.refresh(); 86 | }), 87 | ); 88 | } 89 | 90 | refresh(): void { 91 | this._onDidChangeTreeData.fire(); 92 | } 93 | 94 | getTreeItem(element: ModuleCallItem): ModuleCallItem | Thenable { 95 | return element; 96 | } 97 | 98 | getChildren(element?: ModuleCallItem): vscode.ProviderResult { 99 | if (element) { 100 | return Promise.resolve(element.children); 101 | } else { 102 | const m = this.getModules(); 103 | return Promise.resolve(m); 104 | } 105 | } 106 | 107 | async getModules(): Promise { 108 | const activeEditor = getActiveTextEditor(); 109 | 110 | await vscode.commands.executeCommand('setContext', 'terraform.modules.documentOpened', true); 111 | await vscode.commands.executeCommand('setContext', 'terraform.modules.documentIsTerraform', true); 112 | await vscode.commands.executeCommand('setContext', 'terraform.modules.lspConnected', true); 113 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noResponse', false); 114 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noModules', false); 115 | 116 | if (activeEditor?.document === undefined) { 117 | // there is no open document 118 | await vscode.commands.executeCommand('setContext', 'terraform.modules.documentOpened', false); 119 | return []; 120 | } 121 | 122 | if (!isTerraformFile(activeEditor.document)) { 123 | // the open file is not a terraform file 124 | await vscode.commands.executeCommand('setContext', 'terraform.modules.documentIsTerraform', false); 125 | return []; 126 | } 127 | 128 | if (this.client === undefined) { 129 | // connection to terraform-ls failed 130 | await vscode.commands.executeCommand('setContext', 'terraform.modules.lspConnected', false); 131 | return []; 132 | } 133 | 134 | const editor = activeEditor.document.uri; 135 | const documentURI = Utils.dirname(editor); 136 | 137 | let response: terraform.ModuleCallsResponse; 138 | try { 139 | response = await terraform.moduleCalls(documentURI.toString(), this.client); 140 | if (response === null) { 141 | // no response from terraform-ls 142 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noResponse', true); 143 | return []; 144 | } 145 | } catch { 146 | // error from terraform-ls 147 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noResponse', true); 148 | return []; 149 | } 150 | 151 | try { 152 | const list = response.module_calls.map((m) => { 153 | return this.toModuleCall( 154 | m.name, 155 | m.source_addr, 156 | m.version, 157 | m.source_type, 158 | m.docs_link, 159 | this.svg, 160 | m.dependent_modules, 161 | ); 162 | }); 163 | 164 | if (list.length === 0) { 165 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noModules', true); 166 | } 167 | 168 | return list; 169 | } catch { 170 | // error mapping response 171 | await vscode.commands.executeCommand('setContext', 'terraform.modules.noResponse', true); 172 | return []; 173 | } 174 | } 175 | 176 | toModuleCall( 177 | name: string, 178 | sourceAddr: string, 179 | version: string | undefined, 180 | sourceType: string | undefined, 181 | docsLink: string | undefined, 182 | terraformIcon: string, 183 | dependents: terraform.ModuleCall[], 184 | ): ModuleCallItem { 185 | let deps: ModuleCallItem[] = []; 186 | if (dependents.length !== 0) { 187 | deps = dependents.map((dp) => 188 | this.toModuleCall( 189 | dp.name, 190 | dp.source_addr, 191 | dp.version, 192 | dp.source_type, 193 | dp.docs_link, 194 | terraformIcon, 195 | dp.dependent_modules, 196 | ), 197 | ); 198 | } 199 | 200 | return new ModuleCallItem(name, sourceAddr, version, sourceType, docsLink, terraformIcon, deps); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/providers/terraform/moduleProviders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as terraform from '../../api/terraform/terraform'; 7 | import * as vscode from 'vscode'; 8 | import { Utils } from 'vscode-uri'; 9 | import { getActiveTextEditor, isTerraformFile } from '../../utils/vscode'; 10 | import { LanguageClient } from 'vscode-languageclient/node'; 11 | 12 | class ModuleProviderItem extends vscode.TreeItem { 13 | constructor( 14 | public fullName: string, 15 | public displayName: string, 16 | public requiredVersion: string | undefined, 17 | public installedVersion: string | undefined, 18 | public docsLink: string | undefined, 19 | ) { 20 | super(displayName, vscode.TreeItemCollapsibleState.None); 21 | 22 | this.description = installedVersion ?? ''; 23 | this.iconPath = new vscode.ThemeIcon('package'); 24 | this.tooltip = `${fullName} ${requiredVersion ?? ''}`; 25 | 26 | if (docsLink) { 27 | this.contextValue = 'moduleProviderHasDocs'; 28 | } 29 | } 30 | } 31 | 32 | export class ModuleProvidersDataProvider implements vscode.TreeDataProvider { 33 | private readonly didChangeTreeData = new vscode.EventEmitter(); 34 | public readonly onDidChangeTreeData = this.didChangeTreeData.event; 35 | 36 | constructor( 37 | ctx: vscode.ExtensionContext, 38 | private client: LanguageClient, 39 | ) { 40 | ctx.subscriptions.push( 41 | vscode.commands.registerCommand('terraform.providers.refreshList', () => this.refresh()), 42 | vscode.window.onDidChangeActiveTextEditor(async () => { 43 | // most of the time this is called when the user switches tabs or closes the file 44 | // we already check for state inside the getprovider function, so we can just call refresh here 45 | this.refresh(); 46 | }), 47 | vscode.commands.registerCommand('opentofu.providers.openDocumentation', (module: ModuleProviderItem) => { 48 | if (module.docsLink) { 49 | vscode.env.openExternal(vscode.Uri.parse(module.docsLink)); 50 | } 51 | }), 52 | ); 53 | } 54 | 55 | refresh(): void { 56 | this.didChangeTreeData.fire(); 57 | } 58 | 59 | getTreeItem(element: ModuleProviderItem): vscode.TreeItem | Thenable { 60 | return element; 61 | } 62 | 63 | getChildren(element?: ModuleProviderItem): vscode.ProviderResult { 64 | if (element) { 65 | return []; 66 | } else { 67 | return this.getProvider(); 68 | } 69 | } 70 | 71 | async getProvider(): Promise { 72 | const activeEditor = getActiveTextEditor(); 73 | 74 | await vscode.commands.executeCommand('setContext', 'terraform.providers.documentOpened', true); 75 | await vscode.commands.executeCommand('setContext', 'terraform.providers.documentIsTerraform', true); 76 | await vscode.commands.executeCommand('setContext', 'terraform.providers.lspConnected', true); 77 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noResponse', false); 78 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noProviders', false); 79 | 80 | if (activeEditor?.document === undefined) { 81 | // there is no open document 82 | await vscode.commands.executeCommand('setContext', 'terraform.providers.documentOpened', false); 83 | return []; 84 | } 85 | 86 | if (!isTerraformFile(activeEditor.document)) { 87 | // the open file is not a terraform file 88 | await vscode.commands.executeCommand('setContext', 'terraform.providers.documentIsTerraform', false); 89 | return []; 90 | } 91 | 92 | if (this.client === undefined) { 93 | // connection to terraform-ls failed 94 | await vscode.commands.executeCommand('setContext', 'terraform.providers.lspConnected', false); 95 | return []; 96 | } 97 | 98 | const editor = activeEditor.document.uri; 99 | const documentURI = Utils.dirname(editor); 100 | 101 | let response: terraform.ModuleProvidersResponse; 102 | try { 103 | response = await terraform.moduleProviders(documentURI.toString(), this.client); 104 | if (response === null) { 105 | // no response from terraform-ls 106 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noResponse', true); 107 | return []; 108 | } 109 | } catch { 110 | // error from terraform-ls 111 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noResponse', true); 112 | return []; 113 | } 114 | 115 | try { 116 | const list = Object.entries(response.provider_requirements).map( 117 | ([provider, details]) => 118 | new ModuleProviderItem( 119 | provider, 120 | details.display_name, 121 | details.version_constraint, 122 | response.installed_providers[provider], 123 | details.docs_link, 124 | ), 125 | ); 126 | 127 | if (list.length === 0) { 128 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noProviders', true); 129 | } 130 | 131 | return list; 132 | } catch { 133 | // error mapping response 134 | await vscode.commands.executeCommand('setContext', 'terraform.providers.noResponse', true); 135 | return []; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | import { config } from './utils/vscode'; 6 | 7 | export interface InitializationOptions { 8 | indexing?: IndexingOptions; 9 | experimentalFeatures?: ExperimentalFeatures; 10 | ignoreSingleFileWarning?: boolean; 11 | terraform?: TerraformOptions; 12 | validation?: ValidationOptions; 13 | } 14 | 15 | export interface TerraformOptions { 16 | path: string; 17 | timeout: string; 18 | logFilePath: string; 19 | } 20 | 21 | export interface IndexingOptions { 22 | ignoreDirectoryNames: string[]; 23 | ignorePaths: string[]; 24 | } 25 | 26 | export interface ExperimentalFeatures { 27 | validateOnSave: boolean; 28 | prefillRequiredFields: boolean; 29 | } 30 | 31 | export interface ValidationOptions { 32 | enableEnhancedValidation: boolean; 33 | } 34 | 35 | export async function getInitializationOptions() { 36 | /* 37 | This is basically a set of settings masquerading as a function. The intention 38 | here is to make room for this to be added to a configuration builder when 39 | we tackle #791 40 | */ 41 | const validation = config('opentofu').get('validation', { 42 | enableEnhancedValidation: true, 43 | }); 44 | 45 | const terraform = config('opentofu').get('languageServer.opentofu', { 46 | path: '', 47 | timeout: '', 48 | logFilePath: '', 49 | }); 50 | 51 | const indexing = config('opentofu').get('languageServer.indexing', { 52 | ignoreDirectoryNames: [], 53 | ignorePaths: [], 54 | }); 55 | const ignoreSingleFileWarning = config('opentofu').get('languageServer.ignoreSingleFileWarning', false); 56 | const experimentalFeatures = config('opentofu').get('experimentalFeatures'); 57 | 58 | // deprecated 59 | const rootModulePaths = config('opentofu').get('languageServer.rootModules', []); 60 | if (rootModulePaths.length > 0 && indexing.ignorePaths.length > 0) { 61 | throw new Error( 62 | 'Only one of rootModules and indexing.ignorePaths can be set at the same time, please remove the conflicting config and reload', 63 | ); 64 | } 65 | 66 | const initializationOptions: InitializationOptions = { 67 | validation, 68 | experimentalFeatures, 69 | ignoreSingleFileWarning, 70 | terraform, 71 | ...(rootModulePaths.length > 0 && { rootModulePaths }), 72 | indexing, 73 | }; 74 | 75 | return initializationOptions; 76 | } 77 | -------------------------------------------------------------------------------- /src/status/installedVersion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | const installedVersion = vscode.languages.createLanguageStatusItem('opentofu.installedVersion', [ 9 | { language: 'terraform' }, 10 | { language: 'terraform-vars' }, 11 | ]); 12 | installedVersion.name = 'OpenTofuInstalledVersion'; 13 | installedVersion.detail = 'OpenTofu Installed'; 14 | 15 | export function setVersion(version: string) { 16 | installedVersion.text = version; 17 | } 18 | 19 | export function setReady() { 20 | installedVersion.busy = false; 21 | } 22 | 23 | export function setWaiting() { 24 | installedVersion.busy = true; 25 | } 26 | -------------------------------------------------------------------------------- /src/status/language.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | const lsStatus = vscode.languages.createLanguageStatusItem('terraform-ls.status', [ 9 | { language: 'terraform' }, 10 | { language: 'terraform-vars' }, 11 | ]); 12 | lsStatus.name = 'OpenTofu LS'; 13 | lsStatus.detail = 'OpenTofu LS'; 14 | 15 | export function setVersion(version: string) { 16 | lsStatus.text = version; 17 | } 18 | 19 | export function setLanguageServerRunning() { 20 | lsStatus.busy = false; 21 | } 22 | 23 | export function setLanguageServerReady() { 24 | lsStatus.busy = false; 25 | } 26 | 27 | export function setLanguageServerStarting() { 28 | lsStatus.busy = true; 29 | } 30 | 31 | export function setLanguageServerBusy() { 32 | lsStatus.busy = true; 33 | } 34 | 35 | export function setLanguageServerStopped() { 36 | // this makes the statusItem a different color in the bar 37 | // and triggers an alert, so the user 'sees' that the LS is stopped 38 | lsStatus.severity = vscode.LanguageStatusSeverity.Warning; 39 | lsStatus.busy = false; 40 | } 41 | 42 | export function setLanguageServerState( 43 | detail: string, 44 | busy: boolean, 45 | severity: vscode.LanguageStatusSeverity = vscode.LanguageStatusSeverity.Information, 46 | ) { 47 | lsStatus.busy = busy; 48 | lsStatus.detail = detail; 49 | lsStatus.severity = severity; 50 | } 51 | -------------------------------------------------------------------------------- /src/status/requiredVersion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | const requiredVersion = vscode.languages.createLanguageStatusItem('opentofu.requiredVersion', [ 9 | { language: 'terraform' }, 10 | { language: 'terraform-vars' }, 11 | ]); 12 | requiredVersion.name = 'OpenTofuRequiredVersion'; 13 | requiredVersion.detail = 'OpenTofu Required'; 14 | 15 | export function setVersion(version: string) { 16 | requiredVersion.text = version; 17 | } 18 | 19 | export function setReady() { 20 | requiredVersion.busy = false; 21 | } 22 | 23 | export function setWaiting() { 24 | requiredVersion.busy = true; 25 | } 26 | -------------------------------------------------------------------------------- /src/test/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "scripts": {}, 5 | "dependencies": { 6 | "@wdio/cli": "^8.27.2", 7 | "@wdio/local-runner": "^8.27.2", 8 | "@wdio/mocha-framework": "^8.27.2", 9 | "@wdio/spec-reporter": "^8.27.2", 10 | "chromedriver": "^120.0.1", 11 | "comment-json": "^4.2.3", 12 | "expect-webdriverio": "^4.2.7", 13 | "runme": "^3.0.1", 14 | "ts-node": "^10.9.1", 15 | "wdio-vscode-service": "^6.0.0", 16 | "webdriverio": "^8.20.4" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.x" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/e2e/specs/extension.e2e.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { browser, expect } from '@wdio/globals'; 7 | 8 | describe('VS Code Extension Testing', () => { 9 | it('should be able to load VSCode', async () => { 10 | const workbench = await browser.getWorkbench(); 11 | expect(await workbench.getTitleBar().getTitle()).toContain('[Extension Development Host]'); 12 | }); 13 | 14 | it('should load and install our VSCode Extension', async () => { 15 | const extensions = await browser.executeWorkbench((vscodeApi) => { 16 | return vscodeApi.extensions.all; 17 | }); 18 | expect(extensions.some((extension) => extension.id === 'hashicorp.terraform')).toBe(true); 19 | }); 20 | 21 | it('should show all activity bar items', async () => { 22 | const workbench = await browser.getWorkbench(); 23 | const viewControls = await workbench.getActivityBar().getViewControls(); 24 | expect(await Promise.all(viewControls.map((vc) => vc.getTitle()))).toEqual([ 25 | 'Explorer', 26 | 'Search', 27 | 'Source Control', 28 | 'Run and Debug', 29 | 'Extensions', 30 | 'HashiCorp Terraform', 31 | 'HCP Terraform', 32 | ]); 33 | }); 34 | 35 | // this does not appear to work in CI 36 | // it('should start the ls', async () => { 37 | // const workbench = await browser.getWorkbench(); 38 | // await workbench.executeCommand('workbench.panel.output.focus'); 39 | 40 | // const bottomBar = workbench.getBottomBar(); 41 | // await bottomBar.maximize(); 42 | 43 | // const outputView = await bottomBar.openOutputView(); 44 | // await outputView.wait(); 45 | // await outputView.selectChannel('HashiCorp Terraform'); 46 | // const output = await outputView.getText(); 47 | 48 | // expect(output.some((element) => element.toLowerCase().includes('dispatching next job'.toLowerCase()))).toBeTruthy(); 49 | // }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/test/e2e/specs/language/terraform.e2e..ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { StatusBar } from 'wdio-vscode-service'; 7 | import { browser, expect } from '@wdio/globals'; 8 | 9 | import path from 'node:path'; 10 | import { fileURLToPath } from 'url'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | describe('Terraform language tests', () => { 16 | let statusBar: StatusBar; 17 | 18 | before(async () => { 19 | const workbench = await browser.getWorkbench(); 20 | statusBar = workbench.getStatusBar(); 21 | 22 | const testFile = path.join(__dirname, '../../../', 'fixtures', `sample.tf`); 23 | browser.executeWorkbench((vscode, fileToOpen) => { 24 | vscode.commands.executeCommand('vscode.open', vscode.Uri.file(fileToOpen)); 25 | }, testFile); 26 | }); 27 | 28 | after(async () => { 29 | // TODO: Close the file 30 | }); 31 | 32 | it('can detect correct language', async () => { 33 | expect(await statusBar.getCurrentLanguage()).toContain('Terraform'); 34 | }); 35 | 36 | // it('can detect terraform version', async () => { 37 | // let item: WebdriverIO.Element | undefined; 38 | // await browser.waitUntil( 39 | // async () => { 40 | // const i = await statusBar.getItems(); 41 | // // console.log(i); 42 | 43 | // item = await statusBar.getItem( 44 | // 'Editor Language Status: 0.32.7, Terraform LS, next: 1.6.6, Terraform Installed, next: any, Terraform Required', 45 | // ); 46 | // }, 47 | // { timeout: 10000, timeoutMsg: 'Did not find a version' }, 48 | // ); 49 | 50 | // expect(item).toBeDefined(); 51 | // }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/test/e2e/specs/views/terraform.e2e.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | import { browser, expect } from '@wdio/globals'; 6 | import { Workbench, CustomTreeItem, SideBarView, ViewSection, ViewControl } from 'wdio-vscode-service'; 7 | 8 | import path from 'node:path'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | describe('Terraform ViewContainer', function () { 15 | this.retries(3); 16 | let workbench: Workbench; 17 | 18 | before(async () => { 19 | workbench = await browser.getWorkbench(); 20 | }); 21 | 22 | after(async () => { 23 | // TODO: Close the file 24 | }); 25 | 26 | it('should have terraform viewcontainer', async () => { 27 | const viewContainers = await workbench.getActivityBar().getViewControls(); 28 | const titles = await Promise.all(viewContainers.map((vc) => vc.getTitle())); 29 | expect(titles).toContain('HashiCorp Terraform'); 30 | }); 31 | 32 | describe('in an terraform project', () => { 33 | before(async () => { 34 | const testFile = path.join(__dirname, '../../../', 'fixtures', `sample.tf`); 35 | browser.executeWorkbench((vscode, fileToOpen) => { 36 | vscode.commands.executeCommand('vscode.open', vscode.Uri.file(fileToOpen)); 37 | }, testFile); 38 | }); 39 | 40 | after(async () => { 41 | // TODO: close the file 42 | }); 43 | 44 | describe('providers view', () => { 45 | let terraformViewContainer: ViewControl | undefined; 46 | let openViewContainer: SideBarView | undefined; 47 | let callSection: ViewSection | undefined; 48 | let items: CustomTreeItem[]; 49 | 50 | before(async () => { 51 | terraformViewContainer = await workbench.getActivityBar().getViewControl('HashiCorp Terraform'); 52 | await terraformViewContainer?.wait(); 53 | await terraformViewContainer?.openView(); 54 | openViewContainer = workbench.getSideBar(); 55 | }); 56 | 57 | it('should have providers view', async () => { 58 | callSection = await openViewContainer?.getContent().getSection('PROVIDERS'); 59 | expect(callSection).toBeDefined(); 60 | }); 61 | 62 | it('should include all providers', async () => { 63 | callSection = await openViewContainer?.getContent().getSection('PROVIDERS'); 64 | 65 | await browser.waitUntil( 66 | async () => { 67 | const provider = await callSection?.getVisibleItems(); 68 | if (!provider) { 69 | return false; 70 | } 71 | 72 | if (provider.length > 0) { 73 | items = provider as CustomTreeItem[]; 74 | return true; 75 | } 76 | }, 77 | { timeout: 3_000, timeoutMsg: 'Never found any providers' }, 78 | ); 79 | 80 | const labels = await Promise.all(items.map((vi) => vi.getLabel())); 81 | expect(labels).toEqual(['-/vault', 'hashicorp/google']); 82 | }); 83 | }); 84 | 85 | describe('calls view', () => { 86 | let terraformViewContainer: ViewControl | undefined; 87 | let openViewContainer: SideBarView | undefined; 88 | let callSection: ViewSection | undefined; 89 | let items: CustomTreeItem[]; 90 | 91 | before(async () => { 92 | terraformViewContainer = await workbench.getActivityBar().getViewControl('HashiCorp Terraform'); 93 | await terraformViewContainer?.wait(); 94 | await terraformViewContainer?.openView(); 95 | openViewContainer = workbench.getSideBar(); 96 | }); 97 | 98 | it('should have module calls view', async () => { 99 | callSection = await openViewContainer?.getContent().getSection('MODULE CALLS'); 100 | expect(callSection).toBeDefined(); 101 | }); 102 | 103 | it('should include all module calls', async () => { 104 | callSection = await openViewContainer?.getContent().getSection('MODULE CALLS'); 105 | 106 | await browser.waitUntil( 107 | async () => { 108 | const calls = await callSection?.getVisibleItems(); 109 | if (!calls) { 110 | return false; 111 | } 112 | 113 | if (calls.length > 0) { 114 | items = calls as CustomTreeItem[]; 115 | return true; 116 | } 117 | }, 118 | { timeout: 3_000, timeoutMsg: 'Never found any modules' }, 119 | ); 120 | 121 | const labels = await Promise.all(items.map((vi) => vi.getLabel())); 122 | expect(labels).toEqual(['compute', 'local']); 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/test/e2e/specs/views/tfc.e2e.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | import { browser, expect } from '@wdio/globals'; 6 | import { fail } from 'assert'; 7 | import { Workbench, SideBarView, ViewSection, ViewControl, WelcomeContentButton } from 'wdio-vscode-service'; 8 | import { Key } from 'webdriverio'; 9 | 10 | let workbench: Workbench; 11 | let terraformViewContainer: SideBarView; 12 | let callSection: ViewSection; 13 | 14 | describe('TFC ViewContainer', function () { 15 | this.retries(3); 16 | 17 | beforeEach(async () => { 18 | workbench = await browser.getWorkbench(); 19 | }); 20 | 21 | it('should have TFC viewcontainer', async () => { 22 | const viewContainers = await workbench.getActivityBar().getViewControls(); 23 | const titles = await Promise.all(viewContainers.map((vc) => vc.getTitle())); 24 | expect(titles).toContain('HCP Terraform'); 25 | }); 26 | 27 | describe('not logged in', () => { 28 | let terraformViewControl: ViewControl | undefined; 29 | 30 | beforeEach(async () => { 31 | terraformViewControl = await workbench.getActivityBar().getViewControl('HCP Terraform'); 32 | expect(terraformViewControl).toBeDefined(); 33 | await terraformViewControl?.wait(); 34 | await terraformViewControl?.openView(); 35 | terraformViewContainer = workbench.getSideBar(); 36 | }); 37 | 38 | it('should have workspaces view', async () => { 39 | callSection = await terraformViewContainer.getContent().getSection('WORKSPACES'); 40 | expect(callSection).toBeDefined(); 41 | 42 | const welcome = await callSection.findWelcomeContent(); 43 | 44 | const text = await welcome?.getTextSections(); 45 | expect(text).toContain('In order to use HCP Terraform features, you need to be logged in'); 46 | }); 47 | 48 | it('should have runs view', async () => { 49 | callSection = await terraformViewContainer.getContent().getSection('RUNS'); 50 | expect(callSection).toBeDefined(); 51 | }); 52 | }); 53 | 54 | describe('logged in', () => { 55 | let terraformViewControl: ViewControl | undefined; 56 | 57 | beforeEach(async () => { 58 | terraformViewControl = await workbench.getActivityBar().getViewControl('HCP Terraform'); 59 | expect(terraformViewControl).toBeDefined(); 60 | await terraformViewControl?.wait(); 61 | await terraformViewControl?.openView(); 62 | terraformViewContainer = workbench.getSideBar(); 63 | }); 64 | 65 | it('should login', async () => { 66 | callSection = await terraformViewContainer.getContent().getSection('WORKSPACES'); 67 | expect(callSection).toBeDefined(); 68 | 69 | const welcome = await callSection.findWelcomeContent(); 70 | 71 | const text = await welcome?.getTextSections(); 72 | expect(text).toContain('In order to use HCP Terraform features, you need to be logged in'); 73 | 74 | const buttons = await welcome?.getButtons(); 75 | expect(buttons).toHaveLength(1); 76 | if (!buttons) { 77 | fail('No buttons found'); 78 | } 79 | 80 | let loginButton: WelcomeContentButton | undefined; 81 | for (const button of buttons) { 82 | const buttonText = await button.getTitle(); 83 | if (buttonText.toLowerCase().includes('login')) { 84 | loginButton = button; 85 | } 86 | } 87 | if (!loginButton) { 88 | fail("Couldn't find the login button"); 89 | } 90 | 91 | (await loginButton.elem).click(); 92 | 93 | // detect modal and click Allow 94 | browser.keys([Key.Enter]); 95 | 96 | // detect quickpick and select Existing user token 97 | browser.keys(['ArrowDown', Key.Enter]); 98 | 99 | // TODO: enter token in input box and hit enter 100 | 101 | // TODO: verify you are logged in 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "moduleResolution": "node", 5 | "module": "ESNext", 6 | "types": [ 7 | "node", 8 | "mocha", 9 | "wdio-vscode-service", 10 | "@wdio/globals/types", 11 | "expect-webdriverio", 12 | "@wdio/mocha-framework" 13 | ], 14 | "target": "es2022", 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["specs", "./wdio.conf.ts"], 18 | "exclude": [] 19 | } 20 | -------------------------------------------------------------------------------- /src/test/fixtures/actions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | # code that needs to be formatted 3 | } 4 | -------------------------------------------------------------------------------- /src/test/fixtures/ai/main.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/src/test/fixtures/ai/main.tf -------------------------------------------------------------------------------- /src/test/fixtures/ai/variables.tf: -------------------------------------------------------------------------------- 1 | variable "agi" { 2 | default = false 3 | } 4 | -------------------------------------------------------------------------------- /src/test/fixtures/compute/main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "google_compute_network" "vpc_network" { 3 | name = "terraform-network" 4 | } 5 | 6 | resource "google_compute_instance" "vm_instance" { 7 | name = var.instance_name 8 | machine_type = var.machine_type 9 | 10 | boot_disk { 11 | initialize_params { 12 | image = "debian-cloud/debian-11" 13 | } 14 | } 15 | 16 | network_interface { 17 | network = google_compute_network.vpc_network.name 18 | access_config { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/fixtures/compute/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ip" { 2 | value = google_compute_instance.vm_instance.network_interface[0].network_ip 3 | } 4 | -------------------------------------------------------------------------------- /src/test/fixtures/compute/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_name" { 2 | type = string 3 | description = "Name of the compute instance" 4 | } 5 | 6 | variable "machine_type" { 7 | type = string 8 | default = "f1-micro" 9 | } 10 | -------------------------------------------------------------------------------- /src/test/fixtures/empty.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/src/test/fixtures/empty.tf -------------------------------------------------------------------------------- /src/test/fixtures/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | 10 | provider "google" { 11 | credentials = file(var.credentials_file) 12 | 13 | project = var.project 14 | region = var.region 15 | zone = var.zone 16 | } 17 | 18 | module "compute" { 19 | source = "./compute" 20 | 21 | instance_name = "terraform-machine" 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/fixtures/sample.tf: -------------------------------------------------------------------------------- 1 | provider "vault" { 2 | } 3 | 4 | resource "vault_auth_backend" "b" { 5 | } 6 | 7 | module "local" { 8 | source = "./modules" 9 | } 10 | -------------------------------------------------------------------------------- /src/test/fixtures/terraform.tfvars: -------------------------------------------------------------------------------- 1 | zone = "us-central1-c" 2 | -------------------------------------------------------------------------------- /src/test/fixtures/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | type = string 3 | } 4 | 5 | variable "credentials_file" { 6 | type = string 7 | } 8 | 9 | variable "region" { 10 | default = "us-central1" 11 | } 12 | 13 | variable "zone" { 14 | default = "us-central1-c" 15 | } 16 | -------------------------------------------------------------------------------- /src/test/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | 9 | export async function open(docUri: vscode.Uri): Promise { 10 | try { 11 | const doc = await vscode.workspace.openTextDocument(docUri); 12 | await vscode.window.showTextDocument(doc); 13 | } catch (e) { 14 | console.error(e); 15 | throw e; 16 | } 17 | } 18 | 19 | export const getDocUri = (p: string): vscode.Uri => { 20 | const workspaceUri = vscode.workspace.workspaceFolders?.at(0)?.uri; 21 | 22 | if (!workspaceUri) { 23 | throw new Error(`No workspace folder found while trying to create uri for file "${p}".`); 24 | } 25 | return vscode.Uri.joinPath(workspaceUri, p); 26 | }; 27 | 28 | export async function moveCursor(position: vscode.Position): Promise { 29 | if (!vscode.window.activeTextEditor) { 30 | throw new Error('No active text editor. please use vscode.workspace.openTextDocument() to open a document first'); 31 | } 32 | vscode.window.activeTextEditor.selections = [new vscode.Selection(position, position)]; 33 | } 34 | 35 | export async function testCompletion( 36 | docUri: vscode.Uri, 37 | position: vscode.Position, 38 | expectedCompletionList: vscode.CompletionList, 39 | ) { 40 | const actualCompletionList = await vscode.commands.executeCommand( 41 | 'vscode.executeCompletionItemProvider', 42 | docUri, 43 | position, 44 | ); 45 | 46 | try { 47 | assert.deepStrictEqual( 48 | actualCompletionList.items.length, 49 | expectedCompletionList.items.length, 50 | `Expected ${expectedCompletionList.items.length} completions but got ${actualCompletionList.items.length}`, 51 | ); 52 | expectedCompletionList.items.forEach((expectedItem, i) => { 53 | const actualItem = actualCompletionList.items[i]; 54 | assert.deepStrictEqual( 55 | actualItem.label, 56 | expectedItem.label, 57 | `Expected label ${expectedItem.label} but got ${actualItem.label}`, 58 | ); 59 | assert.deepStrictEqual( 60 | actualItem.kind, 61 | expectedItem.kind, 62 | `Expected kind ${ 63 | expectedItem.kind ? vscode.CompletionItemKind[expectedItem.kind] : expectedItem.kind 64 | } but got ${actualItem.kind ? vscode.CompletionItemKind[actualItem.kind] : actualItem.kind}`, 65 | ); 66 | }); 67 | } catch (e) { 68 | // print out the actual and expected completion lists for easier debugging when the test fails 69 | console.log('expectedCompletionList', expectedCompletionList); 70 | console.log('actualCompletionList', actualCompletionList); 71 | throw e; 72 | } 73 | } 74 | 75 | export async function testHover(docUri: vscode.Uri, position: vscode.Position, expectedCompletionList: vscode.Hover[]) { 76 | const actualhover = await vscode.commands.executeCommand( 77 | 'vscode.executeHoverProvider', 78 | docUri, 79 | position, 80 | ); 81 | 82 | assert.equal(actualhover.length, expectedCompletionList.length); 83 | expectedCompletionList.forEach((expectedItem, i) => { 84 | const actualItem = actualhover[i]; 85 | assert.deepStrictEqual(actualItem.contents, expectedItem.contents); 86 | }); 87 | } 88 | 89 | export async function testDefinitions( 90 | docUri: vscode.Uri, 91 | position: vscode.Position, 92 | expectedDefinitions: vscode.Location[], 93 | ) { 94 | const actualDefinitions = await vscode.commands.executeCommand( 95 | 'vscode.executeDefinitionProvider', 96 | docUri, 97 | position, 98 | ); 99 | 100 | assert.equal(actualDefinitions.length, expectedDefinitions.length); 101 | expectedDefinitions.forEach((expectedItem, i) => { 102 | const actualItem = actualDefinitions[i]; 103 | if (actualItem instanceof vscode.Location) { 104 | assert.deepStrictEqual(actualItem.uri.path, expectedItem.uri.path); 105 | assert.deepStrictEqual(actualItem.range.start, expectedItem.range.start); 106 | assert.deepStrictEqual(actualItem.range.end, expectedItem.range.end); 107 | return; 108 | } else { 109 | // } else if (actualItem instanceof vscode.LocationLink) { 110 | assert.deepStrictEqual(actualItem.targetUri.path, expectedItem.uri.path); 111 | assert.deepStrictEqual(actualItem.targetRange.start, expectedItem.range.start); 112 | assert.deepStrictEqual(actualItem.targetRange.end, expectedItem.range.end); 113 | } 114 | }); 115 | } 116 | 117 | export async function testReferences( 118 | docUri: vscode.Uri, 119 | position: vscode.Position, 120 | expectedDefinitions: vscode.Location[], 121 | ) { 122 | const actualDefinitions = await vscode.commands.executeCommand( 123 | 'vscode.executeReferenceProvider', 124 | docUri, 125 | position, 126 | ); 127 | 128 | assert.equal(actualDefinitions.length, expectedDefinitions.length); 129 | expectedDefinitions.forEach((expectedItem, i) => { 130 | const actualItem = actualDefinitions[i]; 131 | assert.deepStrictEqual(actualItem.uri.path, expectedItem.uri.path); 132 | assert.deepStrictEqual(actualItem.range.start, expectedItem.range.start); 133 | assert.deepStrictEqual(actualItem.range.end, expectedItem.range.end); 134 | }); 135 | } 136 | 137 | export async function testSymbols(docUri: vscode.Uri, symbolNames: string[]) { 138 | const symbols = await vscode.commands.executeCommand( 139 | 'vscode.executeDocumentSymbolProvider', 140 | docUri, 141 | ); 142 | 143 | assert.strictEqual(symbols.length, symbolNames.length); 144 | symbols.forEach((symbol, i) => { 145 | assert.strictEqual(symbol.name, symbolNames[i]); 146 | }); 147 | } 148 | 149 | export async function activateExtension() { 150 | const ext = vscode.extensions.getExtension('hashicorp.terraform'); 151 | if (!ext?.isActive) { 152 | await ext?.activate(); 153 | await sleep(1000); 154 | } 155 | } 156 | 157 | export async function sleep(ms: number) { 158 | return new Promise((resolve) => setTimeout(resolve, ms)); 159 | } 160 | -------------------------------------------------------------------------------- /src/test/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | 3 | This directory contains integration tests which test the VS Code extension (and the included language server) in a VS Code instance. 4 | 5 | Run these tests via 6 | ```sh 7 | $ npm run test 8 | ``` 9 | 10 | ## Adding new tests 11 | 12 | The way `vscode-test-cli` currently works is that it expects a single workspace to be opened and all tests to run within that workspace. To allow for multiple scenarios, it instead supports specifying an array of separate configs in `.vscode-test.mjs`. To make it easier for us to have multiple test suites with different workspace fixtures without needing to explicitly define all of them in a very similar fashion, our `vscode-test.mjs` builds this configuration dynamically by reading the `src/test/integration` directory. 13 | 14 | To add new test cases for existing test suites (aka tests sharing the same fixtures aka the same workspace), you can just add more `*.test.ts` files alongside the existing ones. 15 | 16 | To add a new test suite (which brings its own workspace), add a new directory within `src/test/integration` that itself contains a directory named `workspace` which in most cases contains the Terraform config that should be used in tests. 17 | 18 | The `mocks` directory contains no `workspace` subdirectory, hence it's not loaded as a test suite. It contains API mocks that are currently shared by all test suites. 19 | 20 | ## Caveats 21 | 22 | ### Stale Extension / Tests 23 | The extension and the tests aren't compiled as part of the tests, if invoking them via the VS Code ["Extension Test Runner" Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner). The easiest way to use the extension for running tests from the editor is to keep running `npm run compile:test:watch` in a terminal. 24 | 25 | Beware that removing TypeScript files won't remove the transpiled JS files in the `out` directory. If you are experiencing Phantom tests running, run `rm -rf out`. 26 | 27 | ### Stale VS Code test instance state 28 | During the development of tests, it is possible to bring VS Code into a state where it has multiple windows open and seems to add one new window every time the tests are run. This happened for example, when trying to replace the current workspace folder with another one. If this happens, run `rm -rf .vscode-test` to remove the directory in which the test instance is installed into and which holds its state. 29 | 30 | ### Language Server isn't re-fetched automatically 31 | The language server used by the extension is automatically downloaded when running `npm install` in `vscode-terraform` if it didn't yet exist in `bin/terraform-ls`. This means that you might be running an outdated language server if you haven't manually removed or replaced that file in a long time. To update the language server, run `rm bin/terraform-ls && npm install`. 32 | -------------------------------------------------------------------------------- /src/test/integration/basics/codeAction.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { expect } from 'chai'; 9 | import { activateExtension, getDocUri, open, sleep } from '../../helper'; 10 | 11 | suite('code actions', () => { 12 | suite('format all', function suite() { 13 | const docUri = getDocUri('actions.tf'); 14 | 15 | this.beforeAll(async () => { 16 | await open(docUri); 17 | await activateExtension(); 18 | }); 19 | 20 | teardown(async () => { 21 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 22 | }); 23 | 24 | test('language is registered', async () => { 25 | const doc = await vscode.workspace.openTextDocument(docUri); 26 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 27 | }); 28 | 29 | test('formats the document', async () => { 30 | const supported = [ 31 | new vscode.CodeAction('Format Document', vscode.CodeActionKind.Source.append('formatAll').append('terraform')), 32 | ]; 33 | 34 | // wait till the LS is ready to accept a code action request 35 | await sleep(1000); 36 | 37 | for (let index = 0; index < supported.length; index++) { 38 | const wanted = supported[index]; 39 | const requested = wanted.kind?.value.toString(); 40 | 41 | const actions = await vscode.commands.executeCommand( 42 | 'vscode.executeCodeActionProvider', 43 | docUri, 44 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), 45 | requested, 46 | ); 47 | 48 | assert.ok(actions); 49 | expect(actions).not.to.be.undefined; 50 | expect(wanted.kind?.value).not.to.be.undefined; 51 | 52 | //TODO: update format tests when laguage server ready 53 | //assert.strictEqual(actions.length, 1); 54 | //assert.strictEqual(actions[1].title, wanted.title); 55 | //assert.strictEqual(actions[0].kind?.value, wanted.kind?.value); 56 | } 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/test/integration/basics/definition.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { activateExtension, getDocUri, open, testDefinitions } from '../../helper'; 9 | 10 | suite('definitions', () => { 11 | suite('go to module definition', function suite() { 12 | const docUri = getDocUri('main.tf'); 13 | 14 | this.beforeAll(async () => { 15 | await open(docUri); 16 | await activateExtension(); 17 | }); 18 | 19 | teardown(async () => { 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('language is registered', async () => { 24 | const doc = await vscode.workspace.openTextDocument(docUri); 25 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 26 | }); 27 | 28 | test('returns definition for module source', async () => { 29 | const location = new vscode.Location( 30 | getDocUri('compute/main.tf'), 31 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), 32 | ); 33 | 34 | // module "compute" { 35 | // source = "./compute" 36 | await testDefinitions(docUri, new vscode.Position(18, 11), [location]); 37 | }); 38 | 39 | test('returns definition for module attribute', async () => { 40 | const location = new vscode.Location( 41 | getDocUri('compute/variables.tf'), 42 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(3, 1)), 43 | ); 44 | 45 | // module "compute" { 46 | // source = "./compute" 47 | // instance_name = "terraform-machine" 48 | await testDefinitions(docUri, new vscode.Position(20, 2), [location]); 49 | }); 50 | 51 | test('returns definition for variable', async () => { 52 | // provider "google" { 53 | // credentials = file(var.credentials_file) 54 | // project = var.project 55 | // region = var.region 56 | // zone = var.zone 57 | const location = new vscode.Location( 58 | getDocUri('variables.tf'), 59 | new vscode.Range(new vscode.Position(4, 0), new vscode.Position(6, 1)), 60 | ); 61 | 62 | await testDefinitions(docUri, new vscode.Position(10, 36), [location]); 63 | }); 64 | }); 65 | 66 | suite('go to variable definition', function suite() { 67 | const docUri = getDocUri('terraform.tfvars'); 68 | 69 | this.beforeAll(async () => { 70 | await open(docUri); 71 | await activateExtension(); 72 | }); 73 | 74 | teardown(async () => { 75 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 76 | }); 77 | 78 | test('language is registered', async () => { 79 | const doc = await vscode.workspace.openTextDocument(docUri); 80 | assert.equal(doc.languageId, 'terraform-vars', 'document language should be `terraform-vars`'); 81 | }); 82 | 83 | test('returns definition for module source', async () => { 84 | const location = new vscode.Location( 85 | getDocUri('variables.tf'), 86 | new vscode.Range(new vscode.Position(12, 0), new vscode.Position(14, 1)), 87 | ); 88 | 89 | // module "compute" { 90 | // source = "./compute" 91 | await testDefinitions(docUri, new vscode.Position(0, 1), [location]); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/test/integration/basics/hover.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { activateExtension, getDocUri, open, testHover } from '../../helper'; 9 | 10 | suite('hover', () => { 11 | suite('core schema', function suite() { 12 | const docUri = getDocUri('main.tf'); 13 | 14 | this.beforeAll(async () => { 15 | await open(docUri); 16 | await activateExtension(); 17 | }); 18 | 19 | teardown(async () => { 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('language is registered', async () => { 24 | const doc = await vscode.workspace.openTextDocument(docUri); 25 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 26 | }); 27 | 28 | test('returns docs for terraform block', async () => { 29 | await testHover(docUri, new vscode.Position(0, 1), [ 30 | new vscode.Hover( 31 | new vscode.MarkdownString( 32 | '**terraform** _Block_\n\nTerraform block used to configure some high-level behaviors of Terraform', 33 | ), 34 | new vscode.Range(new vscode.Position(14, 12), new vscode.Position(14, 20)), 35 | ), 36 | ]); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/test/integration/basics/references.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { activateExtension, getDocUri, open, testReferences } from '../../helper'; 9 | 10 | suite('references', () => { 11 | suite('module references', function suite() { 12 | const docUri = getDocUri('variables.tf'); 13 | 14 | this.beforeAll(async () => { 15 | await open(docUri); 16 | await activateExtension(); 17 | }); 18 | 19 | teardown(async () => { 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('language is registered', async () => { 24 | const doc = await vscode.workspace.openTextDocument(docUri); 25 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 26 | }); 27 | 28 | test('returns definition for module source', async () => { 29 | await testReferences(docUri, new vscode.Position(12, 10), [ 30 | new vscode.Location( 31 | getDocUri('main.tf'), 32 | new vscode.Range(new vscode.Position(14, 12), new vscode.Position(14, 20)), 33 | ), 34 | new vscode.Location( 35 | getDocUri('terraform.tfvars'), 36 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 4)), 37 | ), 38 | ]); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/test/integration/basics/symbols.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { activateExtension, getDocUri, open, testSymbols } from '../../helper'; 9 | 10 | suite('symbols', () => { 11 | suite('basic language symbols', function suite() { 12 | const docUri = getDocUri('sample.tf'); 13 | 14 | this.beforeAll(async () => { 15 | await open(docUri); 16 | await activateExtension(); 17 | }); 18 | 19 | teardown(async () => { 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('language is registered', async () => { 24 | const doc = await vscode.workspace.openTextDocument(docUri); 25 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 26 | }); 27 | 28 | test('returns symbols', async () => { 29 | await testSymbols(docUri, ['provider "vault"', 'resource "vault_auth_backend" "b"', 'module "local"']); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/actions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | # code that needs to be formatted 3 | } 4 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/ai/main.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/src/test/integration/basics/workspace/ai/main.tf -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/ai/variables.tf: -------------------------------------------------------------------------------- 1 | variable "agi" { 2 | default = false 3 | } 4 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/compute/main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "google_compute_network" "vpc_network" { 3 | name = "terraform-network" 4 | } 5 | 6 | resource "google_compute_instance" "vm_instance" { 7 | name = var.instance_name 8 | machine_type = var.machine_type 9 | 10 | boot_disk { 11 | initialize_params { 12 | image = "debian-cloud/debian-11" 13 | } 14 | } 15 | 16 | network_interface { 17 | network = google_compute_network.vpc_network.name 18 | access_config { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/compute/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ip" { 2 | value = google_compute_instance.vm_instance.network_interface[0].network_ip 3 | } -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/compute/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_name" { 2 | type = string 3 | description = "Name of the compute instance" 4 | } 5 | 6 | variable "machine_type" { 7 | type = string 8 | default = "f1-micro" 9 | } 10 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/empty.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/src/test/integration/basics/workspace/empty.tf -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | 10 | provider "google" { 11 | credentials = file(var.credentials_file) 12 | 13 | project = var.project 14 | region = var.region 15 | zone = var.zone 16 | } 17 | 18 | module "compute" { 19 | source = "./compute" 20 | 21 | instance_name = "terraform-machine" 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/registry_module.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "5.8.0" 4 | 5 | external_ 6 | } 7 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/sample.tf: -------------------------------------------------------------------------------- 1 | provider "vault" { 2 | } 3 | 4 | resource "vault_auth_backend" "b" { 5 | } 6 | 7 | module "local" { 8 | source = "./modules" 9 | } 10 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/terraform.tfvars: -------------------------------------------------------------------------------- 1 | zone = "us-central1-c" 2 | -------------------------------------------------------------------------------- /src/test/integration/basics/workspace/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | type = string 3 | } 4 | 5 | variable "credentials_file" { 6 | type = string 7 | } 8 | 9 | variable "region" { 10 | default = "us-central1" 11 | } 12 | 13 | variable "zone" { 14 | default = "us-central1-c" 15 | } 16 | -------------------------------------------------------------------------------- /src/test/integration/init/init.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { assert } from 'chai'; 8 | import { activateExtension, getDocUri, open, testCompletion, sleep } from '../../helper'; 9 | 10 | suite('init', () => { 11 | suite('with bundled provider schema', function suite() { 12 | const docUri = getDocUri('main.tf'); 13 | 14 | this.beforeAll(async () => { 15 | await open(docUri); 16 | await activateExtension(); 17 | }); 18 | 19 | teardown(async () => { 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('language is registered', async () => { 24 | const doc = await vscode.workspace.openTextDocument(docUri); 25 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 26 | }); 27 | 28 | test('completes resource available in bundled schema', async () => { 29 | // aws_eip_domain_name was added in provider version 5.46.0 30 | const expected = [new vscode.CompletionItem('aws_eip_domain_name', vscode.CompletionItemKind.Field)]; 31 | 32 | await testCompletion(docUri, new vscode.Position(13, 25), { 33 | items: expected, 34 | }); 35 | }); 36 | }); 37 | 38 | suite('with provider schema from init', function suite() { 39 | const docUri = getDocUri('main.tf'); 40 | 41 | this.beforeAll(async () => { 42 | await open(docUri); 43 | await activateExtension(); 44 | 45 | // run terraform init command to download provider schema 46 | await vscode.commands.executeCommand('opentofu.initCurrent'); 47 | // wait for schema to be loaded 48 | await sleep(5_000); 49 | }); 50 | 51 | this.afterAll(async () => { 52 | // remove .terraform directory 53 | const dotTerraform = getDocUri('.terraform'); 54 | await vscode.workspace.fs.delete(dotTerraform, { recursive: true }); 55 | // remove .terraform.lock.hcl 56 | const lockfile = getDocUri('.terraform.lock.hcl'); 57 | await vscode.workspace.fs.delete(lockfile); 58 | }); 59 | 60 | teardown(async () => { 61 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 62 | }); 63 | 64 | test('language is registered', async () => { 65 | const doc = await vscode.workspace.openTextDocument(docUri); 66 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 67 | }); 68 | 69 | /* test('completes resource not available in downloaded schema', async () => { 70 | const actualCompletionList = await vscode.commands.executeCommand( 71 | 'vscode.executeCompletionItemProvider', 72 | docUri, 73 | new vscode.Position(13, 25), 74 | ); 75 | 76 | const item = actualCompletionList.items.find((item) => { 77 | if (item.label === 'aws_eip_domain_name') { 78 | return item; 79 | } 80 | }); 81 | 82 | // aws_eip_domain_name was added in provider version 5.46.0 but we initialized with 5.45.0 83 | assert.isUndefined(item, 'aws_eip_domain_name should not be in completion list'); 84 | });*/ 85 | }); 86 | 87 | // This test is skipped as it fails weirdly on CI. It works fine locally and on Windows and OS X in CI. 88 | // On Ubuntu it fails with the following behavior: 89 | // We have a matrix build running the tests on three different VS Code versions. When this test fails, it 90 | // fails on one or two of the VS Code versions, but not all three (so far). The weird thing is that the 91 | // succeeding job changes between commits and retries of the failed job continue to fail. The current suspicion 92 | // is that it is placed on some Ubuntu machine that has a different configuration than the others and that retries 93 | // will run on the same machine the job was assigned to initially. 94 | // When failing, it is missing the inputs for the module that should have been downloaded from the git repository 95 | // via Terraform init. There are no errors in the logs indicating anything wrong. 96 | suite.skip('with module schema from git', function suite() { 97 | const docUri = getDocUri('git_module.tf'); 98 | 99 | this.beforeAll(async () => { 100 | await open(docUri); 101 | await activateExtension(); 102 | 103 | // run terraform init command to download provider schema 104 | await vscode.commands.executeCommand('opentofu.initCurrent'); 105 | // wait for schema to be loaded 106 | await sleep(5_000); 107 | }); 108 | 109 | this.afterAll(async () => { 110 | // remove .terraform directory 111 | const dotTerraform = getDocUri('.terraform'); 112 | await vscode.workspace.fs.delete(dotTerraform, { recursive: true }); 113 | // remove .terraform.lock.hcl 114 | const lockfile = getDocUri('.terraform.lock.hcl'); 115 | await vscode.workspace.fs.delete(lockfile); 116 | }); 117 | 118 | teardown(async () => { 119 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 120 | }); 121 | 122 | test('language is registered', async () => { 123 | const doc = await vscode.workspace.openTextDocument(docUri); 124 | assert.equal(doc.languageId, 'terraform', 'document language should be `terraform`'); 125 | }); 126 | 127 | test('completes module from downloaded schema', async () => { 128 | const expected = [ 129 | new vscode.CompletionItem('count', vscode.CompletionItemKind.Property), 130 | new vscode.CompletionItem('depends_on', vscode.CompletionItemKind.Property), 131 | new vscode.CompletionItem('for_each', vscode.CompletionItemKind.Property), 132 | new vscode.CompletionItem('prefix', vscode.CompletionItemKind.Property), 133 | new vscode.CompletionItem('providers', vscode.CompletionItemKind.Property), 134 | new vscode.CompletionItem('suffix', vscode.CompletionItemKind.Property), 135 | new vscode.CompletionItem('unique-include-numbers', vscode.CompletionItemKind.Property), 136 | new vscode.CompletionItem('unique-length', vscode.CompletionItemKind.Property), 137 | new vscode.CompletionItem('unique-seed', vscode.CompletionItemKind.Property), 138 | new vscode.CompletionItem('version', vscode.CompletionItemKind.Property), 139 | // snippets 140 | new vscode.CompletionItem({ label: 'fore', description: 'For Each' }, vscode.CompletionItemKind.Snippet), 141 | new vscode.CompletionItem({ label: 'module', description: 'Module' }, vscode.CompletionItemKind.Snippet), 142 | new vscode.CompletionItem({ label: 'output', description: 'Output' }, vscode.CompletionItemKind.Snippet), 143 | new vscode.CompletionItem( 144 | { label: 'provisioner', description: 'Provisioner' }, 145 | vscode.CompletionItemKind.Snippet, 146 | ), 147 | new vscode.CompletionItem({ label: 'vare', description: 'Empty variable' }, vscode.CompletionItemKind.Snippet), 148 | new vscode.CompletionItem({ label: 'varm', description: 'Map variable' }, vscode.CompletionItemKind.Snippet), 149 | ]; 150 | 151 | await testCompletion(docUri, new vscode.Position(2, 5), { 152 | items: expected, 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/test/integration/init/workspace/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordBasedSuggestions": "off" // required so that completions don't fall back to completing words if there are no results 3 | } 4 | -------------------------------------------------------------------------------- /src/test/integration/init/workspace/git_module.tf: -------------------------------------------------------------------------------- 1 | module "naming" { 2 | source = "git::https://github.com/Azure/terraform-azurerm-naming?ref=5c19ec284757f2a8fd2308505d2622a69c850fad" 3 | 4 | } 5 | -------------------------------------------------------------------------------- /src/test/integration/init/workspace/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "5.45.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | # Configuration options 12 | } 13 | 14 | resource "aws_eip_domain_name" "name" { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/.terraform-version: -------------------------------------------------------------------------------- 1 | 1.10.0-alpha20240606 2 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | provider "registry.terraform.io/hashicorp/archive" { 2 | version = "2.2.0" 3 | constraints = "~> 2.2.0" 4 | hashes = [ 5 | "h1:2K5LQkuWRS2YN1/YoNaHn9MAzjuTX8Gaqy6i8Mbfv8Y=", 6 | "h1:62mVchC1L6vOo5QS9uUf52uu0emsMM+LsPQJ1BEaTms=", 7 | "h1:CIWi5G6ob7p2wWoThRQbOB8AbmFlCzp7Ka81hR3cVp0=", 8 | "zh:06bd875932288f235c16e2237142b493c2c2b6aba0e82e8c85068332a8d2a29e", 9 | "zh:0c681b481372afcaefddacc7ccdf1d3bb3a0c0d4678a526bc8b02d0c331479bc", 10 | "zh:100fc5b3fc01ea463533d7bbfb01cb7113947a969a4ec12e27f5b2be49884d6c", 11 | "zh:55c0d7ddddbd0a46d57c51fcfa9b91f14eed081a45101dbfc7fd9d2278aa1403", 12 | "zh:73a5dd68379119167934c48afa1101b09abad2deb436cd5c446733e705869d6b", 13 | "zh:841fc4ac6dc3479981330974d44ad2341deada8a5ff9e3b1b4510702dfbdbed9", 14 | "zh:91be62c9b41edb137f7f835491183628d484e9d6efa82fcb75cfa538c92791c5", 15 | "zh:acd5f442bd88d67eb948b18dc2ed421c6c3faee62d3a12200e442bfff0aa7d8b", 16 | "zh:ad5720da5524641ad718a565694821be5f61f68f1c3c5d2cfa24426b8e774bef", 17 | "zh:e63f12ea938520b3f83634fc29da28d92eed5cfbc5cc8ca08281a6a9c36cca65", 18 | "zh:f6542918faa115df46474a36aabb4c3899650bea036b5f8a5e296be6f8f25767", 19 | ] 20 | } 21 | 22 | provider "registry.terraform.io/hashicorp/aws" { 23 | version = "5.23.0" 24 | constraints = "~> 5.23" 25 | hashes = [ 26 | "h1:AwjyBYctD8UKCXcm+kLJfRjYdUYzG0hetStKrw8UL9M=", 27 | "h1:jV3S2mVUT0sc3pxG6XrQLizk5epHYEFd8Eh1Wciw4Mw=", 28 | "zh:100966f25b1878b7c4ee250dcbaf09e5a2dad4bcebba2482d77c4cc4e48957da", 29 | "zh:57ed5e66949568d25788ebcd170abf5961f81bb141f69d3acca9a7454994d0c5", 30 | "zh:5acf55f8901d5443b6994463d7b2dcbb137a242486f47963e0f33c4cce30171a", 31 | "zh:7036770df1223d15e0982be39bedf32b2e2cae1eabac717138cbc90bbf94e30e", 32 | "zh:79f3f151984a97a7dee14e74ca9d9926b2add30982fe44a450645b89a6da6e00", 33 | "zh:8a1b0bc5e237609fc1ad7af17e15a95f93a56c3403c0d022d94163ac1989507c", 34 | "zh:94f3baf6a3ba728e31844d6786dae9aa505323389c6323e2eb820a3c81e82229", 35 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 36 | "zh:ac4059a4f45c77432897605efb3642451c125ddddabe14d36a4a85dad13ae6cb", 37 | "zh:d2a8d1c9a9100ae3fec34f119d3a90faefb89bf93780fc6934898533c6900cba", 38 | "zh:de647167adb585a54cfbfc4c5d204c5d0a444624d386a773eae75789aa75f363", 39 | "zh:edb533b3df81f2d1ef7387380cab843877f3f3c756f7a87cbba1961b3f01e4a2", 40 | "zh:f56491ecb31b1ebde35cbfe8261e3c82c983b3039837f8756834cf27018bd93a", 41 | "zh:fba46b50c35e40ea27947f4305320aaa61cdc22812b138571841e9bf8c7f5db9", 42 | "zh:fcb92b5c6fbb70ae9137291ffc8ef06c48daec9cf0fafb980d178fe925658160", 43 | ] 44 | } 45 | 46 | provider "registry.terraform.io/hashicorp/random" { 47 | version = "3.1.3" 48 | constraints = "~> 3.1.0" 49 | hashes = [ 50 | "h1:7+wnAXQM7IpNEAQ6WZXdO0ZfQW/ncQFXYJ5T2KaR+Z8=", 51 | "h1:LPSVX+oXKGaZmxgtaPf2USxoEsWK/pnhmm/5FKw+PtU=", 52 | "h1:nLWniS8xhb32qRQy+n4bDPjQ7YWZPVMR3v1vSrx7QyY=", 53 | "zh:26e07aa32e403303fc212a4367b4d67188ac965c37a9812e07acee1470687a73", 54 | "zh:27386f48e9c9d849fbb5a8828d461fde35e71f6b6c9fc235bc4ae8403eb9c92d", 55 | "zh:5f4edda4c94240297bbd9b83618fd362348cadf6bf24ea65ea0e1844d7ccedc0", 56 | "zh:646313a907126cd5e69f6a9fafe816e9154fccdc04541e06fed02bb3a8fa2d2e", 57 | "zh:7349692932a5d462f8dee1500ab60401594dddb94e9aa6bf6c4c0bd53e91bbb8", 58 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 59 | "zh:9034daba8d9b32b35930d168f363af04cecb153d5849a7e4a5966c97c5dc956e", 60 | "zh:bb81dfca59ef5f949ef39f19ea4f4de25479907abc28cdaa36d12ecd7c0a9699", 61 | "zh:bcf7806b99b4c248439ae02c8e21f77aff9fadbc019ce619b929eef09d1221bb", 62 | "zh:d708e14d169e61f326535dd08eecd3811cd4942555a6f8efabc37dbff9c6fc61", 63 | "zh:dc294e19a46e1cefb9e557a7b789c8dd8f319beca99b8c265181bc633dc434cc", 64 | "zh:f9d758ee53c55dc016dd736427b6b0c3c8eb4d0dbbc785b6a3579b0ffedd9e42", 65 | ] 66 | } 67 | 68 | provider "registry.terraform.io/hashicorp/local" { 69 | version = "2.4.0" 70 | hashes = [ 71 | "h1:R97FTYETo88sT2VHfMgkPU3lzCsZLunPftjSI5vfKe8=", 72 | "h1:ZUEYUmm2t4vxwzxy1BvN1wL6SDWrDxfH7pxtzX8c6d0=", 73 | "zh:53604cd29cb92538668fe09565c739358dc53ca56f9f11312b9d7de81e48fab9", 74 | "zh:66a46e9c508716a1c98efbf793092f03d50049fa4a83cd6b2251e9a06aca2acf", 75 | "zh:70a6f6a852dd83768d0778ce9817d81d4b3f073fab8fa570bff92dcb0824f732", 76 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 77 | "zh:82a803f2f484c8b766e2e9c32343e9c89b91997b9f8d2697f9f3837f62926b35", 78 | "zh:9708a4e40d6cc4b8afd1352e5186e6e1502f6ae599867c120967aebe9d90ed04", 79 | "zh:973f65ce0d67c585f4ec250c1e634c9b22d9c4288b484ee2a871d7fa1e317406", 80 | "zh:c8fa0f98f9316e4cfef082aa9b785ba16e36ff754d6aba8b456dab9500e671c6", 81 | "zh:cfa5342a5f5188b20db246c73ac823918c189468e1382cb3c48a9c0c08fc5bf7", 82 | "zh:e0e2b477c7e899c63b06b38cd8684a893d834d6d0b5e9b033cedc06dd7ffe9e2", 83 | "zh:f62d7d05ea1ee566f732505200ab38d94315a4add27947a60afa29860822d3fc", 84 | "zh:fa7ce69dde358e172bd719014ad637634bbdabc49363104f4fca759b4b73f2ce", 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordBasedSuggestions": "off" // required so that completions don't fall back to completing words if there are no results 3 | } 4 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/README.md: -------------------------------------------------------------------------------- 1 | # lambda-multi-account-stack 2 | 3 | This fixture is based on https://github.com/hashicorp-guides/lambda-multi-account-stack -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/api-gateway/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "random_pet" "api_gateway_name" { 5 | prefix = "hello-world-lambda-gw" 6 | length = 2 7 | } 8 | 9 | resource "aws_apigatewayv2_api" "lambda" { 10 | name = random_pet.api_gateway_name.id 11 | protocol_type = "HTTP" 12 | } 13 | 14 | resource "aws_apigatewayv2_stage" "lambda" { 15 | api_id = aws_apigatewayv2_api.lambda.id 16 | 17 | name = "serverless_lambda_stage" 18 | auto_deploy = true 19 | 20 | access_log_settings { 21 | destination_arn = aws_cloudwatch_log_group.api_gw.arn 22 | 23 | format = jsonencode({ 24 | requestId = "$context.requestId" 25 | sourceIp = "$context.identity.sourceIp" 26 | requestTime = "$context.requestTime" 27 | protocol = "$context.protocol" 28 | httpMethod = "$context.httpMethod" 29 | resourcePath = "$context.resourcePath" 30 | routeKey = "$context.routeKey" 31 | status = "$context.status" 32 | responseLength = "$context.responseLength" 33 | integrationErrorMessage = "$context.integrationErrorMessage" 34 | } 35 | ) 36 | } 37 | } 38 | 39 | resource "aws_apigatewayv2_integration" "hello_world" { 40 | api_id = aws_apigatewayv2_api.lambda.id 41 | 42 | integration_uri = var.lambda_invoke_arn 43 | integration_type = "AWS_PROXY" 44 | integration_method = "POST" 45 | } 46 | 47 | resource "aws_apigatewayv2_route" "hello_world" { 48 | api_id = aws_apigatewayv2_api.lambda.id 49 | 50 | route_key = "GET /hello" 51 | target = "integrations/${aws_apigatewayv2_integration.hello_world.id}" 52 | } 53 | 54 | resource "aws_cloudwatch_log_group" "api_gw" { 55 | name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}" 56 | 57 | retention_in_days = 30 58 | } 59 | 60 | resource "aws_lambda_permission" "api_gw" { 61 | statement_id = "AllowExecutionFromAPIGateway" 62 | action = "lambda:InvokeFunction" 63 | function_name = var.lambda_function_name 64 | principal = "apigateway.amazonaws.com" 65 | 66 | source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*" 67 | } 68 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/api-gateway/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "hello_url" { 5 | description = "URL for invoking our Lambda function" 6 | 7 | value = aws_apigatewayv2_stage.lambda.invoke_url 8 | } 9 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/api-gateway/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "lambda_function_name" { 5 | type = string 6 | } 7 | 8 | variable "lambda_invoke_arn" { 9 | type = string 10 | } 11 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/components.tfstack.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | component "foo" { 5 | 6 | } 7 | 8 | component "s3" { 9 | source = "./s3" 10 | 11 | inputs = { 12 | region = var.region 13 | } 14 | 15 | providers = { 16 | aws = provider.aws.this 17 | random = provider.random.this 18 | } 19 | } 20 | 21 | component "lambda" { 22 | source = "./lambda" 23 | 24 | inputs = { 25 | region = var.region 26 | bucket_id = component.s3.bucket_id 27 | } 28 | 29 | providers = { 30 | aws = provider.aws.this 31 | archive = provider.archive.this 32 | local = provider.local.this 33 | random = provider.random.this 34 | } 35 | } 36 | 37 | component "api_gateway" { 38 | source = "./api-gateway" 39 | 40 | inputs = { 41 | region = var.region 42 | lambda_function_name = component.lambda.function_name 43 | lambda_invoke_arn = component.lambda.invoke_arn 44 | } 45 | 46 | providers = { 47 | aws = provider.aws.this 48 | random = provider.random.this 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/deployments.tfdeploy.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | identity_token "aws" { 5 | audience = [""] 6 | } 7 | 8 | # A single workload token can be trusted by multiple accounts - but optionally, you can generate a 9 | # separate token with a difference audience value for your second account and use it below. 10 | # 11 | # identity_token "account_2" { 12 | # audience = [""] 13 | # } 14 | 15 | deployment "development" { 16 | inputs = { 17 | region = "us-east-1" 18 | role_arn = "" 19 | identity_token_file = identity_token.aws.jwt_filename 20 | default_tags = { stacks-preview-example = "lambda-multi-account-stack" } 21 | } 22 | } 23 | 24 | deployment "production" { 25 | inputs = { 26 | region = "us-east-1" 27 | role_arn = "" 28 | identity_token_file = identity_token.aws.jwt_filename 29 | default_tags = { stacks-preview-example = "lambda-multi-account-stack" } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/lambda/hello-world/hello.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | require 'json' 5 | 6 | module LambdaFunctions 7 | class Handler 8 | def self.process(event:,context:) 9 | nameQuery = event.dig("queryStringParameters", "name") 10 | name = nameQuery || "World" 11 | 12 | { 13 | statusCode: 200, 14 | headers: { 15 | 'Content-Type' => 'text/plain' 16 | }, 17 | body: "Hello, #{name}!", 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/lambda/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | data "archive_file" "lambda_hello_world" { 5 | type = "zip" 6 | 7 | source_dir = "${path.module}/hello-world" 8 | 9 | # HACK: We're manually utilizing the agent's tmp dir; a proper temporary file interface should be 10 | # used here instead. 11 | output_path = "${path.module}/../../../tmp/hello-world.zip" 12 | } 13 | 14 | # HACK: The tmp dir in the agent is not yet persisted between plan/apply. This hack allows us to 15 | # cheat by using the plan itself as a cache between operations. 16 | data "local_file" "lambda_hello_world" { 17 | filename = data.archive_file.lambda_hello_world.output_path 18 | } 19 | 20 | resource "aws_s3_object" "lambda_hello_world" { 21 | bucket = var.bucket_id 22 | 23 | key = "hello-world.zip" 24 | 25 | content_base64 = data.local_file.lambda_hello_world.content_base64 26 | # source = data.archive_file.lambda_hello_world.output_path 27 | 28 | etag = data.local_file.lambda_hello_world.content_md5 29 | # etag = filemd5(data.archive_file.lambda_hello_world.output_path) 30 | } 31 | 32 | resource "random_pet" "lambda_function_name" { 33 | prefix = "hello-world-lambda-changed" 34 | length = 2 35 | } 36 | 37 | resource "aws_lambda_function" "hello_world" { 38 | function_name = random_pet.lambda_function_name.id 39 | 40 | s3_bucket = var.bucket_id 41 | s3_key = aws_s3_object.lambda_hello_world.key 42 | 43 | runtime = "ruby3.2" 44 | handler = "hello.LambdaFunctions::Handler.process" 45 | 46 | source_code_hash = data.archive_file.lambda_hello_world.output_base64sha256 47 | 48 | role = aws_iam_role.lambda_exec.arn 49 | } 50 | 51 | resource "aws_cloudwatch_log_group" "hello_world" { 52 | name = "/aws/lambda/${aws_lambda_function.hello_world.function_name}" 53 | 54 | retention_in_days = 30 55 | } 56 | 57 | resource "aws_iam_role" "lambda_exec" { 58 | name = random_pet.lambda_function_name.id 59 | 60 | assume_role_policy = jsonencode({ 61 | Version = "2012-10-17" 62 | Statement = [{ 63 | Action = "sts:AssumeRole" 64 | Effect = "Allow" 65 | Sid = "" 66 | Principal = { 67 | Service = "lambda.amazonaws.com" 68 | } 69 | } 70 | ] 71 | }) 72 | } 73 | 74 | resource "aws_iam_role_policy_attachment" "lambda_policy" { 75 | role = aws_iam_role.lambda_exec.name 76 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "function_name" { 5 | description = "Name of the Lambda function." 6 | value = aws_lambda_function.hello_world.function_name 7 | } 8 | 9 | output "invoke_arn" { 10 | description = "The invocation ARN of the function" 11 | value = aws_lambda_function.hello_world.invoke_arn 12 | } 13 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/lambda/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "bucket_id" { 5 | type = string 6 | } 7 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/providers.tfstack.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.7.0" 8 | } 9 | 10 | random = { 11 | source = "hashicorp/random" 12 | version = "~> 3.5.1" 13 | } 14 | 15 | archive = { 16 | source = "hashicorp/archive" 17 | version = "~> 2.4.0" 18 | } 19 | 20 | local = { 21 | source = "hashicorp/local" 22 | version = "~> 2.4.0" 23 | } 24 | } 25 | 26 | provider "aws" "this" { 27 | config { 28 | region = var.region 29 | 30 | assume_role_with_web_identity { 31 | role_arn = var.role_arn 32 | web_identity_token_file = var.identity_token_file 33 | } 34 | 35 | default_tags { 36 | tags = var.default_tags 37 | } 38 | } 39 | } 40 | 41 | provider "random" "this" { 42 | 43 | } 44 | provider "archive" "this" {} 45 | provider "local" "this" {} 46 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/s3/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | resource "random_pet" "lambda_bucket_name" { 5 | prefix = "hello-world-lambda" 6 | length = 2 7 | } 8 | 9 | resource "aws_s3_bucket" "lambda_bucket" { 10 | bucket = random_pet.lambda_bucket_name.id 11 | } 12 | 13 | resource "aws_s3_bucket_ownership_controls" "bucket_controls" { 14 | bucket = aws_s3_bucket.lambda_bucket.id 15 | rule { 16 | object_ownership = "BucketOwnerPreferred" 17 | } 18 | } 19 | 20 | resource "aws_s3_bucket_acl" "bucket_acl" { 21 | depends_on = [aws_s3_bucket_ownership_controls.bucket_controls] 22 | 23 | bucket = aws_s3_bucket.lambda_bucket.id 24 | acl = "private" 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/s3/outputs.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | output "bucket_id" { 5 | description = "The ID of the S3 bucket to be used by a downstream component in this stack." 6 | 7 | value = aws_s3_bucket.lambda_bucket.id 8 | } 9 | -------------------------------------------------------------------------------- /src/test/integration/stacks/workspace/variables.tfstack.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "region" { 5 | type = string 6 | } 7 | 8 | variable "identity_token_file" { 9 | type = string 10 | } 11 | 12 | variable "role_arn" { 13 | type = string 14 | } 15 | 16 | variable "default_tags" { 17 | description = "A map of default tags to apply to all AWS resources" 18 | type = map(string) 19 | default = {} 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/clientHelpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as net from 'net'; 7 | import * as vscode from 'vscode'; 8 | import { Executable, InitializeResult, ServerOptions } from 'vscode-languageclient/node'; 9 | import { config } from './vscode'; 10 | import { ServerPath } from './serverPath'; 11 | 12 | export async function getServerOptions( 13 | lsPath: ServerPath, 14 | outputChannel: vscode.OutputChannel, 15 | ): Promise { 16 | let serverOptions: ServerOptions; 17 | 18 | const port = config('opentofu').get('languageServer.tcp.port'); 19 | if (port) { 20 | const inspect = vscode.workspace.getConfiguration('opentofu').inspect('languageServer.path'); 21 | if (inspect !== undefined && (inspect.globalValue || inspect.workspaceFolderValue || inspect.workspaceValue)) { 22 | vscode.window.showWarningMessage( 23 | 'You cannot use opentofu.languageServer.tcp.port with opentofu.languageServer.path. Ignoring opentofu.languageServer.path and proceeding to connect via TCP', 24 | ); 25 | } 26 | 27 | serverOptions = async () => { 28 | const socket = new net.Socket(); 29 | socket.connect({ 30 | port: port, 31 | host: 'localhost', 32 | }); 33 | return { 34 | writer: socket, 35 | reader: socket, 36 | }; 37 | }; 38 | 39 | outputChannel.appendLine(`Connecting to language server via TCP at localhost:${port}`); 40 | return serverOptions; 41 | } 42 | 43 | const cmd = await lsPath.resolvedPathToBinary(); 44 | const serverArgs = config('opentofu').get('languageServer.args', []); 45 | outputChannel.appendLine(`Launching language server: ${cmd} ${serverArgs.join(' ')}`); 46 | const executable: Executable = { 47 | command: cmd, 48 | args: serverArgs, 49 | options: {}, 50 | }; 51 | serverOptions = { 52 | run: executable, 53 | debug: executable, 54 | }; 55 | 56 | return serverOptions; 57 | } 58 | 59 | export function clientSupportsCommand(initializeResult: InitializeResult | undefined, cmdName: string): boolean { 60 | if (!initializeResult) { 61 | return false; 62 | } 63 | 64 | return initializeResult.capabilities.executeCommandProvider?.commands.includes(cmdName) ?? false; 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/serverPath.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as path from 'path'; 7 | import * as vscode from 'vscode'; 8 | import * as which from 'which'; 9 | import { config } from './vscode'; 10 | 11 | const INSTALL_FOLDER_NAME = 'bin'; 12 | 13 | export class ServerPath { 14 | private customBinPath: string | undefined; 15 | 16 | constructor(private context: vscode.ExtensionContext) { 17 | this.customBinPath = config('opentofu').get('languageServer.path'); 18 | } 19 | 20 | private installPath(): string { 21 | return path.join(this.context.extensionPath, INSTALL_FOLDER_NAME); 22 | } 23 | 24 | public hasCustomBinPath(): boolean { 25 | return !!this.customBinPath; 26 | } 27 | 28 | private binPath(): string { 29 | if (this.customBinPath) { 30 | return this.customBinPath; 31 | } 32 | 33 | return path.resolve(this.installPath(), this.binName()); 34 | } 35 | 36 | private binName(): string { 37 | if (this.customBinPath) { 38 | return path.basename(this.customBinPath); 39 | } 40 | 41 | if (process.platform === 'win32') { 42 | return 'opentofu-ls.exe'; 43 | } 44 | return 'opentofu-ls'; 45 | } 46 | 47 | public async resolvedPathToBinary(): Promise { 48 | const pathToBinary = this.binPath(); 49 | let cmd: string; 50 | try { 51 | if (path.isAbsolute(pathToBinary)) { 52 | await vscode.workspace.fs.stat(vscode.Uri.file(pathToBinary)); 53 | cmd = pathToBinary; 54 | } else { 55 | cmd = which.sync(pathToBinary); 56 | } 57 | console.log(`Found server at ${cmd}`); 58 | } catch (err) { 59 | let extraHint = ''; 60 | if (this.customBinPath) { 61 | extraHint = `. Check "opentofu.languageServer.path" in your settings.`; 62 | } 63 | throw new Error(`Unable to launch language server: ${err instanceof Error ? err.message : err}${extraHint}`); 64 | } 65 | 66 | return cmd; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/vscode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { InitializeError, ResponseError } from 'vscode-languageclient'; 8 | 9 | export function config(section: string, scope?: vscode.ConfigurationScope): vscode.WorkspaceConfiguration { 10 | return vscode.workspace.getConfiguration(section, scope); 11 | } 12 | 13 | export function getScope(section: string, settingName: string): vscode.ConfigurationTarget { 14 | let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global; 15 | 16 | // getConfiguration('terraform').inspect('languageServer'); 17 | // not getConfiguration('terraform').inspect('languageServer.external'); ! 18 | // can use when we extract settings 19 | const inspect = vscode.workspace.getConfiguration(section).inspect(settingName); 20 | if (inspect === undefined) { 21 | return target; 22 | } 23 | 24 | if (inspect.globalValue) { 25 | target = vscode.ConfigurationTarget.Global; 26 | } 27 | if (inspect.workspaceFolderValue) { 28 | target = vscode.ConfigurationTarget.WorkspaceFolder; 29 | } 30 | if (inspect.workspaceValue) { 31 | target = vscode.ConfigurationTarget.Workspace; 32 | } 33 | 34 | return target; 35 | } 36 | 37 | // getActiveTextEditor returns an active (visible and focused) TextEditor 38 | // We intentionally do *not* use vscode.window.activeTextEditor here 39 | // because it also contains Output panes which are considered editors 40 | // see also https://github.com/microsoft/vscode/issues/58869 41 | export function getActiveTextEditor(): vscode.TextEditor | undefined { 42 | return vscode.window.visibleTextEditors.find((textEditor) => !!textEditor.viewColumn); 43 | } 44 | 45 | /* 46 | Detects whether this is a Terraform file we can perform operations on 47 | */ 48 | export function isTerraformFile(document?: vscode.TextDocument): boolean { 49 | if (document === undefined) { 50 | return false; 51 | } 52 | 53 | if (document.isUntitled) { 54 | // Untitled files are files which haven't been saved yet, so we don't know if they 55 | // are terraform so we return false 56 | return false; 57 | } 58 | 59 | // TODO: check for supported language IDs here instead 60 | if (document.fileName.endsWith('tf')) { 61 | // For the purposes of this extension, anything with the tf file 62 | // extension is a Terraform file 63 | return true; 64 | } 65 | 66 | // be safe and default to false 67 | return false; 68 | } 69 | 70 | function isInitializeError(error: unknown): error is ResponseError { 71 | return (error as ResponseError).data?.retry !== undefined; 72 | } 73 | 74 | export async function handleLanguageClientStartError(error: unknown, ctx: vscode.ExtensionContext) { 75 | let message = 'Unknown Error'; 76 | if (isInitializeError(error)) { 77 | // handled in initializationFailedHandler 78 | return; 79 | } else if (error instanceof Error) { 80 | message = error.message; 81 | } else if (typeof error === 'string') { 82 | message = error; 83 | } 84 | 85 | if (message === 'INVALID_URI_WSL') { 86 | // handle in startLanguageServer() 87 | if (ctx.globalState.get('opentofu.disableWSLNotification') === true) { 88 | return; 89 | } 90 | 91 | const messageText = 92 | 'It looks like you opened a WSL url using a Windows UNC path' + 93 | ' outside of the [Remote WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl).' + 94 | ' The HashiCorp Terraform Extension works seamlessly with the Remote WSL Extension, but cannot work with this URL. Would you like to reopen this folder' + 95 | ' in the WSL Extension?'; 96 | 97 | const choice = await vscode.window.showErrorMessage( 98 | messageText, 99 | { 100 | detail: messageText, 101 | modal: false, 102 | }, 103 | { title: 'Reopen Folder in WSL' }, 104 | { title: 'More Info' }, 105 | { title: 'Supress' }, 106 | ); 107 | if (choice === undefined) { 108 | return; 109 | } 110 | 111 | switch (choice.title) { 112 | case 'Suppress': 113 | ctx.globalState.update('opentofu.disableWSLNotification', true); 114 | break; 115 | case 'Reopen Folder in WSL': 116 | await vscode.commands.executeCommand('remote-wsl.reopenInWSL'); 117 | break; 118 | case 'More Info': 119 | await vscode.commands.executeCommand( 120 | 'vscode.open', 121 | vscode.Uri.parse('https://github.com/gamunu/vscode-opentofu/blob/main/README.md#remote-extension-support'), 122 | ); 123 | } 124 | } else { 125 | await vscode.window.showErrorMessage(message); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/web/extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | const brand = `OpenTofu`; 9 | const outputChannel = vscode.window.createOutputChannel(brand); 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | context.subscriptions.push(outputChannel); 13 | 14 | outputChannel.appendLine(`Started: OpenTofu ${vscode.env.appHost}`); 15 | } 16 | 17 | export function deactivate() { 18 | outputChannel.appendLine(`Stopped: OpenTofu ${vscode.env.appHost}`); 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "opentofu.languageServer.enable": true, 3 | "editor.codeActionsOnSave": { 4 | "source.formatAll.terraform": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/actions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | # code that needs to be formatted 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/ai/main.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/test/fixtures/ai/main.tf -------------------------------------------------------------------------------- /test/fixtures/ai/variables.tf: -------------------------------------------------------------------------------- 1 | variable "agi" { 2 | default = false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/compute/main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "google_compute_network" "vpc_network" { 3 | name = "terraform-network" 4 | } 5 | 6 | resource "google_compute_instance" "vm_instance" { 7 | name = var.instance_name 8 | machine_type = var.machine_type 9 | 10 | boot_disk { 11 | initialize_params { 12 | image = "debian-cloud/debian-11" 13 | } 14 | } 15 | 16 | network_interface { 17 | network = google_compute_network.vpc_network.name 18 | access_config { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/compute/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ip" { 2 | value = google_compute_instance.vm_instance.network_interface[0].network_ip 3 | } -------------------------------------------------------------------------------- /test/fixtures/compute/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_name" { 2 | type = string 3 | description = "Name of the compute instance" 4 | } 5 | 6 | variable "machine_type" { 7 | type = string 8 | default = "f1-micro" 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/empty.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamunu/vscode-opentofu/87140bd38ad7fee6c98a30050345cea24a5b47d4/test/fixtures/empty.tf -------------------------------------------------------------------------------- /test/fixtures/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | 10 | provider "google" { 11 | credentials = file(var.credentials_file) 12 | 13 | project = var.project 14 | region = var.region 15 | zone = var.zone 16 | } 17 | 18 | module "compute" { 19 | source = "./compute" 20 | 21 | instance_name = "terraform-machine" 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/sample.tf: -------------------------------------------------------------------------------- 1 | provider "vault" { 2 | } 3 | 4 | resource "vault_auth_backend" "b" { 5 | } 6 | 7 | module "local" { 8 | source = "./modules" 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/terraform.tfvars: -------------------------------------------------------------------------------- 1 | zone = "us-central1-c" 2 | -------------------------------------------------------------------------------- /test/fixtures/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | type = string 3 | } 4 | 5 | variable "credentials_file" { 6 | type = string 7 | } 8 | 9 | variable "region" { 10 | default = "us-central1" 11 | } 12 | 13 | variable "zone" { 14 | default = "us-central1-c" 15 | } 16 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terraform-vscode-e2e", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "wdio": "wdio run wdio.conf.ts" 8 | }, 9 | "devDependencies": { 10 | "@wdio/cli": "^8.31.1", 11 | "@wdio/json-reporter": "^8.31.1", 12 | "@wdio/local-runner": "^8.31.1", 13 | "@wdio/mocha-framework": "^8.31.1", 14 | "@wdio/spec-reporter": "^8.31.1", 15 | "wdio-vscode-service": "^6.0.0", 16 | "wdio-wait-for": "^3.0.11", 17 | "webdriverio": "^8.31.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/specs/extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { browser, expect } from '@wdio/globals'; 7 | 8 | describe('VS Code Extension Testing', () => { 9 | it('should be able to load VSCode', async () => { 10 | const workbench = await browser.getWorkbench(); 11 | expect(await workbench.getTitleBar().getTitle()).toContain('[Extension Development Host]'); 12 | }); 13 | 14 | it('should load and install our VSCode Extension', async () => { 15 | const extensions = await browser.executeWorkbench((vscodeApi) => { 16 | return vscodeApi.extensions.all; 17 | }); 18 | expect(extensions.some((extension) => extension.id === 'hashicorp.terraform')).toBe(true); 19 | }); 20 | 21 | it('should show all activity bar items', async () => { 22 | const workbench = await browser.getWorkbench(); 23 | const viewControls = await workbench.getActivityBar().getViewControls(); 24 | expect(await Promise.all(viewControls.map((vc) => vc.getTitle()))).toEqual([ 25 | 'Explorer', 26 | 'Search', 27 | 'Source Control', 28 | 'Run and Debug', 29 | 'Extensions', 30 | 'HashiCorp Terraform', 31 | 'HCP Terraform', 32 | ]); 33 | }); 34 | 35 | // this does not appear to work in CI 36 | // it('should start the ls', async () => { 37 | // const workbench = await browser.getWorkbench(); 38 | // await workbench.executeCommand('workbench.panel.output.focus'); 39 | 40 | // const bottomBar = workbench.getBottomBar(); 41 | // await bottomBar.maximize(); 42 | 43 | // const outputView = await bottomBar.openOutputView(); 44 | // await outputView.wait(); 45 | // await outputView.selectChannel('HashiCorp Terraform'); 46 | // const output = await outputView.getText(); 47 | 48 | // expect(output.some((element) => element.toLowerCase().includes('dispatching next job'.toLowerCase()))).toBeTruthy(); 49 | // }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/specs/language/terraform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { StatusBar } from 'wdio-vscode-service'; 7 | import { browser, expect } from '@wdio/globals'; 8 | 9 | import path from 'node:path'; 10 | import { fileURLToPath } from 'url'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | function getTestWorkspacePath() { 16 | return path.join(__dirname, '../../../', 'testFixture'); 17 | } 18 | 19 | describe('Terraform language tests', () => { 20 | let statusBar: StatusBar; 21 | 22 | before(async () => { 23 | const workbench = await browser.getWorkbench(); 24 | statusBar = workbench.getStatusBar(); 25 | 26 | const testFile = path.join(getTestWorkspacePath(), `sample.tf`); 27 | browser.executeWorkbench((vscode, fileToOpen) => { 28 | vscode.commands.executeCommand('vscode.open', vscode.Uri.file(fileToOpen)); 29 | }, testFile); 30 | }); 31 | 32 | after(async () => { 33 | // TODO: Close the file 34 | }); 35 | 36 | it('can detect correct language', async () => { 37 | expect(await statusBar.getCurrentLanguage()).toContain('Terraform'); 38 | }); 39 | 40 | // it('can detect terraform version', async () => { 41 | // let item: WebdriverIO.Element | undefined; 42 | // await browser.waitUntil( 43 | // async () => { 44 | // const i = await statusBar.getItems(); 45 | // // console.log(i); 46 | 47 | // item = await statusBar.getItem( 48 | // 'Editor Language Status: 0.32.7, Terraform LS, next: 1.6.6, Terraform Installed, next: any, Terraform Required', 49 | // ); 50 | // }, 51 | // { timeout: 10000, timeoutMsg: 'Did not find a version' }, 52 | // ); 53 | 54 | // expect(item).toBeDefined(); 55 | // }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/specs/views/terraform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | import { browser, expect } from '@wdio/globals'; 6 | import { Workbench, CustomTreeItem, SideBarView, ViewSection, ViewControl } from 'wdio-vscode-service'; 7 | 8 | import path from 'node:path'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | export function getTestWorkspacePath() { 15 | return path.join(__dirname, '../../../', 'testFixture'); 16 | } 17 | 18 | describe('Terraform ViewContainer', function () { 19 | this.retries(3); 20 | let workbench: Workbench; 21 | 22 | before(async () => { 23 | workbench = await browser.getWorkbench(); 24 | }); 25 | 26 | after(async () => { 27 | // TODO: Close the file 28 | }); 29 | 30 | it('should have terraform viewcontainer', async () => { 31 | const viewContainers = await workbench.getActivityBar().getViewControls(); 32 | const titles = await Promise.all(viewContainers.map((vc) => vc.getTitle())); 33 | expect(titles).toContain('HashiCorp Terraform'); 34 | }); 35 | 36 | describe('in an terraform project', () => { 37 | before(async () => { 38 | const testFile = path.join(getTestWorkspacePath(), `sample.tf`); 39 | browser.executeWorkbench((vscode, fileToOpen) => { 40 | vscode.commands.executeCommand('vscode.open', vscode.Uri.file(fileToOpen)); 41 | }, testFile); 42 | }); 43 | 44 | after(async () => { 45 | // TODO: close the file 46 | }); 47 | 48 | describe('providers view', () => { 49 | let terraformViewContainer: ViewControl | undefined; 50 | let openViewContainer: SideBarView | undefined; 51 | let callSection: ViewSection | undefined; 52 | let items: CustomTreeItem[]; 53 | 54 | before(async () => { 55 | terraformViewContainer = await workbench.getActivityBar().getViewControl('HashiCorp Terraform'); 56 | await terraformViewContainer?.wait(); 57 | await terraformViewContainer?.openView(); 58 | openViewContainer = workbench.getSideBar(); 59 | }); 60 | 61 | it('should have providers view', async () => { 62 | const openViewContainerElem = await openViewContainer?.elem; 63 | const commandViewElem = await openViewContainerElem?.$$('h3[title="Providers"]'); 64 | expect(commandViewElem).toHaveLength(1); 65 | }); 66 | 67 | it('should include all providers', async () => { 68 | callSection = await openViewContainer?.getContent().getSection('PROVIDERS'); 69 | 70 | await browser.waitUntil( 71 | async () => { 72 | const provider = await callSection?.getVisibleItems(); 73 | if (!provider) { 74 | return false; 75 | } 76 | 77 | if (provider.length > 0) { 78 | items = provider as CustomTreeItem[]; 79 | return true; 80 | } 81 | }, 82 | { timeout: 10000, timeoutMsg: 'Never found any providers' }, 83 | ); 84 | 85 | const labels = await Promise.all(items.map((vi) => vi.getLabel())); 86 | expect(labels).toEqual(['-/vault']); 87 | }); 88 | }); 89 | 90 | describe('calls view', () => { 91 | let terraformViewContainer: ViewControl | undefined; 92 | let openViewContainer: SideBarView | undefined; 93 | let callSection: ViewSection | undefined; 94 | let items: CustomTreeItem[]; 95 | 96 | before(async () => { 97 | terraformViewContainer = await workbench.getActivityBar().getViewControl('HashiCorp Terraform'); 98 | await terraformViewContainer?.wait(); 99 | await terraformViewContainer?.openView(); 100 | openViewContainer = workbench.getSideBar(); 101 | }); 102 | 103 | it('should have module calls view', async () => { 104 | const openViewContainerElem = await openViewContainer?.elem; 105 | const welcomeViewElem = await openViewContainerElem?.$$('h3[title="Module Calls"]'); 106 | 107 | expect(welcomeViewElem).toHaveLength(1); 108 | }); 109 | 110 | it('should include all module calls', async () => { 111 | callSection = await openViewContainer?.getContent().getSection('MODULE CALLS'); 112 | 113 | await browser.waitUntil( 114 | async () => { 115 | const calls = await callSection?.getVisibleItems(); 116 | if (!calls) { 117 | return false; 118 | } 119 | 120 | if (calls.length > 0) { 121 | items = calls as CustomTreeItem[]; 122 | return true; 123 | } 124 | }, 125 | { timeout: 10000, timeoutMsg: 'Never found any projects' }, 126 | ); 127 | 128 | const labels = await Promise.all(items.map((vi) => vi.getLabel())); 129 | expect(labels).toEqual(['local']); 130 | }); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/specs/views/tfc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | import { browser, expect } from '@wdio/globals'; 6 | import { fail } from 'assert'; 7 | import { Workbench, SideBarView, ViewSection, ViewControl, WelcomeContentButton } from 'wdio-vscode-service'; 8 | import { Key } from 'webdriverio'; 9 | 10 | let workbench: Workbench; 11 | let terraformViewContainer: SideBarView; 12 | let callSection: ViewSection; 13 | 14 | describe('TFC ViewContainer', function () { 15 | this.retries(3); 16 | 17 | beforeEach(async () => { 18 | workbench = await browser.getWorkbench(); 19 | }); 20 | 21 | it('should have TFC viewcontainer', async () => { 22 | const viewContainers = await workbench.getActivityBar().getViewControls(); 23 | const titles = await Promise.all(viewContainers.map((vc) => vc.getTitle())); 24 | expect(titles).toContain('HashiCorp Terraform Cloud'); 25 | }); 26 | 27 | describe('not logged in', () => { 28 | let terraformViewControl: ViewControl | undefined; 29 | 30 | beforeEach(async () => { 31 | terraformViewControl = await workbench.getActivityBar().getViewControl('HashiCorp Terraform Cloud'); 32 | expect(terraformViewControl).toBeDefined(); 33 | await terraformViewControl?.wait(); 34 | await terraformViewControl?.openView(); 35 | terraformViewContainer = workbench.getSideBar(); 36 | }); 37 | 38 | it('should have workspaces view', async () => { 39 | const openViewContainerElem = await terraformViewContainer.elem; 40 | const workspaceView = await openViewContainerElem.$$('h3[title="Workspaces"]'); 41 | expect(workspaceView).toHaveLength(1); 42 | 43 | callSection = await terraformViewContainer.getContent().getSection('WORKSPACES'); 44 | 45 | const welcome = await callSection.findWelcomeContent(); 46 | 47 | const text = await welcome?.getTextSections(); 48 | expect(text).toContain('In order to use Terraform Cloud features, you need to be logged in'); 49 | }); 50 | 51 | it('should have runs view', async () => { 52 | const openViewContainerElem = await terraformViewContainer.elem; 53 | const runsView = await openViewContainerElem.$$('h3[title="Runs"]'); 54 | expect(runsView).toHaveLength(1); 55 | }); 56 | }); 57 | 58 | describe('logged in', () => { 59 | let terraformViewControl: ViewControl | undefined; 60 | 61 | beforeEach(async () => { 62 | terraformViewControl = await workbench.getActivityBar().getViewControl('HashiCorp Terraform Cloud'); 63 | expect(terraformViewControl).toBeDefined(); 64 | await terraformViewControl?.wait(); 65 | await terraformViewControl?.openView(); 66 | terraformViewContainer = workbench.getSideBar(); 67 | }); 68 | 69 | it('should login', async () => { 70 | const openViewContainerElem = await terraformViewContainer.elem; 71 | const workspaceView = await openViewContainerElem.$$('h3[title="Workspaces"]'); 72 | expect(workspaceView).toHaveLength(1); 73 | 74 | callSection = await terraformViewContainer.getContent().getSection('WORKSPACES'); 75 | 76 | const welcome = await callSection.findWelcomeContent(); 77 | 78 | const text = await welcome?.getTextSections(); 79 | expect(text).toContain('In order to use Terraform Cloud features, you need to be logged in'); 80 | 81 | const buttons = await welcome?.getButtons(); 82 | expect(buttons).toHaveLength(1); 83 | if (!buttons) { 84 | fail('No buttons found'); 85 | } 86 | 87 | let loginButton: WelcomeContentButton | undefined; 88 | for (const button of buttons) { 89 | const buttonText = await button.getTitle(); 90 | if (buttonText.toLowerCase().includes('login')) { 91 | loginButton = button; 92 | } 93 | } 94 | if (!loginButton) { 95 | fail("Couldn't find the login button"); 96 | } 97 | 98 | (await loginButton.elem).click(); 99 | 100 | // detect modal and click Allow 101 | browser.keys([Key.Enter]); 102 | 103 | // detect quickpick and select Existing user token 104 | browser.keys(['ArrowDown', Key.Enter]); 105 | 106 | // TODO: enter token in input box and hit enter 107 | 108 | // TODO: verify you are logged in 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework", "expect-webdriverio", "wdio-vscode-service"], 4 | "noEmit": true, 5 | "moduleResolution": "bundler", 6 | "allowImportingTsExtensions": true, 7 | "resolveJsonModule": true, 8 | "module": "ESNext", 9 | "target": "ES2021", 10 | "isolatedModules": true, 11 | "sourceMap": true, 12 | "noImplicitAny": false, 13 | "experimentalDecorators": true, 14 | "strict": true /* enable all strict type-checking options */, 15 | "forceConsistentCasingInFileNames": true, 16 | "composite": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "lib": ["ES2022", "WebWorker"], 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "sourceMap": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", ".vscode-test", ".vscode-test-web", "src/test/e2e"] 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | //@ts-check 7 | 8 | /* eslint-disable no-undef */ 9 | /* eslint-disable @typescript-eslint/no-var-requires */ 10 | /* eslint-disable @typescript-eslint/naming-convention */ 11 | 12 | 'use strict'; 13 | 14 | const path = require('path'); 15 | const webpack = require('webpack'); 16 | 17 | //@ts-check 18 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 19 | 20 | /** @type WebpackConfig */ 21 | const extensionConfig = { 22 | name: 'desktop', 23 | context: __dirname, 24 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 25 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 26 | 27 | entry: { 28 | extension: './src/extension.ts', 29 | }, // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 30 | output: { 31 | // the bundle is stored in the 'out' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 32 | path: path.resolve(__dirname, 'out'), 33 | filename: '[name].js', 34 | libraryTarget: 'commonjs2', 35 | }, 36 | externals: { 37 | // modules added here also need to be added in the .vscodeignore file 38 | mocha: 'commonjs mocha', // don't bundle 39 | '@vscode/test-electron': 'commonjs @vscode/test-electron', // don't bundle 40 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 41 | }, 42 | resolve: { 43 | mainFields: ['module', 'main'], 44 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 45 | extensions: ['.ts', '.js'], 46 | }, 47 | performance: { 48 | hints: false, 49 | }, 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.ts$/, 54 | exclude: /node_modules/, 55 | loader: 'esbuild-loader', 56 | }, 57 | ], 58 | }, 59 | devtool: 'nosources-source-map', 60 | infrastructureLogging: { 61 | level: 'log', // enables logging required for problem matchers 62 | }, 63 | }; 64 | 65 | /** @type WebpackConfig */ 66 | const webExtensionConfig = { 67 | name: 'web', 68 | context: __dirname, 69 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 70 | target: 'webworker', // extensions run in a webworker context 71 | entry: { 72 | extension: './src/web/extension.ts', 73 | }, 74 | output: { 75 | filename: '[name].js', 76 | path: path.join(__dirname, './out/web'), 77 | libraryTarget: 'commonjs', 78 | devtoolModuleFilenameTemplate: '../../[resource-path]', 79 | }, 80 | resolve: { 81 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 82 | extensions: ['.ts', '.js'], // support ts-files and js-files 83 | alias: { 84 | // provides alternate implementation for node module and source files 85 | }, 86 | fallback: { 87 | // Webpack 5 no longer polyfills Node.js core modules automatically. 88 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 89 | // for the list of Node.js core module polyfills. 90 | assert: require.resolve('assert'), 91 | }, 92 | }, 93 | module: { 94 | rules: [ 95 | { 96 | test: /\.ts$/, 97 | exclude: /node_modules/, 98 | loader: 'esbuild-loader', 99 | }, 100 | ], 101 | }, 102 | plugins: [ 103 | new webpack.optimize.LimitChunkCountPlugin({ 104 | maxChunks: 1, // disable chunks by default since web extensions must be a single bundle 105 | }), 106 | new webpack.ProvidePlugin({ 107 | process: 'process/browser', // provide a shim for the global `process` variable 108 | }), 109 | new webpack.WatchIgnorePlugin({ paths: [/\.*\.js/] }), 110 | ], 111 | externals: { 112 | // modules added here also need to be added in the .vscodeignore file 113 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 114 | }, 115 | performance: { 116 | hints: false, 117 | }, 118 | devtool: 'nosources-source-map', // create a source map that points to the original source file 119 | infrastructureLogging: { 120 | level: 'log', // enables logging required for problem matchers 121 | }, 122 | }; 123 | 124 | module.exports = [extensionConfig, webExtensionConfig]; 125 | --------------------------------------------------------------------------------