svelte-language-server
Version:
A language server for Svelte
401 lines • 15.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractScriptTags = extractScriptTags;
exports.extractStyleTag = extractStyleTag;
exports.extractTemplateTag = extractTemplateTag;
exports.positionAt = positionAt;
exports.offsetAt = offsetAt;
exports.getLineOffsets = getLineOffsets;
exports.isInTag = isInTag;
exports.isRangeInTag = isRangeInTag;
exports.getTextInRange = getTextInRange;
exports.getLineAtPosition = getLineAtPosition;
exports.isAtEndOfLine = isAtEndOfLine;
exports.updateRelativeImport = updateRelativeImport;
exports.getNodeIfIsInComponentStartTag = getNodeIfIsInComponentStartTag;
exports.getNodeIfIsInHTMLStartTag = getNodeIfIsInHTMLStartTag;
exports.getNodeIfIsInStartTag = getNodeIfIsInStartTag;
exports.isInHTMLTagRange = isInHTMLTagRange;
exports.getWordRangeAt = getWordRangeAt;
exports.getWordAt = getWordAt;
exports.toRange = toRange;
exports.getLangAttribute = getLangAttribute;
exports.isInsideMoustacheTag = isInsideMoustacheTag;
exports.inStyleOrScript = inStyleOrScript;
const utils_1 = require("../../utils");
const vscode_languageserver_1 = require("vscode-languageserver");
const path = __importStar(require("path"));
const parseHtml_1 = require("./parseHtml");
function parseAttributes(rawAttrs) {
const attrs = {};
if (!rawAttrs) {
return attrs;
}
Object.keys(rawAttrs).forEach((attrName) => {
const attrValue = rawAttrs[attrName];
attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue);
});
return attrs;
function removeOuterQuotes(attrValue) {
if ((attrValue.startsWith('"') && attrValue.endsWith('"')) ||
(attrValue.startsWith("'") && attrValue.endsWith("'"))) {
return attrValue.slice(1, attrValue.length - 1);
}
return attrValue;
}
}
const regexIf = new RegExp('{\\s*#if\\s.*?}', 'gms');
const regexIfEnd = new RegExp('{\\s*/if}', 'gms');
const regexEach = new RegExp('{\\s*#each\\s.*?}', 'gms');
const regexEachEnd = new RegExp('{\\s*/each}', 'gms');
const regexAwait = new RegExp('{\\s*#await\\s.*?}', 'gms');
const regexAwaitEnd = new RegExp('{\\s*/await}', 'gms');
const regexHtml = new RegExp('{\\s*@html\\s.*?', 'gms');
/**
* Extracts a tag (style or script) from the given text
* and returns its start, end and the attributes on that tag.
* @param text text content to extract tag from
* @param tag the tag to extract
*/
function extractTags(text, tag, html) {
const rootNodes = html?.roots || (0, parseHtml_1.parseHtml)(text).roots;
const matchedNodes = rootNodes
.filter((node) => node.tag === tag)
.filter((tag) => {
return isNotInsideControlFlowTag(tag) && isNotInsideHtmlTag(tag);
});
return matchedNodes.map(transformToTagInfo);
/**
* For every match AFTER the tag do a search for `{/X`.
* If that is BEFORE `{#X`, we are inside a moustache tag.
*/
function isNotInsideControlFlowTag(tag) {
const nodes = rootNodes.slice(rootNodes.indexOf(tag));
const rootContentAfterTag = nodes
.map((node, idx) => {
const start = node.startTagEnd ? node.end : node.start + (node.tag?.length || 0);
return text.substring(start, nodes[idx + 1]?.start);
})
.join('');
return ![
[regexIf, regexIfEnd],
[regexEach, regexEachEnd],
[regexAwait, regexAwaitEnd]
].some((pair) => {
pair[0].lastIndex = 0;
pair[1].lastIndex = 0;
const start = pair[0].exec(rootContentAfterTag);
const end = pair[1].exec(rootContentAfterTag);
return (end?.index ?? text.length) < (start?.index ?? text.length);
});
}
/**
* For every match BEFORE the tag do a search for `{@html`.
* If that is BEFORE `}`, we are inside a moustache tag.
*/
function isNotInsideHtmlTag(tag) {
const nodes = rootNodes.slice(0, rootNodes.indexOf(tag));
const rootContentBeforeTag = [{ start: 0, end: 0 }, ...nodes]
.map((node, idx) => {
return text.substring(node.end, nodes[idx]?.start);
})
.join('');
return !((0, utils_1.regexLastIndexOf)(rootContentBeforeTag, regexHtml) >
rootContentBeforeTag.lastIndexOf('}'));
}
function transformToTagInfo(matchedNode) {
const start = matchedNode.startTagEnd ?? matchedNode.start;
const end = matchedNode.endTagStart ?? matchedNode.end;
const startPos = positionAt(start, text);
const endPos = positionAt(end, text);
const container = {
start: matchedNode.start,
end: matchedNode.end
};
const content = text.substring(start, end);
return {
content,
attributes: parseAttributes(matchedNode.attributes),
start,
end,
startPos,
endPos,
container
};
}
}
function extractScriptTags(source, html) {
const scripts = extractTags(source, 'script', html);
if (!scripts.length) {
return null;
}
const script = scripts.find((s) => s.attributes['context'] !== 'module' && !('module' in s.attributes));
const moduleScript = scripts.find((s) => s.attributes['context'] === 'module' || 'module' in s.attributes);
return { script, moduleScript };
}
function extractStyleTag(source, html) {
const styles = extractTags(source, 'style', html);
if (!styles.length) {
return null;
}
// There can only be one style tag
return styles[0];
}
function extractTemplateTag(source, html) {
const templates = extractTags(source, 'template', html);
if (!templates.length) {
return null;
}
// There should only be one style tag
return templates[0];
}
/**
* Get the line and character based on the offset
* @param offset The index of the position
* @param text The text for which the position should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
function positionAt(offset, text, lineOffsets = getLineOffsets(text)) {
offset = (0, utils_1.clamp)(offset, 0, text.length);
let low = 0;
let high = lineOffsets.length;
if (high === 0) {
return vscode_languageserver_1.Position.create(0, offset);
}
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const lineOffset = lineOffsets[mid];
if (lineOffset === offset) {
return vscode_languageserver_1.Position.create(mid, 0);
}
else if (offset > lineOffset) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return vscode_languageserver_1.Position.create(line, offset - lineOffsets[line]);
}
/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
function offsetAt(position, text, lineOffsets = getLineOffsets(text)) {
if (position.line >= lineOffsets.length) {
return text.length;
}
else if (position.line < 0) {
return 0;
}
const lineOffset = lineOffsets[position.line];
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
return (0, utils_1.clamp)(nextLineOffset, lineOffset, lineOffset + position.character);
}
function getLineOffsets(text) {
const lineOffsets = [];
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}
if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
}
function isInTag(position, tagInfo) {
return !!tagInfo && (0, utils_1.isInRange)(vscode_languageserver_1.Range.create(tagInfo.startPos, tagInfo.endPos), position);
}
function isRangeInTag(range, tagInfo) {
return isInTag(range.start, tagInfo) && isInTag(range.end, tagInfo);
}
function getTextInRange(range, text) {
return text.substring(offsetAt(range.start, text), offsetAt(range.end, text));
}
function getLineAtPosition(position, text) {
return text.substring(offsetAt({ line: position.line, character: 0 }, text), offsetAt({ line: position.line, character: Number.MAX_VALUE }, text));
}
/**
* Assumption: Is called with a line. A line does only contain line break characters
* at its end.
*/
function isAtEndOfLine(line, offset) {
return [undefined, '\r', '\n'].includes(line[offset]);
}
/**
* Updates a relative import
*
* @param oldPath Old absolute path
* @param newPath New absolute path
* @param relativeImportPath Import relative to the old path
*/
function updateRelativeImport(oldPath, newPath, relativeImportPath) {
let newImportPath = path
.join(path.relative(newPath, oldPath), relativeImportPath)
.replace(/\\/g, '/');
if (!newImportPath.startsWith('.')) {
newImportPath = './' + newImportPath;
}
return newImportPath;
}
/**
* Returns the node if offset is inside a component's starttag
*/
function getNodeIfIsInComponentStartTag(html, document, offset) {
const node = html.findNodeAt(offset);
if (!!node.tag &&
(node.tag[0] === node.tag[0].toUpperCase() ||
(document.isSvelte5 && node.tag.includes('.'))) &&
(!node.startTagEnd || offset < node.startTagEnd)) {
return node;
}
}
/**
* Returns the node if offset is inside a HTML starttag
*/
function getNodeIfIsInHTMLStartTag(html, offset) {
const node = html.findNodeAt(offset);
if (!!node.tag &&
node.tag[0] === node.tag[0].toLowerCase() &&
(!node.startTagEnd || offset < node.startTagEnd)) {
return node;
}
}
/**
* Returns the node if offset is inside a starttag (HTML or component)
*/
function getNodeIfIsInStartTag(html, offset) {
const node = html.findNodeAt(offset);
if (!!node.tag && (!node.startTagEnd || offset < node.startTagEnd)) {
return node;
}
}
/**
* Returns `true` if `offset` is a html tag and within the name of the start tag or end tag
*/
function isInHTMLTagRange(html, offset) {
const node = html.findNodeAt(offset);
return (!!node.tag &&
node.tag[0] === node.tag[0].toLowerCase() &&
(node.start + node.tag.length + 1 >= offset ||
(!!node.endTagStart && node.endTagStart <= offset)));
}
/**
* Gets word range at position.
* Delimiter is by default a whitespace, but can be adjusted.
*/
function getWordRangeAt(str, pos, delimiterRegex = { left: /\S+$/, right: /\s/ }) {
let start = str.slice(0, pos).search(delimiterRegex.left);
if (start < 0) {
start = pos;
}
let end = str.slice(pos).search(delimiterRegex.right);
if (end < 0) {
end = str.length;
}
else {
end = end + pos;
}
return { start, end };
}
/**
* Gets word at position.
* Delimiter is by default a whitespace, but can be adjusted.
*/
function getWordAt(str, pos, delimiterRegex = { left: /\S+$/, right: /\s/ }) {
const { start, end } = getWordRangeAt(str, pos, delimiterRegex);
return str.slice(start, end);
}
function toRange(str, start, end) {
if (typeof str === 'string') {
return vscode_languageserver_1.Range.create(positionAt(start, str), positionAt(end, str));
}
return vscode_languageserver_1.Range.create(str.positionAt(start), str.positionAt(end));
}
/**
* Returns the language from the given tags, return the first from which a language is found.
* Searches inside lang and type and removes leading 'text/'
*/
function getLangAttribute(...tags) {
const tag = tags.find((tag) => tag?.attributes.lang || tag?.attributes.type);
if (!tag) {
return null;
}
const attribute = tag.attributes.lang || tag.attributes.type;
if (!attribute) {
return null;
}
return attribute.replace(/^text\//, '');
}
/**
* Checks whether given position is inside a moustache tag (which includes control flow tags)
* using a simple bracket matching heuristic which might fail under conditions like
* `{#if {a: true}.a}`
*/
function isInsideMoustacheTag(html, tagStart, position) {
if (tagStart === null) {
// Not inside <tag ... >
const charactersBeforePosition = html.substring(0, position);
return (Math.max(
// TODO make this just check for '{'?
// Theoretically, someone could do {a < b} in a simple moustache tag
charactersBeforePosition.lastIndexOf('{#'), charactersBeforePosition.lastIndexOf('{:'), charactersBeforePosition.lastIndexOf('{@')) > charactersBeforePosition.lastIndexOf('}'));
}
else {
// Inside <tag ... >
const charactersInNode = html.substring(tagStart, position);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
}
}
function inStyleOrScript(document, position) {
return (isInTag(position, document.styleInfo) ||
isInTag(position, document.scriptInfo) ||
isInTag(position, document.moduleScriptInfo));
}
//# sourceMappingURL=utils.js.map