diff --git a/src/document/Document.ts b/src/document/Document.ts index c06bb73e..9a0d87f3 100644 --- a/src/document/Document.ts +++ b/src/document/Document.ts @@ -13,6 +13,7 @@ export class Document { private _cfnFileType: CloudFormationFileType; public readonly fileName: string; private tabSize: number; + private indentationDetected: boolean = false; private cachedParsedContent: unknown; constructor( @@ -160,10 +161,25 @@ export class Document { }; } - public getTabSize() { + public getTabSize(detectIndentation: boolean = true) { + if (detectIndentation) { + this.refreshIndentationIfNeeded(); + } return this.tabSize; } + private refreshIndentationIfNeeded(): void { + if (this.indentationDetected) { + return; + } + + const detected = this.detectIndentationFromContent(); + if (detected !== undefined) { + this.tabSize = detected; + this.indentationDetected = true; + } + } + public processIndentation(detectIndentation: boolean, fallbackTabSize: number) { if (!detectIndentation) { this.tabSize = fallbackTabSize; @@ -172,6 +188,7 @@ export class Document { const detected = this.detectIndentationFromContent(); this.tabSize = detected ?? fallbackTabSize; + this.indentationDetected = detected !== undefined; } private detectIndentationFromContent(): number | undefined { diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index 4d3255ec..bae65e1d 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -118,7 +118,7 @@ export class DocumentManager implements SettingsConfigurable, Closeable { return { ...this.editorSettings, - tabSize: document.getTabSize(), + tabSize: document.getTabSize(this.editorSettings.detectIndentation), }; } diff --git a/tst/e2e/Completion.yaml.test.ts b/tst/e2e/Completion.yaml.test.ts index dacba112..847dbd5d 100644 --- a/tst/e2e/Completion.yaml.test.ts +++ b/tst/e2e/Completion.yaml.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test, afterAll } from 'vitest'; -import { CompletionList } from 'vscode-languageserver'; +import { CompletionList, InsertTextFormat } from 'vscode-languageserver'; import { TestExtension } from '../utils/TestExtension'; import { WaitFor } from '../utils/Utils'; @@ -15,6 +15,96 @@ describe('Completion Tests', () => { await extension.close(); }); + describe('Indentation Detection', () => { + test('should use user-typed 2-space indentation in snippets when file starts empty', async () => { + // Start with empty file (simulates user opening new file) + await extension.openDocument({ + textDocument: { + uri: documentUri, + languageId: 'yaml', + version: 1, + text: '', + }, + }); + + // User types content with 2-space indentation + await extension.changeDocument({ + textDocument: { uri: documentUri, version: 2 }, + contentChanges: [ + { + text: `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + `, + }, + ], + }); + + await WaitFor.waitFor(async () => { + const completions = await extension.completion({ + textDocument: { uri: documentUri }, + position: { line: 4, character: 4 }, + context: { triggerKind: 2 }, + }); + + const completionList = completions as CompletionList; + const propertiesCompletion = completionList.items.find( + (item) => item.label === 'Properties' && item.insertTextFormat === InsertTextFormat.Snippet, + ); + + expect(propertiesCompletion).toBeDefined(); + // Snippet should use 2-space indentation (detected from user's typing) + const insertText = propertiesCompletion?.insertText ?? ''; + expect(insertText).toContain('\n '); // 2-space indent + expect(insertText).not.toContain('\n '); // Not 4-space + }); + }); + + test('should use 4-space indentation when user types with 4 spaces', async () => { + await extension.openDocument({ + textDocument: { + uri: documentUri, + languageId: 'yaml', + version: 1, + text: '', + }, + }); + + // User types with 4-space indentation + await extension.changeDocument({ + textDocument: { uri: documentUri, version: 2 }, + contentChanges: [ + { + text: `AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket + `, + }, + ], + }); + + await WaitFor.waitFor(async () => { + const completions = await extension.completion({ + textDocument: { uri: documentUri }, + position: { line: 4, character: 8 }, + context: { triggerKind: 2 }, + }); + + const completionList = completions as CompletionList; + const propertiesCompletion = completionList.items.find( + (item) => item.label === 'Properties' && item.insertTextFormat === InsertTextFormat.Snippet, + ); + + expect(propertiesCompletion).toBeDefined(); + // Snippet should use 4-space indentation + const insertText = propertiesCompletion?.insertText ?? ''; + expect(insertText).toContain('\n '); // 4-space indent + }); + }); + }); + test('should provide context-based completions for non-empty file', async () => { await extension.openDocument({ textDocument: { diff --git a/tst/unit/document/Document.test.ts b/tst/unit/document/Document.test.ts index 461e6345..e553b762 100644 --- a/tst/unit/document/Document.test.ts +++ b/tst/unit/document/Document.test.ts @@ -321,4 +321,84 @@ describe('Document', () => { }); }); }); + + describe('indentation detection', () => { + it('should use fallback tabSize for empty file', () => { + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, ''); + const doc = new Document(textDocument, true, 4); + + expect(doc.getTabSize(true)).toBe(4); + }); + + it('should detect 2-space indentation from content', () => { + const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); + const doc = new Document(textDocument, true, 4); + + expect(doc.getTabSize(true)).toBe(2); + }); + + it('should detect 4-space indentation from content', () => { + const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); + const doc = new Document(textDocument, true, 2); + + expect(doc.getTabSize(true)).toBe(4); + }); + + it('should use fallback when detectIndentation is disabled', () => { + const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); + const doc = new Document(textDocument, false, 4); + + expect(doc.getTabSize(false)).toBe(4); + }); + + it('should lazily detect indentation when content is added after creation', () => { + // Start with empty content + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, ''); + const doc = new Document(textDocument, true, 4); + + // Initially uses fallback + expect(doc.getTabSize(true)).toBe(4); + + // Simulate user typing with 2-space indentation + Object.defineProperty(textDocument, 'getText', { + value: () => 'Resources:\n Bucket:', + }); + + // Should now detect 2-space indentation + expect(doc.getTabSize(true)).toBe(2); + }); + + it('should not re-detect once indentation is detected', () => { + const content = 'Resources:\n Bucket:'; + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); + const doc = new Document(textDocument, true, 4); + + // First call detects 2-space + expect(doc.getTabSize(true)).toBe(2); + + // Simulate content change to 4-space indentation + Object.defineProperty(textDocument, 'getText', { + value: () => 'Resources:\n Bucket:', + }); + + // Should still return 2 (already detected) + expect(doc.getTabSize(true)).toBe(2); + }); + + it('should reset detection when processIndentation is called with detectIndentation=false', () => { + const content = 'Resources:\n Bucket:'; + const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); + const doc = new Document(textDocument, true, 4); + + expect(doc.getTabSize(true)).toBe(2); + + // Reset with detectIndentation disabled + doc.processIndentation(false, 8); + + expect(doc.getTabSize(false)).toBe(8); + }); + }); });