├── .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 |
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 |
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 |
--------------------------------------------------------------------------------