diff --git a/.changes/unreleased/ENHANCEMENTS-20250701-174433.yaml b/.changes/unreleased/ENHANCEMENTS-20250701-174433.yaml new file mode 100644 index 0000000000..7d59212cf7 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250701-174433.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: Add support for Terraform Search files (.tfquery.hcl). This provides block and attribute completion, hover, and diagnostics along with syntax validation for Terraform Search files. +time: 2025-07-01T17:44:33.274267+05:30 +custom: + Issue: "2062" + Repository: vscode-terraform diff --git a/assets/icons/terraform_stacks.svg b/assets/icons/terraform_feature.svg similarity index 100% rename from assets/icons/terraform_stacks.svg rename to assets/icons/terraform_feature.svg diff --git a/package.json b/package.json index 331f914ca3..3765bbe913 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "vscode": "^1.92.2" }, "langServer": { - "version": "0.36.5" + "version": "0.37.0" }, "syntax": { "version": "0.7.1" @@ -86,8 +86,8 @@ ], "configuration": "./language-configuration.json", "icon": { - "dark": "assets/icons/terraform_stacks.svg", - "light": "assets/icons/terraform_stacks.svg" + "dark": "assets/icons/terraform_feature.svg", + "light": "assets/icons/terraform_feature.svg" } }, { @@ -100,8 +100,8 @@ ], "configuration": "./language-configuration.json", "icon": { - "dark": "assets/icons/terraform_stacks.svg", - "light": "assets/icons/terraform_stacks.svg" + "dark": "assets/icons/terraform_feature.svg", + "light": "assets/icons/terraform_feature.svg" } }, { @@ -127,8 +127,8 @@ ], "configuration": "./language-configuration.json", "icon": { - "dark": "assets/icons/terraform_stacks.svg", - "light": "assets/icons/terraform_stacks.svg" + "dark": "assets/icons/terraform_feature.svg", + "light": "assets/icons/terraform_feature.svg" } }, { @@ -141,8 +141,22 @@ ], "configuration": "./language-configuration.json", "icon": { - "dark": "assets/icons/terraform_stacks.svg", - "light": "assets/icons/terraform_stacks.svg" + "dark": "assets/icons/terraform_feature.svg", + "light": "assets/icons/terraform_feature.svg" + } + }, + { + "id": "terraform-search", + "aliases": [ + "Terraform Search" + ], + "extensions": [ + ".tfquery.hcl" + ], + "configuration": "./language-configuration.json", + "icon": { + "dark": "assets/icons/terraform_feature.svg", + "light": "assets/icons/terraform_feature.svg" } }, { @@ -182,6 +196,11 @@ "language": "terraform-mock", "scopeName": "source.hcl", "path": "./syntaxes/hcl.tmGrammar.json" + }, + { + "language": "terraform-search", + "scopeName": "source.hcl", + "path": "./syntaxes/hcl.tmGrammar.json" } ], "semanticTokenTypes": [ diff --git a/src/extension.ts b/src/extension.ts index 461ead5ade..02c0105c3b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -46,6 +46,7 @@ const documentSelector: DocumentSelector = [ { scheme: 'file', language: 'terraform-deploy' }, { scheme: 'file', language: 'terraform-test' }, { scheme: 'file', language: 'terraform-mock' }, + { scheme: 'file', language: 'terraform-search' }, ]; const outputChannel = vscode.window.createOutputChannel(brand); const tfcOutputChannel = vscode.window.createOutputChannel('HCP Terraform'); @@ -94,6 +95,7 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.workspace.createFileSystemWatcher('**/*.tfdeploy.hcl'), vscode.workspace.createFileSystemWatcher('**/*.tftest.hcl'), vscode.workspace.createFileSystemWatcher('**/*.tfmock.hcl'), + vscode.workspace.createFileSystemWatcher('**/*.tfquery.hcl'), ], }, diagnosticCollectionName: 'HashiCorpTerraform', diff --git a/src/status/language.ts b/src/status/language.ts index 589da135be..b821457dad 100644 --- a/src/status/language.ts +++ b/src/status/language.ts @@ -12,6 +12,7 @@ const lsStatus = vscode.languages.createLanguageStatusItem('terraform-ls.status' { language: 'terraform-deploy' }, { language: 'terraform-test' }, { language: 'terraform-mock' }, + { language: 'terraform-search' }, ]); lsStatus.name = 'Terraform LS'; lsStatus.detail = 'Terraform LS'; diff --git a/src/test/integration/search/search.test.ts b/src/test/integration/search/search.test.ts new file mode 100644 index 0000000000..ea9f531ad8 --- /dev/null +++ b/src/test/integration/search/search.test.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import * as vscode from 'vscode'; +import { assert } from 'chai'; +import { activateExtension, getDocUri, open, testCompletion } from '../../helper'; + +suite('search (.tfquery.hcl)', () => { + suite('root', function suite() { + const docUri = getDocUri('main.tfquery.hcl'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + this.afterAll(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform-search', 'document language should be `terraform-search`'); + }); + + test('completes blocks available for search files', async () => { + const expected = [ + new vscode.CompletionItem('list', vscode.CompletionItemKind.Class), + new vscode.CompletionItem('locals', vscode.CompletionItemKind.Class), + new vscode.CompletionItem('provider', vscode.CompletionItemKind.Class), + new vscode.CompletionItem('variable', vscode.CompletionItemKind.Class), + ]; + + await testCompletion(docUri, new vscode.Position(1, 0), { + items: expected, + }); + }); + }); + + suite('list', function suite() { + const docUri = getDocUri('main.tfquery.hcl'); + + this.beforeAll(async () => { + await open(docUri); + await activateExtension(); + }); + + this.afterAll(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + this.afterEach(async () => { + // revert any changes made to the document after each test + await vscode.commands.executeCommand('workbench.action.files.revert'); + }); + + test('language is registered', async () => { + const doc = await vscode.workspace.openTextDocument(docUri); + assert.equal(doc.languageId, 'terraform-search', 'document language should be `terraform-search`'); + }); + + test('completes attributes of list block - provider', async () => { + const expected = [ + new vscode.CompletionItem('aws.this', vscode.CompletionItemKind.Variable), + new vscode.CompletionItem('azurerm.this', vscode.CompletionItemKind.Variable), + ]; + + await testCompletion(docUri, new vscode.Position(24, 21), { + items: expected, + }); + }); + + test('completes attributes of list block - number variable', async () => { + const expected = [ + new vscode.CompletionItem('count.index', vscode.CompletionItemKind.Variable), + new vscode.CompletionItem('local.number_local', vscode.CompletionItemKind.Variable), + new vscode.CompletionItem('var.number_variable', vscode.CompletionItemKind.Variable), + ]; + + await testCompletion(docUri, new vscode.Position(25, 21), { + items: expected, + }); + }); + + test('completes attributes of list block - boolean variable', async () => { + const expected = [ + new vscode.CompletionItem('false', vscode.CompletionItemKind.EnumMember), + new vscode.CompletionItem('true', vscode.CompletionItemKind.EnumMember), + new vscode.CompletionItem('var.boolean_variable', vscode.CompletionItemKind.Variable), + ]; + + await testCompletion(docUri, new vscode.Position(26, 21), { + items: expected, + }); + }); + }); +}); diff --git a/src/test/integration/search/workspace/main.tf b/src/test/integration/search/workspace/main.tf new file mode 100644 index 0000000000..b81f6e1329 --- /dev/null +++ b/src/test/integration/search/workspace/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "=3.0.0" + } + } +} diff --git a/src/test/integration/search/workspace/main.tfquery.hcl b/src/test/integration/search/workspace/main.tfquery.hcl new file mode 100644 index 0000000000..ffc9e3ae27 --- /dev/null +++ b/src/test/integration/search/workspace/main.tfquery.hcl @@ -0,0 +1,32 @@ + +provider "aws" { + alias = "this" +} + +provider "azurerm" { + alias = "this" +} + +variable "boolean_variable" { + default = true + type = bool +} + +locals { + number_local = 500 +} + +variable "number_variable" { + default = 10 + type = number +} + +list "concept_pet" "name_1" { + provider = + limit = + include_resource = + count = 10 + config { + + } +}