Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Live Preview for Wrapping Individual Lines with Abbreviation #45453

Merged
merged 3 commits into from
Mar 14, 2018
Merged
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
267 changes: 113 additions & 154 deletions extensions/emmet/src/abbreviationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,70 +32,130 @@ interface PreviewRangesWithContent {
}

export function wrapWithAbbreviation(args: any) {
return doWrapping(false, args);
}

export function wrapIndividualLinesWithAbbreviation(args: any) {
return doWrapping(true, args);
}

function doWrapping(individualLines: boolean, args: any) {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}

const editor = vscode.window.activeTextEditor;
let rootNode = parseDocument(editor.document, false);

const syntax = getSyntaxFromArgs({ language: editor.document.languageId }) || '';
const rootNode = parseDocument(editor.document, false);
if (individualLines) {
if (editor.selections.length === 1 && editor.selection.isEmpty) {
vscode.window.showInformationMessage('Select more than 1 line and try again.');
return;
}
if (editor.selections.find(x => x.isEmpty)) {
vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
return;
}
}
const syntax = getSyntaxFromArgs({ language: editor.document.languageId });
if (!syntax) {
return;
}

let inPreview = false;
let currentValue = '';
const helper = getEmmetHelper();

// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
let rangesToReplace: PreviewRangesWithContent[] = [];

editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.line - b.start.line; }).forEach(selection => {
let rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => {
let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
let previousLine = rangeToReplace.end.line - 1;
let lastChar = editor.document.lineAt(previousLine).text.length;
const previousLine = rangeToReplace.end.line - 1;
const lastChar = editor.document.lineAt(previousLine).text.length;
rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
} else if (rangeToReplace.isEmpty) {
let { active } = selection;
let currentNode = getNode(rootNode, active, true);
const { active } = selection;
const currentNode = getNode(rootNode, active, true);
if (currentNode && (currentNode.start.line === active.line || currentNode.end.line === active.line)) {
rangeToReplace = new vscode.Range(currentNode.start, currentNode.end);
} else {
rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
}
}

rangeToReplace = ignoreExtraWhitespaceSelected(rangeToReplace, editor.document);
const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character);
const matches = firstLineOfSelection.match(/^(\s*)/);
const extraWhiteSpaceSelected = matches ? matches[1].length : 0;
rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhiteSpaceSelected, rangeToReplace.end.line, rangeToReplace.end.character);

const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
const otherMatches = wholeFirstLine.match(/^(\s*)/);
const preceedingWhiteSpace = otherMatches ? otherMatches[1] : '';
let textToWrapInPreview: string[];
let textToReplace = editor.document.getText(rangeToReplace);
let textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + preceedingWhiteSpace).join('\n\t') + '\n'];
rangesToReplace.push({ previewRange: rangeToReplace, originalRange: rangeToReplace, originalContent: textToReplace, textToWrapInPreview });
if (individualLines) {
textToWrapInPreview = textToReplace.split('\n').map(x => x.trim());
} else {
const wholeFirstLine = editor.document.lineAt(rangeToReplace.start).text;
const otherMatches = wholeFirstLine.match(/^(\s*)/);
const preceedingWhiteSpace = otherMatches ? otherMatches[1] : '';
textToWrapInPreview = rangeToReplace.isSingleLine ? [textToReplace] : ['\n\t' + textToReplace.split('\n' + preceedingWhiteSpace).join('\n\t') + '\n'];
}

return {
previewRange: rangeToReplace,
originalRange: rangeToReplace,
originalContent: textToReplace,
textToWrapInPreview
};
});

let abbreviationPromise;
let currentValue = '';
function revertPreview(): Thenable<any> {
return editor.edit(builder => {
for (let i = 0; i < rangesToReplace.length; i++) {
builder.replace(rangesToReplace[i].previewRange, rangesToReplace[i].originalContent);
rangesToReplace[i].previewRange = rangesToReplace[i].originalRange;
}
}, { undoStopBefore: false, undoStopAfter: false });
}

function inputChanged(value: string): string {
if (value !== currentValue) {
currentValue = value;
makeChanges(value, inPreview, false).then((out) => {
if (typeof out === 'boolean') {
inPreview = out;
function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
let totalLinesInserted = 0;

return editor.edit(builder => {
for (let i = 0; i < rangesToReplace.length; i++) {
const expandedText = expandAbbr(expandAbbrList[i]) || '';
if (!expandedText) {
// Failed to expand text. We already showed an error inside expandAbbr.
break;
}
});
}
return '';
}

abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
const helper = getEmmetHelper();
const oldPreviewRange = rangesToReplace[i].previewRange;
const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character));
const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1];

let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => { // Replacing Placeholders
return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
});
builder.replace(oldPreviewRange, newText);

const expandedTextLines = newText.split('\n');
const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
const newLinesInserted = expandedTextLines.length - oldPreviewLines;

let lastLineEnd = expandedTextLines[expandedTextLines.length - 1].length;
if (expandedTextLines.length === 1) {
// If the expandedText is single line, add the length of preceeding whitespace as it will not be included in line length.
lastLineEnd += oldPreviewRange.start.character;
}

rangesToReplace[i].previewRange = new vscode.Range(oldPreviewRange.start.line + totalLinesInserted, oldPreviewRange.start.character, oldPreviewRange.end.line + totalLinesInserted + newLinesInserted, lastLineEnd);
totalLinesInserted += newLinesInserted;
}
}, { undoStopBefore: false, undoStopAfter: false });
}

function makeChanges(inputAbbreviation: string | undefined, inPreview: boolean, definitive: boolean): Thenable<boolean> {
function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> {
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) {
return inPreview ? revertPreview(editor, rangesToReplace).then(() => { return false; }) : Promise.resolve(inPreview);
return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview);
}

let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
Expand All @@ -107,145 +167,44 @@ export function wrapWithAbbreviation(args: any) {

let { abbreviation, filter } = extractedResults;
if (definitive) {
const revertPromise = inPreview ? revertPreview(editor, rangesToReplace) : Promise.resolve();
const revertPromise = inPreview ? revertPreview() : Promise.resolve();
return revertPromise.then(() => {
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
let rangeToReplace = rangesAndContent.originalRange;
let textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
return { syntax, abbreviation, rangeToReplace, textToWrap, filter };
let textToWrap: string[];
if (individualLines) {
textToWrap = rangesAndContent.textToWrapInPreview;
} else {
textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n'];
}
return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
});
return expandAbbreviationInRange(editor, expandAbbrList, true).then(() => { return true; });
return expandAbbreviationInRange(editor, expandAbbrList, !individualLines).then(() => { return true; });
});
}

const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
return { syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter };
});

return applyPreview(editor, expandAbbrList, rangesToReplace);
return applyPreview(expandAbbrList);
}

// On inputBox closing
return abbreviationPromise.then(inputAbbreviation => {
return makeChanges(inputAbbreviation, inPreview, true);
});
}

function revertPreview(editor: vscode.TextEditor, rangesToReplace: PreviewRangesWithContent[]): Thenable<any> {
return editor.edit(builder => {
for (let i = 0; i < rangesToReplace.length; i++) {
builder.replace(rangesToReplace[i].previewRange, rangesToReplace[i].originalContent);
rangesToReplace[i].previewRange = rangesToReplace[i].originalRange;
}
}, { undoStopBefore: false, undoStopAfter: false });
}

function applyPreview(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], rangesToReplace: PreviewRangesWithContent[]): Thenable<boolean> {
let totalLinesInserted = 0;

return editor.edit(builder => {
for (let i = 0; i < rangesToReplace.length; i++) {
const expandedText = expandAbbr(expandAbbrList[i]) || '';
if (!expandedText) {
// Failed to expand text. We already showed an error inside expandAbbr.
break;
}

const oldPreviewRange = rangesToReplace[i].previewRange;
const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character));
const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1];

let newText = expandedText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => { // Replacing Placeholders
return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
function inputChanged(value: string): string {
if (value !== currentValue) {
currentValue = value;
makeChanges(value, false).then((out) => {
if (typeof out === 'boolean') {
inPreview = out;
}
});
builder.replace(oldPreviewRange, newText);

const expandedTextLines = newText.split('\n');
const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
const newLinesInserted = expandedTextLines.length - oldPreviewLines;

let lastLineEnd = expandedTextLines[expandedTextLines.length - 1].length;
if (expandedTextLines.length === 1) {
// If the expandedText is single line, add the length of preceeding whitespace as it will not be included in line length.
lastLineEnd += oldPreviewRange.start.character;
}

rangesToReplace[i].previewRange = new vscode.Range(oldPreviewRange.start.line + totalLinesInserted, oldPreviewRange.start.character, oldPreviewRange.end.line + totalLinesInserted + newLinesInserted, lastLineEnd);
totalLinesInserted += newLinesInserted;
}
}, { undoStopBefore: false, undoStopAfter: false });
}

export function wrapIndividualLinesWithAbbreviation(args: any) {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}

const editor = vscode.window.activeTextEditor;
if (editor.selections.length === 1 && editor.selection.isEmpty) {
vscode.window.showInformationMessage('Select more than 1 line and try again.');
return;
}
if (editor.selections.find(x => x.isEmpty)) {
vscode.window.showInformationMessage('Select more than 1 line in each selection and try again.');
return;
}
let rangesToReplace: vscode.Range[] = [];
editor.selections.forEach(selection => {
let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection;
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
let previousLine = rangeToReplace.end.line - 1;
let lastChar = editor.document.lineAt(previousLine).text.length;
rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar));
}

rangeToReplace = ignoreExtraWhitespaceSelected(rangeToReplace, editor.document);
rangesToReplace.push(rangeToReplace);
});

const syntax = getSyntaxFromArgs({ language: editor.document.languageId });
if (!syntax) {
return;
return '';
}

const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' });
const helper = getEmmetHelper();

const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged });
return abbreviationPromise.then(inputAbbreviation => {
let expandAbbrInput: ExpandAbbreviationInput[] = [];
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { return false; }

let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
if (!extractedResults) {
return false;
}
rangesToReplace.forEach(rangeToReplace => {
let lines = editor.document.getText(rangeToReplace).split('\n').map(x => x.trim());

let { abbreviation, filter } = extractedResults;
let input: ExpandAbbreviationInput = {
syntax,
abbreviation,
rangeToReplace,
textToWrap: lines,
filter
};
expandAbbrInput.push(input);
});

return expandAbbreviationInRange(editor, expandAbbrInput, false);
return makeChanges(inputAbbreviation, true);
});

}

function ignoreExtraWhitespaceSelected(range: vscode.Range, document: vscode.TextDocument): vscode.Range {
const firstLineOfSelection = document.lineAt(range.start).text.substr(range.start.character);
const matches = firstLineOfSelection.match(/^(\s*)/);
const extraWhiteSpaceSelected = matches ? matches[1].length : 0;

return new vscode.Range(range.start.line, range.start.character + extraWhiteSpaceSelected, range.end.line, range.end.character);
}

export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
Expand Down