svelte-language-server
Version:
A language server for Svelte
403 lines • 17.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CSSPlugin = void 0;
const emmet_helper_1 = require("@vscode/emmet-helper");
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../lib/documents");
const CSSDocument_1 = require("./CSSDocument");
const service_1 = require("./service");
const global_vars_1 = require("./global-vars");
const getIdClassCompletion_1 = require("./features/getIdClassCompletion");
const parseHtml_1 = require("../../lib/documents/parseHtml");
const StyleAttributeDocument_1 = require("./StyleAttributeDocument");
const documentContext_1 = require("../documentContext");
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const indentFolding_1 = require("../../lib/foldingRange/indentFolding");
const wordHighlight_1 = require("../../lib/documentHighlight/wordHighlight");
const utils_1 = require("../../utils");
// https://github.com/microsoft/vscode/blob/c6f507deeb99925e713271b1048f21dbaab4bd54/extensions/css/language-configuration.json#L34
const wordPattern = /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g;
class CSSPlugin {
constructor(docManager, configManager, workspaceFolders, cssLanguageServices) {
this.__name = 'css';
this.cssDocuments = new WeakMap();
this.triggerCharacters = ['.', ':', '-', '/'];
this.cssLanguageServices = cssLanguageServices;
this.workspaceFolders = workspaceFolders;
this.configManager = configManager;
this.updateConfigs();
const workspacePaths = workspaceFolders
.map((folder) => (0, utils_1.urlToPath)(folder.uri))
.filter(utils_1.isNotNullOrUndefined);
this.globalVars = new global_vars_1.GlobalVars(workspacePaths);
this.globalVars.watchFiles(this.configManager.get('css.globals'));
this.configManager.onChange((config) => {
this.globalVars.watchFiles(config.get('css.globals'));
this.updateConfigs();
});
docManager.on('documentChange', (document) => this.cssDocuments.set(document, new CSSDocument_1.CSSDocument(document, this.cssLanguageServices)));
docManager.on('documentClose', (document) => this.cssDocuments.delete(document));
}
getSelectionRange(document, position) {
if (!this.featureEnabled('selectionRange') || !(0, documents_1.isInTag)(position, document.styleInfo)) {
return null;
}
const cssDocument = this.getCSSDoc(document);
const [range] = this.getLanguageService(extractLanguage(cssDocument)).getSelectionRanges(cssDocument, [cssDocument.getGeneratedPosition(position)], cssDocument.stylesheet);
if (!range) {
return null;
}
return (0, documents_1.mapSelectionRangeToParent)(cssDocument, range);
}
getDiagnostics(document) {
if (!this.featureEnabled('diagnostics')) {
return [];
}
const cssDocument = this.getCSSDoc(document);
const kind = extractLanguage(cssDocument);
if (shouldExcludeValidation(kind)) {
return [];
}
return this.getLanguageService(kind)
.doValidation(cssDocument, cssDocument.stylesheet)
.map((diagnostic) => ({ ...diagnostic, source: (0, service_1.getLanguage)(kind) }))
.map((diagnostic) => (0, documents_1.mapObjWithRangeToOriginal)(cssDocument, diagnostic));
}
doHover(document, position) {
if (!this.featureEnabled('hover')) {
return null;
}
const cssDocument = this.getCSSDoc(document);
if (shouldExcludeHover(cssDocument)) {
return null;
}
if (cssDocument.isInGenerated(position)) {
return this.doHoverInternal(cssDocument, position);
}
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
if (attributeContext &&
this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange;
return this.doHoverInternal(new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end, this.cssLanguageServices), position);
}
return null;
}
doHoverInternal(cssDocument, position) {
const hoverInfo = this.getLanguageService(extractLanguage(cssDocument)).doHover(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
return hoverInfo ? (0, documents_1.mapHoverToParent)(cssDocument, hoverInfo) : hoverInfo;
}
async getCompletions(document, position, completionContext) {
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
if (isCustomTriggerCharacter &&
triggerCharacter &&
!this.triggerCharacters.includes(triggerCharacter)) {
return null;
}
if (!this.featureEnabled('completions')) {
return null;
}
const cssDocument = this.getCSSDoc(document);
if (cssDocument.isInGenerated(position)) {
return this.getCompletionsInternal(document, position, cssDocument);
}
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
if (!attributeContext) {
return null;
}
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange;
return this.getCompletionsInternal(document, position, new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end, this.cssLanguageServices));
}
else {
return (0, getIdClassCompletion_1.getIdClassCompletion)(cssDocument, attributeContext);
}
}
inStyleAttributeWithoutInterpolation(attrContext, text) {
return (attrContext.name === 'style' &&
!!attrContext.valueRange &&
!text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{'));
}
async getCompletionsInternal(document, position, cssDocument) {
if (isSASS(cssDocument)) {
// the css language service does not support sass, still we can use
// the emmet helper directly to at least get emmet completions
return ((0, emmet_helper_1.doComplete)(document, position, 'sass', this.configManager.getEmmetConfig()) ||
null);
}
const type = extractLanguage(cssDocument);
if (shouldExcludeCompletion(type)) {
return null;
}
const lang = this.getLanguageService(type);
let emmetResults = {
isIncomplete: false,
items: []
};
if (this.configManager.getConfig().css.completions.emmet &&
this.configManager.getEmmetConfig().showExpandedAbbreviation !== 'never') {
lang.setCompletionParticipants([
{
onCssProperty: (context) => {
if (context?.propertyName) {
emmetResults =
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, service_1.getLanguage)(type), this.configManager.getEmmetConfig()) || emmetResults;
}
},
onCssPropertyValue: (context) => {
if (context?.propertyValue) {
emmetResults =
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, service_1.getLanguage)(type), this.configManager.getEmmetConfig()) || emmetResults;
}
}
}
]);
}
const results = await lang.doComplete2(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet, (0, documentContext_1.getDocumentContext)(cssDocument.uri, this.workspaceFolders));
return vscode_languageserver_1.CompletionList.create(this.appendGlobalVars([...(results ? results.items : []), ...emmetResults.items].map((completionItem) => (0, documents_1.mapCompletionItemToOriginal)(cssDocument, completionItem))),
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0);
}
appendGlobalVars(items) {
// Finding one value with that item kind means we are in a value completion scenario
const value = items.find((item) => item.kind === vscode_languageserver_1.CompletionItemKind.Value);
if (!value) {
return items;
}
const additionalItems = this.globalVars
.getGlobalVars()
.map((globalVar) => ({
label: `var(${globalVar.name})`,
sortText: '-',
detail: `${globalVar.filename}\n\n${globalVar.name}: ${globalVar.value}`,
kind: vscode_languageserver_1.CompletionItemKind.Value
}));
return [...items, ...additionalItems];
}
getDocumentColors(document) {
if (!this.featureEnabled('documentColors')) {
return [];
}
const cssDocument = this.getCSSDoc(document);
if (shouldExcludeColor(cssDocument)) {
return [];
}
return this.getLanguageService(extractLanguage(cssDocument))
.findDocumentColors(cssDocument, cssDocument.stylesheet)
.map((colorInfo) => (0, documents_1.mapObjWithRangeToOriginal)(cssDocument, colorInfo));
}
getColorPresentations(document, range, color) {
if (!this.featureEnabled('colorPresentations')) {
return [];
}
const cssDocument = this.getCSSDoc(document);
if ((!cssDocument.isInGenerated(range.start) && !cssDocument.isInGenerated(range.end)) ||
shouldExcludeColor(cssDocument)) {
return [];
}
return this.getLanguageService(extractLanguage(cssDocument))
.getColorPresentations(cssDocument, cssDocument.stylesheet, color, (0, documents_1.mapRangeToGenerated)(cssDocument, range))
.map((colorPres) => (0, documents_1.mapColorPresentationToOriginal)(cssDocument, colorPres));
}
getDocumentSymbols(document) {
if (!this.featureEnabled('documentColors')) {
return [];
}
const cssDocument = this.getCSSDoc(document);
if (shouldExcludeDocumentSymbols(cssDocument)) {
return [];
}
return this.getLanguageService(extractLanguage(cssDocument))
.findDocumentSymbols(cssDocument, cssDocument.stylesheet)
.map((symbol) => {
if (!symbol.containerName) {
return {
...symbol,
// TODO: this could contain other things, e.g. style.myclass
containerName: 'style'
};
}
return symbol;
})
.map((symbol) => (0, documents_1.mapSymbolInformationToOriginal)(cssDocument, symbol));
}
getFoldingRanges(document) {
if (!document.styleInfo) {
return [];
}
const cssDocument = this.getCSSDoc(document);
if (shouldUseIndentBasedFolding(cssDocument.languageId)) {
return this.nonSyntacticFolding(document, document.styleInfo);
}
return this.getLanguageService(extractLanguage(cssDocument))
.getFoldingRanges(cssDocument)
.map((range) => {
const originalRange = (0, documents_1.mapRangeToOriginal)(cssDocument, {
start: { line: range.startLine, character: range.startCharacter ?? 0 },
end: { line: range.endLine, character: range.endCharacter ?? 0 }
});
return {
startLine: originalRange.start.line,
endLine: originalRange.end.line,
kind: range.kind
};
});
}
nonSyntacticFolding(document, styleInfo) {
const ranges = (0, indentFolding_1.indentBasedFoldingRangeForTag)(document, styleInfo);
const startRegion = /^\s*(\/\/|\/\*\*?)\s*#?region\b/;
const endRegion = /^\s*(\/\/|\/\*\*?)\s*#?endregion\b/;
const lines = document
.getText()
.split(/\r?\n/)
.slice(styleInfo.startPos.line, styleInfo.endPos.line);
let start = -1;
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
if (startRegion.test(line)) {
start = index;
}
else if (endRegion.test(line)) {
if (start >= 0) {
ranges.push({
startLine: start + styleInfo.startPos.line,
endLine: index + styleInfo.startPos.line,
kind: vscode_languageserver_types_1.FoldingRangeKind.Region
});
}
start = -1;
}
}
return ranges.sort((a, b) => a.startLine - b.startLine);
}
findDocumentHighlight(document, position) {
const cssDocument = this.getCSSDoc(document);
if (cssDocument.isInGenerated(position)) {
if (shouldExcludeDocumentHighlights(cssDocument)) {
return (0, wordHighlight_1.wordHighlightForTag)(document, position, document.styleInfo, wordPattern);
}
return this.findDocumentHighlightInternal(cssDocument, position);
}
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
if (attributeContext &&
this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange;
return this.findDocumentHighlightInternal(new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end, this.cssLanguageServices), position);
}
return null;
}
findDocumentHighlightInternal(cssDocument, position) {
const kind = extractLanguage(cssDocument);
const result = (0, service_1.getLanguageService)(this.cssLanguageServices, kind)
.findDocumentHighlights(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet)
.map((highlight) => (0, documents_1.mapObjWithRangeToOriginal)(cssDocument, highlight));
return result;
}
getCSSDoc(document) {
let cssDoc = this.cssDocuments.get(document);
if (!cssDoc || cssDoc.version < document.version) {
cssDoc = new CSSDocument_1.CSSDocument(document, this.cssLanguageServices);
this.cssDocuments.set(document, cssDoc);
}
return cssDoc;
}
updateConfigs() {
this.getLanguageService('css')?.configure(this.configManager.getCssConfig());
this.getLanguageService('scss')?.configure(this.configManager.getScssConfig());
this.getLanguageService('less')?.configure(this.configManager.getLessConfig());
}
featureEnabled(feature) {
return (this.configManager.enabled('css.enable') &&
this.configManager.enabled(`css.${feature}.enable`));
}
getLanguageService(kind) {
return (0, service_1.getLanguageService)(this.cssLanguageServices, kind);
}
}
exports.CSSPlugin = CSSPlugin;
function shouldExcludeValidation(kind) {
switch (kind) {
case 'postcss':
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldExcludeCompletion(kind) {
switch (kind) {
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldExcludeDocumentSymbols(document) {
switch (extractLanguage(document)) {
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldExcludeHover(document) {
switch (extractLanguage(document)) {
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldExcludeColor(document) {
switch (extractLanguage(document)) {
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldUseIndentBasedFolding(kind) {
switch (kind) {
case 'postcss':
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function shouldExcludeDocumentHighlights(document) {
switch (extractLanguage(document)) {
case 'postcss':
case 'sass':
case 'stylus':
case 'styl':
return true;
default:
return false;
}
}
function isSASS(document) {
switch (extractLanguage(document)) {
case 'sass':
return true;
default:
return false;
}
}
function extractLanguage(document) {
const lang = document.languageId;
return lang.replace(/^text\//, '');
}
//# sourceMappingURL=CSSPlugin.js.map