Skip to content

Commit 2f95aa5

Browse files
authored
Merge pull request #56 from lumaxis/feature/convert-tabs-to-spaces
New: Allow to convert tabs to spaces when copying a snippet
2 parents 3f38e6e + ec8cfb8 commit 2f95aa5

File tree

6 files changed

+96
-29
lines changed

6 files changed

+96
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Add `markdownCodeBlock.includeLanguageIdentifier` configuration option for Markdown block behavior. **This replaces `snippet-copy.addLanguageIdentifierToMarkdownBlock`**.
1212
- Allows to set whether Markdown blocks should always include the [language identifier](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) which allows for syntax highlighting in some tools (for example github.com) but is not compatible with others (for example Slack).
1313
- The default is "`prompt`" which makes the extension always prompt when using the "Copy Snippet as Markdown Code Block" command.
14+
- Add configuration setting to convert tabs to spaces when copying snippet. Additionally allows to configure the tab size for the conversion.
1415

1516
## [0.2.3]
1617

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@
5656
"Copy a fenced Markdown code block that includes the language identifier of the current document. \nThis enables [automatic syntax highlighting](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting) on e.g. GitHub or StackOverflow but isn't compatible with some apps, for example Slack.",
5757
"Always prompt when copying a snippet as a Markdown code block whether to include the language identifier or not."
5858
]
59+
},
60+
"snippet-copy.convertTabsToSpaces.enabled": {
61+
"type": "boolean",
62+
"default": "true",
63+
"description": "Convert tabs in a snippet to spaces"
64+
},
65+
"snippet-copy.convertTabsToSpaces.tabSize": {
66+
"type": "integer",
67+
"default": 2,
68+
"description": "How many spaces to replace a tab with"
5969
}
6070
}
6171
},

src/lib/textHelpers.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ type MarkdownCodeBlockFlavorQuickPickItems = QuickPickItem & {
88
};
99

1010
export async function generateSnippet(document: TextDocument, selections: Selection[], wrapInMarkdownCodeBlock = false): Promise<string> {
11+
const config = workspace.getConfiguration('snippet-copy') as ExtensionConfig;
1112
const texts: string[] = [];
1213
selections.forEach(selection => {
13-
texts.push(generateCopyableText(document, selection));
14+
texts.push(generateCopyableText(document, selection, config));
1415
});
1516

1617
const snippet = texts.join(endOfLineCharacter(document));
@@ -22,7 +23,7 @@ export async function generateSnippet(document: TextDocument, selections: Select
2223
return snippet;
2324
}
2425

25-
export function generateCopyableText(document: TextDocument, selection: Selection): string {
26+
export function generateCopyableText(document: TextDocument, selection: Selection, config: ExtensionConfig): string {
2627
const lineIndexes = lineIndexesForSelection(selection);
2728

2829
// Remove last line's index if there's no selected text on that line
@@ -33,7 +34,11 @@ export function generateCopyableText(document: TextDocument, selection: Selectio
3334
const minimumIndentation = minimumIndentationForLineIndexes(document, lineIndexes);
3435
const text = contentOfLinesWithAdjustedIndentation(document, lineIndexes, minimumIndentation);
3536

36-
return text;
37+
if (!config.convertTabsToSpaces.enabled) {
38+
return text;
39+
}
40+
41+
return replaceLeadingTabsWithSpaces(text, config.convertTabsToSpaces.tabSize);
3742
}
3843

3944
export function wrapTextInMarkdownCodeBlock(document: TextDocument, text: string, addLanguageId = false): string {
@@ -59,6 +64,12 @@ export async function includeLanguageIdentifier(config: ExtensionConfig): Promis
5964
return includeLanguageIdentifier === 'always';
6065
}
6166

67+
export function replaceLeadingTabsWithSpaces(text: string, tabSize = 2): string {
68+
const spaces = ' '.repeat(tabSize);
69+
70+
return text.replace(/^\t+/gm, (tabs) => tabs.replace(/\t/g, spaces));
71+
}
72+
6273
export async function promptForMarkdownCodeBlockFlavor(): Promise<MarkdownCodeBlockFlavorQuickPickItems | undefined> {
6374
const quickPickItems: MarkdownCodeBlockFlavorQuickPickItems[] = [
6475
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import logging
2+
3+
def fizzBuzz
4+
logging.info(" FizzBuzz")

src/test/suite/lib/textHelpers.test.ts

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,86 +3,107 @@ import * as path from 'path';
33
import * as td from 'testdouble';
44
import * as vscode from 'vscode';
55
import { Position, Selection, TextDocument } from 'vscode';
6-
import { generateCopyableText, generateSnippet, includeLanguageIdentifier, isMarkdownCodeBlockFlavor, wrapTextInMarkdownCodeBlock } from '../../../lib/textHelpers';
6+
import { generateCopyableText, generateSnippet, includeLanguageIdentifier, isMarkdownCodeBlockFlavor, replaceLeadingTabsWithSpaces, wrapTextInMarkdownCodeBlock } from '../../../lib/textHelpers';
77
import { ExtensionConfig } from '../../../types/config';
88

99
const fixturesPath = '/../../../../src/test/fixtures/';
10-
const uri = vscode.Uri.file(
11-
path.join(__dirname + fixturesPath + 'javascript-example.js')
10+
const uri = (fileName: string) => vscode.Uri.file(
11+
path.join(__dirname + fixturesPath + fileName)
1212
);
1313

1414
interface TestSelection {
1515
selection: Selection;
16-
content: string;
16+
expectedResult: string;
1717
}
1818

1919
describe('Text Helpers', () => {
20-
const testSelection1: TestSelection = {
21-
content: 'if (aValue) {\n console.log(`Doing something with ${aValue}!`);\n}',
20+
const javaScriptTestSelection1: TestSelection = {
21+
expectedResult: 'if (aValue) {\n console.log(`Doing something with ${aValue}!`);\n}',
2222
selection: new Selection(2, 4, 4, 5)
2323
};
24-
const testSelection2: TestSelection = {
25-
content: 'doSomethingElse() {\n throw new Error(\'Nope!\');\n}',
24+
const javaScriptTestSelection2: TestSelection = {
25+
expectedResult: 'doSomethingElse() {\n throw new Error(\'Nope!\');\n}',
2626
selection: new Selection(7, 2, 9, 3)
2727
};
28-
const testSelection3: TestSelection = {
29-
content: '}\n\ndoSomethingElse() {',
28+
const javaScriptTestSelection3: TestSelection = {
29+
expectedResult: '}\n\ndoSomethingElse() {',
3030
selection: new Selection(5, 0, 7, 21)
3131
};
32-
let document: TextDocument;
32+
const pythonTestSelection1: TestSelection = {
33+
expectedResult: 'def fizzBuzz\n logging.info(" FizzBuzz")',
34+
selection: new Selection(2, 1, 3, 27)
35+
};
36+
let document1: TextDocument;
37+
let document2: TextDocument;
3338

3439
before(async () => {
35-
document = await vscode.workspace.openTextDocument(uri);
40+
document1 = await vscode.workspace.openTextDocument(uri('javascript-example.js'));
41+
document2 = await vscode.workspace.openTextDocument(uri('tabs-python-example.py'));
3642
});
3743

3844
context('generateSnippet', () => {
3945
it('generates the correct snippet for a single selection', async () => {
40-
assert.deepEqual(testSelection1.content, await generateSnippet(document, [testSelection1.selection]));
46+
assert.deepEqual(javaScriptTestSelection1.expectedResult, await generateSnippet(document1, [javaScriptTestSelection1.selection]));
4147
});
4248

4349
it('generates the correct snippet for multiple selections', async () => {
44-
assert.deepEqual(testSelection1.content + '\n' + testSelection2.content,
45-
await generateSnippet(document, [testSelection1.selection, testSelection2.selection])
50+
assert.deepEqual(javaScriptTestSelection1.expectedResult + '\n' + javaScriptTestSelection2.expectedResult,
51+
await generateSnippet(document1, [javaScriptTestSelection1.selection, javaScriptTestSelection2.selection])
4652
);
4753
});
4854

4955
it('generates the correct snippet for multiple selections where one ends on the beginning of a newline', async () => {
50-
assert.deepEqual(testSelection1.content + '\n' + testSelection2.content,
51-
await generateSnippet(document, [
52-
new Selection(testSelection1.selection.start, new Position(5, 0)),
53-
testSelection2.selection
56+
assert.deepEqual(javaScriptTestSelection1.expectedResult + '\n' + javaScriptTestSelection2.expectedResult,
57+
await generateSnippet(document1, [
58+
new Selection(javaScriptTestSelection1.selection.start, new Position(5, 0)),
59+
javaScriptTestSelection2.selection
5460
])
5561
);
5662
});
5763
});
5864

5965
context('generateCopyableText', () => {
6066
it('generates the correct text', () => {
61-
assert.deepEqual(testSelection1.content, generateCopyableText(document, testSelection1.selection));
67+
const config: unknown = td.object({ convertTabsToSpaces: { enabled: false, tabSize: 2 } });
68+
69+
assert.deepEqual(generateCopyableText(document1, javaScriptTestSelection1.selection, config as ExtensionConfig),
70+
javaScriptTestSelection1.expectedResult);
6271
});
6372

6473
it('generates the correct text when the cursor is on a newline', () => {
65-
assert.deepEqual(testSelection1.content,
66-
generateCopyableText(document, new Selection(testSelection1.selection.start, new Position(5, 0)))
74+
const config: unknown = td.object({ convertTabsToSpaces: { enabled: false, tabSize: 2 } });
75+
76+
assert.deepEqual(generateCopyableText(document1, new Selection(javaScriptTestSelection1.selection.start, new Position(5, 0)), config as ExtensionConfig),
77+
javaScriptTestSelection1.expectedResult
6778
);
6879
});
6980

7081
it('generates the correct text when selection contains empty line', () => {
71-
assert.deepEqual(testSelection3.content,
72-
generateCopyableText(document, testSelection3.selection)
82+
const config: unknown = td.object({ convertTabsToSpaces: { enabled: false, tabSize: 2 } });
83+
84+
assert.deepEqual(generateCopyableText(document1, javaScriptTestSelection3.selection, config as ExtensionConfig),
85+
javaScriptTestSelection3.expectedResult
86+
);
87+
});
88+
89+
it('generates the correct text when selection contains tabs and replacement is enabled', () => {
90+
const config: unknown = td.object({ convertTabsToSpaces: { enabled: true, tabSize: 2 } });
91+
92+
assert.deepEqual(generateCopyableText(document2, pythonTestSelection1.selection, config as ExtensionConfig),
93+
pythonTestSelection1.expectedResult
7394
);
7495
});
7596
});
7697

7798
context('wrapTextInMarkdownCodeBlock', () => {
7899
it('returns the text wrapped in a Markdown code block', () => {
79100
const codeSnippet = 'console.log("Yo");';
80-
assert.equal(wrapTextInMarkdownCodeBlock(document, codeSnippet), '```\n' + codeSnippet + '\n```');
101+
assert.equal(wrapTextInMarkdownCodeBlock(document1, codeSnippet), '```\n' + codeSnippet + '\n```');
81102
});
82103

83104
it('returns the wrapped text with a language identifier', () => {
84105
const codeSnippet = 'console.log("Yo");';
85-
assert.equal(wrapTextInMarkdownCodeBlock(document, codeSnippet, true), '```javascript\n' + codeSnippet + '\n```');
106+
assert.equal(wrapTextInMarkdownCodeBlock(document1, codeSnippet, true), '```javascript\n' + codeSnippet + '\n```');
86107
});
87108
});
88109

@@ -106,6 +127,22 @@ describe('Text Helpers', () => {
106127
});
107128
});
108129

130+
context('replaceLeadingTabsWithSpaces', () => {
131+
it('returns the correct text with the default tabSize', () => {
132+
assert.equal(replaceLeadingTabsWithSpaces(' '), ' ');
133+
});
134+
135+
it('returns the correct text with the custom tabSize', () => {
136+
assert.equal(replaceLeadingTabsWithSpaces(' ', 3), ' ');
137+
});
138+
139+
it('correctly replaces in a multiline string but does not replace tabs in the middle of the string', () => {
140+
assert.equal(replaceLeadingTabsWithSpaces(` console.log(" Hello")
141+
console.log("World")`), ` console.log(" Hello")
142+
console.log("World")`);
143+
});
144+
});
145+
109146
context('isMarkdownCodeBlockFlavor', () => {
110147
it('returns true if value is "never"', () => {
111148
assert.equal(isMarkdownCodeBlockFlavor('never'), true);

src/types/config.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ export type ExtensionConfig = WorkspaceConfiguration & {
77
includeLanguageIdentifier: IncludeLanguageIdentifier | 'prompt'
88
};
99
addLanguageIdentifierToMarkdownBlock?: boolean;
10+
convertTabsToSpaces: {
11+
enabled: boolean;
12+
tabSize: number;
13+
}
1014
};

0 commit comments

Comments
 (0)