Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/document/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -172,6 +188,7 @@ export class Document {

const detected = this.detectIndentationFromContent();
this.tabSize = detected ?? fallbackTabSize;
this.indentationDetected = detected !== undefined;
}

private detectIndentationFromContent(): number | undefined {
Expand Down
2 changes: 1 addition & 1 deletion src/document/DocumentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class DocumentManager implements SettingsConfigurable, Closeable {

return {
...this.editorSettings,
tabSize: document.getTabSize(),
tabSize: document.getTabSize(this.editorSettings.detectIndentation),
};
}

Expand Down
92 changes: 91 additions & 1 deletion tst/e2e/Completion.yaml.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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: {
Expand Down
80 changes: 80 additions & 0 deletions tst/unit/document/Document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading