-
Notifications
You must be signed in to change notification settings - Fork 29.9k
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
Emmet - Wrap with abbreviation with live preview #45092
Changes from 9 commits
39e6a2b
4a566ef
4947eaf
3b9af18
57082a4
b855519
b576c20
2821c46
22ca9c4
12674c6
0889378
106bc42
5ea306b
56d3f88
9fd6348
70c0a26
206c968
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,12 @@ interface ExpandAbbreviationInput { | |
filter?: string; | ||
} | ||
|
||
interface PreviewRangesWithContent { | ||
previewRange: vscode.Range; | ||
originalRange: vscode.Range; | ||
originalContent: string; | ||
} | ||
|
||
export function wrapWithAbbreviation(args: any) { | ||
if (!validate(false) || !vscode.window.activeTextEditor) { | ||
return; | ||
|
@@ -32,48 +38,134 @@ export function wrapWithAbbreviation(args: any) { | |
const editor = vscode.window.activeTextEditor; | ||
let rootNode = parseDocument(editor.document, false); | ||
|
||
const syntax = getSyntaxFromArgs({ language: editor.document.languageId }); | ||
const syntax = getSyntaxFromArgs({ language: editor.document.languageId }) || ''; | ||
if (!syntax) { | ||
return; | ||
} | ||
|
||
const abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation' }); | ||
let previewMade = false; | ||
|
||
// 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 rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; | ||
if (rangeToReplace.isEmpty) { | ||
let { active } = selection; | ||
let 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); | ||
} | ||
} | ||
|
||
const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); | ||
const matches = firstLineOfSelection.match(/^(\s*)/); | ||
const preceedingWhiteSpace = matches ? matches[1].length : 0; | ||
|
||
rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + preceedingWhiteSpace, rangeToReplace.end.line, rangeToReplace.end.character); | ||
let textToReplace = editor.document.getText(rangeToReplace); | ||
rangesToReplace.push({ previewRange: rangeToReplace, originalRange: rangeToReplace, originalContent: textToReplace }); | ||
}); | ||
|
||
let abbreviationPromise; | ||
let currentValue = ''; | ||
|
||
function inputChanged(value: string): string { | ||
if (value !== currentValue) { | ||
currentValue = value; | ||
makeChanges(value, previewMade, false).then((out) => { | ||
if (typeof out === 'boolean') { | ||
previewMade = out; | ||
} | ||
}); | ||
} | ||
return ''; | ||
} | ||
|
||
abbreviationPromise = (args && args['abbreviation']) ? Promise.resolve(args['abbreviation']) : vscode.window.showInputBox({ prompt: 'Enter Abbreviation', validateInput: inputChanged }); | ||
const helper = getEmmetHelper(); | ||
|
||
return abbreviationPromise.then(inputAbbreviation => { | ||
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { return false; } | ||
function makeChanges(inputAbbreviation: string | undefined, previewMade?: boolean, definitive?: boolean): Thenable<any> { | ||
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { | ||
return previewMade ? revertPreview(editor, rangesToReplace) : Promise.resolve(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning false allows us to not revert the preview several times. That would not a problem per se, since revertPreview only replaces the current range with the original content, but why make extra unnecessary edits? |
||
} | ||
|
||
let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be an exisitng bug... but shouldnt we check for the validity of the abbreviation after it is extracted instead of before? I would suggest to move the extraction to be the first thing that we do in
|
||
if (!extractedResults) { | ||
return false; | ||
return Promise.resolve(previewMade); | ||
} | ||
|
||
let { abbreviation, filter } = extractedResults; | ||
if (definitive) { | ||
const revertPromise = previewMade ? revertPreview(editor, rangesToReplace) : 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 }; | ||
}); | ||
return expandAbbreviationInRange(editor, expandAbbrList, true).then(() => { return Promise.resolve(); }); | ||
}); | ||
} | ||
|
||
let expandAbbrList: ExpandAbbreviationInput[] = []; | ||
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => { | ||
let match = rangesAndContent.originalContent.match(/\n[\s]*/g); | ||
let textToWrap = match ? ['\n\t' + rangesAndContent.originalContent.split(match[match.length - 1]).join('\n\t') + '\n'] : [rangesAndContent.originalContent]; | ||
return { syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap, filter }; | ||
}); | ||
|
||
editor.selections.forEach(selection => { | ||
let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; | ||
if (rangeToReplace.isEmpty) { | ||
let { active } = selection; | ||
let 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); | ||
} | ||
} | ||
return applyPreview(editor, expandAbbrList, rangesToReplace); | ||
} | ||
|
||
const firstLineOfSelection = editor.document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); | ||
const matches = firstLineOfSelection.match(/^(\s*)/); | ||
const preceedingWhiteSpace = matches ? matches[1].length : 0; | ||
// On inputBox closing | ||
return abbreviationPromise.then(inputAbbreviation => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests are currently failing as the promise returned by Since |
||
return makeChanges(inputAbbreviation, previewMade, true); | ||
}); | ||
} | ||
|
||
rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + preceedingWhiteSpace, rangeToReplace.end.line, rangeToReplace.end.character); | ||
let textToWrap = rangeToReplace.isSingleLine ? ['$TM_SELECTED_TEXT'] : ['\n\t$TM_SELECTED_TEXT\n']; | ||
expandAbbrList.push({ syntax, abbreviation, rangeToReplace, textToWrap, filter }); | ||
}); | ||
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 }); | ||
} | ||
|
||
return expandAbbreviationInRange(editor, expandAbbrList, true); | ||
}); | ||
function applyPreview(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], rangesToReplace: PreviewRangesWithContent[]): Thenable<any> { | ||
const anyExpandAbbrInput = expandAbbrList[0]; | ||
let expandedText = expandAbbr(anyExpandAbbrInput); | ||
let linesInserted = 0; | ||
|
||
if (expandedText) { | ||
return editor.edit(builder => { | ||
for (let i = 0; i < rangesToReplace.length; i++) { | ||
if (i) { | ||
expandedText = expandAbbr(expandAbbrList[i]); | ||
} | ||
let thisRange = rangesToReplace[i].previewRange; | ||
let indentPrefix = ''; | ||
let preceedingText = editor.document.getText(new vscode.Range(new vscode.Position(thisRange.start.line, 0), thisRange.start)); | ||
// If there is only whitespace before the text to wrap, take that as prefix. If not, take as much whitespace as there is before text appears. | ||
if (!preceedingText.match(/[^\s]/)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this if/else, why not follow the same logic as |
||
indentPrefix = preceedingText; | ||
} else { | ||
indentPrefix = (preceedingText.match(/(^[\s]*)/) || ['', ''])[1]; | ||
} | ||
|
||
let newText = expandedText || ''; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would we continue if |
||
newText = newText.replace(/\n/g, '\n' + indentPrefix).replace(/\$\{[\d]*\}/g, '|'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will not replace placeholders. Try wrapping with |
||
let newTextLines = (newText.match(/\n/g) || []).length + 1; | ||
let currentTextLines = thisRange.end.line - thisRange.start.line + 1; | ||
builder.replace(thisRange, newText); | ||
rangesToReplace[i].previewRange = new vscode.Range(thisRange.start.line + linesInserted, thisRange.start.character, thisRange.start.line + linesInserted + newTextLines - 1, 1000); // TODO: fix 1000! Count characters in the resulting text? | ||
linesInserted += newTextLines - currentTextLines; | ||
} | ||
}, { undoStopBefore: false, undoStopAfter: false }); | ||
} | ||
return Promise.resolve([]); | ||
} | ||
|
||
export function wrapIndividualLinesWithAbbreviation(args: any) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2058,16 +2058,12 @@ vscode-emmet-helper@^1.2.0: | |
dependencies: | ||
"@emmetio/extract-abbreviation" "0.1.6" | ||
jsonc-parser "^1.0.0" | ||
vscode-languageserver-types "^3.6.0-next.1" | ||
vscode-languageserver-types "^3.5.0" | ||
|
||
vscode-languageserver-types@^3.5.0: | ||
version "3.5.0" | ||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374" | ||
|
||
vscode-languageserver-types@^3.6.0-next.1: | ||
version "3.6.0-next.1" | ||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.6.0-next.1.tgz#98e488d3f87b666b4ee1a3d89f0023e246d358f3" | ||
|
||
[email protected]: | ||
version "3.2.1" | ||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.1.tgz#b1f3e04e8a94a715d5a7bcbc8339c51e6d74ca51" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if
inputChanged
gets called before the previousmakeChanges
completes?