extensions/emmet/src/abbreviationActions.ts/applyPreview
export async function wrapWithAbbreviation(args: any): Promise<boolean> {
	if (!validate(false)) {
		return false;
	}

	const editor = vscode.window.activeTextEditor!;
	const document = editor.document;

	args = args || {};
	if (!args['language']) {
		args['language'] = document.languageId;
	}
	// we know it's not stylesheet due to the validate(false) call above
	const syntax = getSyntaxFromArgs(args) || 'html';
	const rootNode = parseDocument(document, true);

	const helper = getEmmetHelper();

	const operationRanges = Array.from(editor.selections).sort((a, b) => a.start.compareTo(b.start)).map(selection => {
		let rangeToReplace: vscode.Range = selection;
		// wrap around the node if the selection falls inside its open or close tag
		{
			let { start, end } = rangeToReplace;

			const startOffset = document.offsetAt(start);
			const documentText = document.getText();
			const startNode = getHtmlFlatNode(documentText, rootNode, startOffset, true);
			if (startNode && isOffsetInsideOpenOrCloseTag(startNode, startOffset)) {
				start = document.positionAt(startNode.start);
				const nodeEndPosition = document.positionAt(startNode.end);
				end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
			}

			const endOffset = document.offsetAt(end);
			const endNode = getHtmlFlatNode(documentText, rootNode, endOffset, true);
			if (endNode && isOffsetInsideOpenOrCloseTag(endNode, endOffset)) {
				const nodeStartPosition = document.positionAt(endNode.start);
				start = nodeStartPosition.isBefore(start) ? nodeStartPosition : start;
				const nodeEndPosition = document.positionAt(endNode.end);
				end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
			}

			rangeToReplace = new vscode.Range(start, end);
		}
		// in case of multi-line, exclude last empty line from rangeToReplace
		if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
			const previousLine = rangeToReplace.end.line - 1;
			rangeToReplace = new vscode.Range(rangeToReplace.start, document.lineAt(previousLine).range.end);
		}
		// wrap line the cursor is on
		if (rangeToReplace.isEmpty) {
			rangeToReplace = document.lineAt(rangeToReplace.start).range;
		}

		// ignore whitespace on the first line
		const firstLineOfRange = document.lineAt(rangeToReplace.start);
		if (!firstLineOfRange.isEmptyOrWhitespace && firstLineOfRange.firstNonWhitespaceCharacterIndex > rangeToReplace.start.character) {
			rangeToReplace = rangeToReplace.with(new vscode.Position(rangeToReplace.start.line, firstLineOfRange.firstNonWhitespaceCharacterIndex));
		}

		return rangeToReplace;
	}).reduce((mergedRanges, range) => {
		// Merge overlapping ranges
		if (mergedRanges.length > 0 && range.intersection(mergedRanges[mergedRanges.length - 1])) {
			mergedRanges.push(range.union(mergedRanges.pop()!));
		} else {
			mergedRanges.push(range);
		}
		return mergedRanges;
	}, [] as vscode.Range[]);

	// Backup orginal selections and update selections
	// Also helps with https://github.com/microsoft/vscode/issues/113930 by avoiding `editor.linkedEditing`
	// execution if selection is inside an open or close tag
	const oldSelections = editor.selections;
	editor.selections = operationRanges.map(range => new vscode.Selection(range.start, range.end));

	// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
	const rangesToReplace: PreviewRangesWithContent[] = operationRanges.map(rangeToReplace => {
		let textToWrapInPreview: string[];
		const textToReplace = document.getText(rangeToReplace);

		// the following assumes all the lines are indented the same way as the first
		// this assumption helps with applyPreview later
		const wholeFirstLine = document.lineAt(rangeToReplace.start).text;
		const otherMatches = wholeFirstLine.match(/^(\s*)/);
		const baseIndent = otherMatches ? otherMatches[1] : '';
		textToWrapInPreview = rangeToReplace.isSingleLine ?
			[textToReplace] :
			textToReplace.split('\n' + baseIndent).map(x => x.trimEnd());

		// escape $ characters, fixes #52640
		textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));

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

	const { tabSize, insertSpaces } = editor.options;
	const indent = insertSpaces ? ' '.repeat(tabSize as number) : '\t';

	function revertPreview(): Thenable<boolean> {
		return editor.edit(builder => {
			for (const rangeToReplace of rangesToReplace) {
				builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
				rangeToReplace.previewRange = rangeToReplace.originalRange;
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}

	function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
		let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
		let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
		let totalNewLinesInserted = 0;

		return editor.edit(builder => {
			// the edits are applied in order top-down
			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;
				}

				// get the current preview range, format the new wrapped text, and then replace
				// the text in the preview range with that new text
				const oldPreviewRange = rangesToReplace[i].previewRange;
				const newText = expandedText
					.replace(/\$\{[\d]*\}/g, '|') // Removing Tabstops
					.replace(/\$\{[\d]*:([^}]*)\}/g, (_, placeholder) => placeholder) // Replacing Placeholders
					.replace(/\\\$/g, '$'); // Remove backslashes before $
				builder.replace(oldPreviewRange, newText);

				// calculate the new preview range to use for future previews
				// we also have to take into account that the previous expansions could:
				// - cause new lines to appear
				// - be on the same line as other expansions
				const expandedTextLines = newText.split('\n');
				const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
				const newLinesInserted = expandedTextLines.length - oldPreviewLines;

				const newPreviewLineStart = oldPreviewRange.start.line + totalNewLinesInserted;
				let newPreviewStart = oldPreviewRange.start.character;
				const newPreviewLineEnd = oldPreviewRange.end.line + totalNewLinesInserted + newLinesInserted;
				let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length;
				if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) {
					// If newPreviewLineEnd is equal to the previous expandedText lineEnd,
					// set newPreviewStart to the length of the previous expandedText in that line
					// plus the number of characters between both selections.
					newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
					newPreviewEnd += newPreviewStart;
				} else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
					// Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
					newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
				} else if (expandedTextLines.length === 1) {
					// If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
					newPreviewEnd += oldPreviewRange.start.character;
				}

				lastOldPreviewRange = rangesToReplace[i].previewRange;
				lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
				rangesToReplace[i].previewRange = lastNewPreviewRange;
				totalNewLinesInserted += newLinesInserted;
			}
		}, { undoStopBefore: false, undoStopAfter: false });
	}

	let inPreviewMode = false;
	async function makeChanges(inputAbbreviation: string | undefined, previewChanges: boolean): Promise<boolean> {
		const isAbbreviationValid = !!inputAbbreviation && !!inputAbbreviation.trim() && helper.isAbbreviationValid(syntax, inputAbbreviation);
		const extractedResults = isAbbreviationValid ? helper.extractAbbreviationFromText(inputAbbreviation!, syntax) : undefined;
		if (!extractedResults) {
			if (inPreviewMode) {
				inPreviewMode = false;
				await revertPreview();
			}
			return false;
		}

		const { abbreviation, filter } = extractedResults;
		if (abbreviation !== inputAbbreviation) {
			// Not clear what should we do in this case. Warn the user? How?
		}

		if (previewChanges) {
			const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent =>
				({ syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter, indent, baseIndent: rangesAndContent.baseIndent })
			);

			inPreviewMode = true;
			return applyPreview(expandAbbrList);
		}

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

		if (inPreviewMode) {
			inPreviewMode = false;
			await revertPreview();
		}

		return expandAbbreviationInRange(editor, expandAbbrList, false);
	}

	let currentValue = '';
	function inputChanged(value: string): string {
		if (value !== currentValue) {
			currentValue = value;
			makeChanges(value, true);
		}
		return '';
	}

	const prompt = vscode.l10n.t("Enter Abbreviation");
	const inputAbbreviation = (args && args['abbreviation'])
		? (args['abbreviation'] as string)
		: await vscode.window.showInputBox({ prompt, validateInput: inputChanged });

	const changesWereMade = await makeChanges(inputAbbreviation, false);
	if (!changesWereMade) {
		editor.selections = oldSelections;
	}

	return changesWereMade;
}