diff --git a/README.md b/README.md index b8f15cd..251b229 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Build Status](https://travis-ci.org/Microsoft/vscode-htmltagwrap.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-htmltagwrap) ## What is it -Wraps your selection in HTML tags. Can wrap an inline selection or a selection of multiple lines. +Wraps your selection in HTML tags. Can wrap an inline selection, a selection of multiple lines or mutiple selections. -To use, select a chunk of code and press **"Alt + W" ("Option + W" for Mac).** +To use, select one or many chunks of code and press **"Alt + W" ("Option + W" for Mac).** ![Wrap text in your images](images/screenshot.gif) @@ -19,10 +19,12 @@ This extension works best in files that either use tabs or spaces for indentatio I welcome pull requests. Please report an issue on GitHub if you have trouble. ## Updates +### 0.0.4 +* Support for multiple selections + ### 0.0.3 * Spaces and tabs for indentation are now both supported. - Special thanks to @gdziadkiewicz for his PR #4, which also introduced tests for development! ## Future features being explored - #5 Once a user hits spacebar, then the second multi-cursor is lost so the user can write attributes like classes or styles. (Idea courtesy of Ruben Rios) -- Should we support `getSelections()` allowing multiple selections to be wrapped? diff --git a/package.json b/package.json index 1088711..007c152 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "url" : "https://github.com/Microsoft/vscode-htmltagwrap" }, "description": "Wraps selected code with HTML tags", - "version": "0.0.3", + "version": "0.0.4", "publisher": "bradgashler", "engines": { "vscode": "^1.0.0" diff --git a/src/extension.ts b/src/extension.ts index d9a413c..dcaa4d0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,7 +18,7 @@ export function activate() { // Use the console to output diagnostic information (console.log) and errors (console.error) // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "copythis" is now active!'); + console.log('Congratulations, your extension "htmlTagWrap" is now active!'); // The command has been defined in the package.json file // Now provide the implementation of the command with registerCommand @@ -26,38 +26,31 @@ export function activate() { vscode.commands.registerCommand('extension.htmlTagWrap', () => { // The code you place here will be executed every time your command is executed - var editor = vscode.window.activeTextEditor; - if (editor != undefined) { + const editor = vscode.window.activeTextEditor; + let tabSizeSpace = getTabString(editor); - var selection = editor.selection; - var selectedText = editor.document.getText(selection); + if(editor == null) { + return; + } + + editor.edit((editBuilder) => { - var firstIndex = 1; - var lastIndex = selectedText.length; + const selections = editor.selections; + + for(const selection of selections) { - console.log('selection is: ' + selectedText); - console.log('length is: ' + lastIndex); - console.log('selection.start.character: ' + selection.start.character); - console.log('selection.end.character: ' + selection.end.character); + const selectionStart = selection.start; + const selectionEnd = selection.end; + + if(selectionEnd.line !== selectionStart.line) { + // Wrap it as a block + var selectionStart_spaces = editor.document.lineAt(selectionStart.line).text.substring(0, selectionStart.character); + //console.log('selectionStart_spaces = ' + selectionStart_spaces); + //console.log('tabsizeSpace =' + tabSizeSpace); - var selectionStart = selection.start; - var selectionEnd = selection.end; - - if (selectionEnd.line > selectionStart.line) { - //Wrap it as a block - var lineAbove = selectionStart.line - 1; - var lineBelow = selectionEnd.line + 1; - - //console.log('tabSize = ' + tabSize); - let tabSizeSpace = getTabString(editor); - var selectionStart_spaces = editor.document.lineAt(selectionStart.line).text.substring(0, selectionStart.character); - //console.log('selectionStart_spaces = ' + selectionStart_spaces); - //console.log('tabsizeSpace =' + tabSizeSpace); - - editor.edit((editBuilder) => { - // Modify last line of selection editBuilder.insert(new vscode.Position(selectionEnd.line, selectionEnd.character), '\n' + selectionStart_spaces + '

'); editBuilder.insert(new vscode.Position(selectionEnd.line, 0), tabSizeSpace); + console.log('End line done. Line #: ' + selectionEnd.line); for (let lineNumber = selectionEnd.line - 1; lineNumber > selectionStart.line; lineNumber--) { @@ -65,41 +58,57 @@ export function activate() { editBuilder.insert(new vscode.Position(lineNumber, 0), tabSizeSpace); } - // Modify firs line of selection + // Modify first line of selection editBuilder.insert(new vscode.Position(selectionStart.line, selectionStart.character), '

\n' + selectionStart_spaces + tabSizeSpace); console.log('Start Line done. Line #: ' + selectionStart.line); - }).then(() => { - console.log('Edit applied!'); - - var bottomTagLine = lineBelow + 1; - var firstTagSelectionSelection: vscode.Selection = new vscode.Selection(selectionStart.line, selectionStart.character + 1, selectionStart.line, selectionStart.character + 2); - var lastTagSelectionSelection: vscode.Selection = new vscode.Selection(bottomTagLine, selectionStart.character + 2, bottomTagLine, selectionStart.character + 3); - var tagSelections: vscode.Selection[] = [firstTagSelectionSelection, lastTagSelectionSelection]; - - editor.selections = tagSelections; - }, (err) => { - console.log('Edit rejected!'); - console.error(err); - }); + } + else { + //Wrap it inline + editBuilder.insert(new vscode.Position(selectionEnd.line, selectionStart.character), '

'); + editBuilder.insert(new vscode.Position(selectionEnd.line, selectionEnd.character), '

'); + } } - else { - //Wrap it inline - editor.edit((editBuilder) => { - editBuilder.insert(new vscode.Position(selectionEnd.line, selectionEnd.character), '

'); - editBuilder.insert(new vscode.Position(selectionEnd.line, selectionStart.character), '

'); - }).then(() => { - console.log('Edit applied!'); - - var firstTagSelectionSelection: vscode.Selection = new vscode.Selection(selectionStart.line, selectionStart.character + 1, selectionStart.line, selectionStart.character + 2); - var lastTagSelectionSelection: vscode.Selection = new vscode.Selection(selectionEnd.line, selectionEnd.character + 3 + 2, selectionEnd.line, selectionEnd.character + 3 + 3); - var tagSelections: vscode.Selection[] = [firstTagSelectionSelection, lastTagSelectionSelection]; - - editor.selections = tagSelections; - }, (err) => { - console.log('Edit rejected!'); - console.error(err); - }); + }).then(() => { + console.log('Edit applied!'); + + // Need to fetch selections again as they are no longer accurate (since the new tags were inserted) + const selections = editor.selections; + const toSelect: Array = new Array(); + + for(let selection of selections) { + + // Careful : the selection starts at the beginning of the text but ends *after* the closing

tag + if(selection.end.line !== selection.start.line) { + // block + let lineAbove = selection.start.line - 1; + let lineBelow = selection.end.line; + let startPosition = selection.start.character - tabSizeSpace.length + 1; + let endPosition = selection.end.character - 2; + + toSelect.push(new vscode.Selection(lineAbove, startPosition, lineAbove, startPosition + 1)); + toSelect.push(new vscode.Selection(lineBelow, endPosition, lineBelow, endPosition + 1)); + } + else { + // same line, just get to the p by navigating backwards + let startPosition = selection.start.character - 2; + let endPosition = selection.end.character - 2; + + // When dealing with an empty selection, both the start and end position end up being *after* the closing

tag + // backtrack to account for that + if(selection.start.character === selection.end.character) { + startPosition -= 4; + } + + toSelect.push(new vscode.Selection(selection.start.line, startPosition, selection.start.line, startPosition + 1)) + toSelect.push(new vscode.Selection(selection.end.line, endPosition, selection.end.line, endPosition + 1)) + } + + editor.selections = toSelect; } - }; + + }, (err) => { + console.log('Edit rejected!'); + console.error(err); + }); }); } \ No newline at end of file diff --git a/test/extension.test.ts b/test/extension.test.ts index 18a1470..97819db 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -8,12 +8,22 @@ import {workspace, window, Selection, Position, commands, extensions, Uri, TextE import {copySync, CopyOptions, emptyDir} from 'fs-extra'; import * as extension from '../src/extension'; +// A cursor selection is a StartPosition : EndPosition couple +type CursorSelection = [Position, Position]; + extension.activate(); let extensionID = 'bradgashler.htmltagwrap'; let samplesFolder = extensions.getExtension(extensionID).extensionPath + '/test/sampleFiles/'; let tempFolder = samplesFolder + 'temp/'; -function parametrizedTest(startFilePath: string, expectedResultFilePath: string, selectionStart: Position, selectionEnd: Position, failMessage: string) { +function parametrizedSingleSelectionTest(startFilePath: string, expectedResultFilePath: string, selectionStart: Position, selectionEnd: Position, failMessage: string) { + const selection:CursorSelection = [selectionStart, selectionEnd]; + const selections: Array = [selection]; + + return parametrizedMultiSelectionTest(startFilePath, expectedResultFilePath, selections, failMessage); +} + +function parametrizedMultiSelectionTest(startFilePath: string, expectedResultFilePath: string, selections: Array, failMessage: string) { let result: string; let expectedResult: string; let editor: TextEditor; @@ -25,7 +35,7 @@ function parametrizedTest(startFilePath: string, expectedResultFilePath: string, }).then((_editor) => { editor = _editor; }).then(() => { - editor.selection = new Selection(selectionStart, selectionEnd); + editor.selections = selections.map(s => new Selection(s[0], s[1])); return commands.executeCommand('extension.htmlTagWrap').then(() => new Promise((f) => setTimeout(f, 500))); }).then(() => { result = editor.document.getText(); @@ -46,17 +56,63 @@ function parametrizedTest(startFilePath: string, expectedResultFilePath: string, suite('Extension Tests', function () { + + // Single selection tests test('HTML with tabs block wrap test', function () { - return parametrizedTest('tabFile.html', 'expectedTabBlockWrapFileResult.html', new Position(1, 1), new Position(6, 6), 'Tab using block wrap does not work'); + return parametrizedSingleSelectionTest('tabFile.html', 'expectedTabBlockWrapFileResult.html', new Position(1, 1), new Position(6, 6), 'Tab using block wrap does not work'); }); test('HTML with spaces block wrap test', function () { - return parametrizedTest('spaceFile.html', 'expectedSpaceBlockWrapFileResult.html', new Position(1, 4), new Position(7, 9), 'Space using block wrap does not work'); + return parametrizedSingleSelectionTest('spaceFile.html', 'expectedSpaceBlockWrapFileResult.html', new Position(1, 4), new Position(7, 9), 'Space using block wrap does not work'); }); test('HTML with tabs line wrap test', function () { - return parametrizedTest('tabFile.html', 'expectedTabLineWrapFileResult.html', new Position(2, 2), new Position(2, 11), 'Tab using line wrap does not work'); + return parametrizedSingleSelectionTest('tabFile.html', 'expectedTabLineWrapFileResult.html', new Position(2, 2), new Position(2, 11), 'Tab using line wrap does not work'); }); test('HTML with spaces line wrap test', function () { - return parametrizedTest('spaceFile.html', 'expectedSpaceLineWrapFileResult.html', new Position(2, 8), new Position(2, 17), 'Space using line wrap does not work'); + return parametrizedSingleSelectionTest('spaceFile.html', 'expectedSpaceLineWrapFileResult.html', new Position(2, 8), new Position(2, 17), 'Space using line wrap does not work'); }); + test('Empty selection line wrap test', function() { + return parametrizedSingleSelectionTest('emptyFile.html', 'expectedEmptyFileSingleCursorResult.html', new Position(0, 0), new Position(0, 0), 'Empty selection tag wrap does not work'); + }); + + // Multiple selecetion tests + test('Multiple Empty selections line wrap test', function() { + const selections: Array = [ + [new Position(1, 0), new Position(1, 0)], + [new Position(2, 0), new Position(2, 0)], + [new Position(3, 0), new Position(3, 0)] + ]; + return parametrizedMultiSelectionTest('emptySelectionMultipleCursors.html', 'expectedEmptySelectionMultipleCursorsResult.html', selections, 'Empty selection tag wrap does not work with multiple selections'); + }); + + test('Multiple selections block wrap test', function() { + const selections: Array = [ + [new Position(1, 4), new Position(2, 17)], + [new Position(5, 0), new Position(6, 13)], + [new Position(10, 8), new Position(11, 15)] + ]; + return parametrizedMultiSelectionTest('textBlocks.html', 'expectedMultiSelectionTextBlocksFileResult.html', selections, 'Multiple selections text block wrap does not work'); + }); + + test('Multiple selections block wrap test', function() { + const selections: Array = [ + [new Position(1, 4), new Position(2, 17)], + [new Position(5, 0), new Position(6, 13)], + [new Position(10, 8), new Position(11, 15)] + ]; + return parametrizedMultiSelectionTest('textBlocks.html', 'expectedMultiSelectionTextBlocksFileResult.html', selections, 'Multiple selections text block wrap does not work'); + }); + + test('Multiple selections mix block / text wrap test', function() { + const selections: Array = [ + [new Position(1, 4), new Position(1, 21)], + [new Position(2, 4), new Position(2, 17)], + [new Position(5, 0), new Position(6, 13)], + [new Position(10, 8), new Position(10, 19)], + [new Position(11, 11), new Position(11, 15)] + ]; + return parametrizedMultiSelectionTest('textBlocks.html', 'expectedMultiSelectionMixedLineBlockFileResult.html', selections, 'Multiple selections mixed (text and block) does not work'); + }); + + teardown((done) => emptyDir(tempFolder, done)); }); \ No newline at end of file diff --git a/test/sampleFiles/emptyFile.html b/test/sampleFiles/emptyFile.html new file mode 100644 index 0000000..e69de29 diff --git a/test/sampleFiles/emptySelectionMultipleCursors.html b/test/sampleFiles/emptySelectionMultipleCursors.html new file mode 100644 index 0000000..f6ab8d6 --- /dev/null +++ b/test/sampleFiles/emptySelectionMultipleCursors.html @@ -0,0 +1,5 @@ +
content
+ + + +
content
\ No newline at end of file diff --git a/test/sampleFiles/expectedEmptyFileSingleCursorResult.html b/test/sampleFiles/expectedEmptyFileSingleCursorResult.html new file mode 100644 index 0000000..540135a --- /dev/null +++ b/test/sampleFiles/expectedEmptyFileSingleCursorResult.html @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/test/sampleFiles/expectedEmptySelectionMultipleCursorsResult.html b/test/sampleFiles/expectedEmptySelectionMultipleCursorsResult.html new file mode 100644 index 0000000..4f05fd7 --- /dev/null +++ b/test/sampleFiles/expectedEmptySelectionMultipleCursorsResult.html @@ -0,0 +1,5 @@ +
content
+

+

+

+
content
\ No newline at end of file diff --git a/test/sampleFiles/expectedMultiSelectionMixedLineBlockFileResult.html b/test/sampleFiles/expectedMultiSelectionMixedLineBlockFileResult.html new file mode 100644 index 0000000..2a41625 --- /dev/null +++ b/test/sampleFiles/expectedMultiSelectionMixedLineBlockFileResult.html @@ -0,0 +1,16 @@ +
+

this is the first

+

block of text

+
+ +

+ this is the second + block of text +

+ +
+
+

this is the

third block + of

text

+
+
diff --git a/test/sampleFiles/expectedMultiSelectionTextBlocksFileResult.html b/test/sampleFiles/expectedMultiSelectionTextBlocksFileResult.html new file mode 100644 index 0000000..e17a1f5 --- /dev/null +++ b/test/sampleFiles/expectedMultiSelectionTextBlocksFileResult.html @@ -0,0 +1,20 @@ +
+

+ this is the first + block of text +

+
+ +

+ this is the second + block of text +

+ +
+
+

+ this is the third block + of text +

+
+
diff --git a/test/sampleFiles/textBlocks.html b/test/sampleFiles/textBlocks.html new file mode 100644 index 0000000..08ebc13 --- /dev/null +++ b/test/sampleFiles/textBlocks.html @@ -0,0 +1,14 @@ +
+ this is the first + block of text +
+ +this is the second +block of text + +
+
+ this is the third block + of text +
+