blob: 30533270b050a0f5a0f16a086eca3d59aa142da8 [file] [log] [blame]
/**
* Until SelectionRange lands in LSP, we'll return Range from server and convert it to
* SelectionRange on client side
*/
import { Range, SelectionRange } from 'vscode-languageserver-types';
import { createScanner } from '../parser/htmlScanner';
import { parse } from '../parser/htmlParser';
import { TokenType } from '../htmlLanguageTypes';
export function getSelectionRanges(document, positions) {
function getSelectionRange(position) {
var applicableRanges = getApplicableRanges(document, position);
var prev = undefined;
var current = undefined;
for (var index = applicableRanges.length - 1; index >= 0; index--) {
var range = applicableRanges[index];
if (!prev || range[0] !== prev[0] || range[1] !== prev[1]) {
current = SelectionRange.create(Range.create(document.positionAt(applicableRanges[index][0]), document.positionAt(applicableRanges[index][1])), current);
}
prev = range;
}
if (!current) {
current = SelectionRange.create(Range.create(position, position));
}
return current;
}
return positions.map(getSelectionRange);
}
function getApplicableRanges(document, position) {
var htmlDoc = parse(document.getText());
var currOffset = document.offsetAt(position);
var currNode = htmlDoc.findNodeAt(currOffset);
var result = getAllParentTagRanges(currNode);
// Self-closing or void elements
if (currNode.startTagEnd && !currNode.endTagStart) {
// THe rare case of unmatching tag pairs like <div></div1>
if (currNode.startTagEnd !== currNode.end) {
return [[currNode.start, currNode.end]];
}
var closeRange = Range.create(document.positionAt(currNode.startTagEnd - 2), document.positionAt(currNode.startTagEnd));
var closeText = document.getText(closeRange);
// Self-closing element
if (closeText === '/>') {
result.unshift([currNode.start + 1, currNode.startTagEnd - 2]);
}
// Void element
else {
result.unshift([currNode.start + 1, currNode.startTagEnd - 1]);
}
var attributeLevelRanges = getAttributeLevelRanges(document, currNode, currOffset);
result = attributeLevelRanges.concat(result);
return result;
}
if (!currNode.startTagEnd || !currNode.endTagStart) {
return result;
}
/**
* For html like
* `<div class="foo">bar</div>`
*/
result.unshift([currNode.start, currNode.end]);
/**
* Cursor inside `<div class="foo">`
*/
if (currNode.start < currOffset && currOffset < currNode.startTagEnd) {
result.unshift([currNode.start + 1, currNode.startTagEnd - 1]);
var attributeLevelRanges = getAttributeLevelRanges(document, currNode, currOffset);
result = attributeLevelRanges.concat(result);
return result;
}
/**
* Cursor inside `bar`
*/
else if (currNode.startTagEnd <= currOffset && currOffset <= currNode.endTagStart) {
result.unshift([currNode.startTagEnd, currNode.endTagStart]);
return result;
}
/**
* Cursor inside `</div>`
*/
else {
// `div` inside `</div>`
if (currOffset >= currNode.endTagStart + 2) {
result.unshift([currNode.endTagStart + 2, currNode.end - 1]);
}
return result;
}
}
function getAllParentTagRanges(initialNode) {
var currNode = initialNode;
var getNodeRanges = function (n) {
if (n.startTagEnd && n.endTagStart && n.startTagEnd < n.endTagStart) {
return [
[n.startTagEnd, n.endTagStart],
[n.start, n.end]
];
}
return [
[n.start, n.end]
];
};
var result = [];
while (currNode.parent) {
currNode = currNode.parent;
getNodeRanges(currNode).forEach(function (r) { return result.push(r); });
}
return result;
}
function getAttributeLevelRanges(document, currNode, currOffset) {
var currNodeRange = Range.create(document.positionAt(currNode.start), document.positionAt(currNode.end));
var currNodeText = document.getText(currNodeRange);
var relativeOffset = currOffset - currNode.start;
/**
* Tag level semantic selection
*/
var scanner = createScanner(currNodeText);
var token = scanner.scan();
/**
* For text like
* <div class="foo">bar</div>
*/
var positionOffset = currNode.start;
var result = [];
var isInsideAttribute = false;
var attrStart = -1;
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.AttributeName: {
if (relativeOffset < scanner.getTokenOffset()) {
isInsideAttribute = false;
break;
}
if (relativeOffset <= scanner.getTokenEnd()) {
// `class`
result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]);
}
isInsideAttribute = true;
attrStart = scanner.getTokenOffset();
break;
}
case TokenType.AttributeValue: {
if (!isInsideAttribute) {
break;
}
var valueText = scanner.getTokenText();
if (relativeOffset < scanner.getTokenOffset()) {
// `class="foo"`
result.push([attrStart, scanner.getTokenEnd()]);
break;
}
if (relativeOffset >= scanner.getTokenOffset() && relativeOffset <= scanner.getTokenEnd()) {
// `"foo"`
result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]);
// `foo`
if ((valueText[0] === "\"" && valueText[valueText.length - 1] === "\"") || (valueText[0] === "'" && valueText[valueText.length - 1] === "'")) {
if (relativeOffset >= scanner.getTokenOffset() + 1 && relativeOffset <= scanner.getTokenEnd() - 1) {
result.unshift([scanner.getTokenOffset() + 1, scanner.getTokenEnd() - 1]);
}
}
// `class="foo"`
result.push([attrStart, scanner.getTokenEnd()]);
}
break;
}
}
token = scanner.scan();
}
return result.map(function (pair) {
return [pair[0] + positionOffset, pair[1] + positionOffset];
});
}