275 lines
13 KiB
JavaScript
275 lines
13 KiB
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("../../core/documents");
|
||
|
const parseHtml_1 = require("../../core/documents/parseHtml");
|
||
|
const CSSDocument_1 = require("./CSSDocument");
|
||
|
const getIdClassCompletions_1 = require("./features/getIdClassCompletions");
|
||
|
const language_service_1 = require("./language-service");
|
||
|
const StyleAttributeDocument_1 = require("./StyleAttributeDocument");
|
||
|
class CSSPlugin {
|
||
|
constructor(configManager) {
|
||
|
this.__name = 'css';
|
||
|
this.cssDocuments = new WeakMap();
|
||
|
this.triggerCharacters = new Set(['.', ':', '-', '/']);
|
||
|
this.configManager = configManager;
|
||
|
}
|
||
|
async doHover(document, position) {
|
||
|
if (!(await this.featureEnabled(document, 'hover'))) {
|
||
|
return null;
|
||
|
}
|
||
|
if ((0, documents_1.isInsideFrontmatter)(document.getText(), document.offsetAt(position))) {
|
||
|
return null;
|
||
|
}
|
||
|
const styleTag = this.getStyleTagForPosition(document, position);
|
||
|
// We technically can return results even for open tags, however, a lot of the info returned is not valid
|
||
|
// Since most editors will automatically close the tag before the user start working in them, this shouldn't be a problem
|
||
|
if (styleTag && !styleTag.closed) {
|
||
|
return null;
|
||
|
}
|
||
|
// If we don't have a style tag at this position, we might be in a style property instead, let's check
|
||
|
if (!styleTag) {
|
||
|
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.doHoverInternal(new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end), position);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
|
||
|
const cssLang = extractLanguage(cssDocument);
|
||
|
if (!isSupportedByLangService(cssLang)) {
|
||
|
return null;
|
||
|
}
|
||
|
return this.doHoverInternal(cssDocument, position);
|
||
|
}
|
||
|
doHoverInternal(cssDocument, position) {
|
||
|
const hoverInfo = (0, language_service_1.getLanguageService)(extractLanguage(cssDocument)).doHover(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
|
||
|
return hoverInfo ? (0, documents_1.mapHoverToParent)(cssDocument, hoverInfo) : hoverInfo;
|
||
|
}
|
||
|
async getCompletions(document, position, completionContext) {
|
||
|
if (!(await this.featureEnabled(document, 'completions'))) {
|
||
|
return null;
|
||
|
}
|
||
|
if ((0, documents_1.isInsideFrontmatter)(document.getText(), document.offsetAt(position))) {
|
||
|
return null;
|
||
|
}
|
||
|
const triggerCharacter = completionContext?.triggerCharacter;
|
||
|
const triggerKind = completionContext?.triggerKind;
|
||
|
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
|
||
|
if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
|
||
|
return null;
|
||
|
}
|
||
|
const styleTag = this.getStyleTagForPosition(document, position);
|
||
|
if (styleTag && !styleTag.closed) {
|
||
|
return null;
|
||
|
}
|
||
|
if (!styleTag) {
|
||
|
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
|
||
|
if (!attributeContext) {
|
||
|
return null;
|
||
|
}
|
||
|
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
|
||
|
const [start, end] = attributeContext.valueRange;
|
||
|
return await this.getCompletionsInternal(document, position, new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end));
|
||
|
}
|
||
|
// If we're not in a style attribute, instead give completions for ids and classes used in the current document
|
||
|
else if ((attributeContext.name == 'id' || attributeContext.name == 'class') && attributeContext.inValue) {
|
||
|
const stylesheets = this.getStylesheetsForDocument(document);
|
||
|
return (0, getIdClassCompletions_1.getIdClassCompletion)(stylesheets, attributeContext);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
|
||
|
return await this.getCompletionsInternal(document, position, cssDocument);
|
||
|
}
|
||
|
async getCompletionsInternal(document, position, cssDocument) {
|
||
|
const emmetConfig = await this.configManager.getEmmetConfig(document);
|
||
|
if (isSASS(cssDocument)) {
|
||
|
// The CSS language service does not support SASS (not to be confused with SCSS)
|
||
|
// however we can at least still at least provide Emmet completions in SASS blocks
|
||
|
return (0, emmet_helper_1.doComplete)(document, position, 'sass', emmetConfig) || null;
|
||
|
}
|
||
|
const cssLang = extractLanguage(cssDocument);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
let emmetResults = {
|
||
|
isIncomplete: false,
|
||
|
items: [],
|
||
|
};
|
||
|
const extensionConfig = await this.configManager.getConfig('astro', document.uri);
|
||
|
if (extensionConfig?.css?.completions?.emmet ?? true) {
|
||
|
langService.setCompletionParticipants([
|
||
|
{
|
||
|
onCssProperty: (context) => {
|
||
|
if (context?.propertyName) {
|
||
|
emmetResults =
|
||
|
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, language_service_1.getLanguage)(cssLang), emmetConfig) || emmetResults;
|
||
|
}
|
||
|
},
|
||
|
onCssPropertyValue: (context) => {
|
||
|
if (context?.propertyValue) {
|
||
|
emmetResults =
|
||
|
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, language_service_1.getLanguage)(cssLang), emmetConfig) || emmetResults;
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
]);
|
||
|
}
|
||
|
const results = langService.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
|
||
|
return vscode_languageserver_1.CompletionList.create([...(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);
|
||
|
}
|
||
|
async getDocumentColors(document) {
|
||
|
if (!(await this.featureEnabled(document, 'documentColors'))) {
|
||
|
return [];
|
||
|
}
|
||
|
const allColorInfo = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
|
||
|
const cssLang = extractLanguage(cssDoc);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
if (!isSupportedByLangService(cssLang)) {
|
||
|
return [];
|
||
|
}
|
||
|
return langService
|
||
|
.findDocumentColors(cssDoc, cssDoc.stylesheet)
|
||
|
.map((colorInfo) => (0, documents_1.mapObjWithRangeToOriginal)(cssDoc, colorInfo));
|
||
|
});
|
||
|
return allColorInfo;
|
||
|
}
|
||
|
async getColorPresentations(document, range, color) {
|
||
|
if (!(await this.featureEnabled(document, 'documentColors'))) {
|
||
|
return [];
|
||
|
}
|
||
|
const allColorPres = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
|
||
|
const cssLang = extractLanguage(cssDoc);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
if ((!cssDoc.isInGenerated(range.start) && !cssDoc.isInGenerated(range.end)) ||
|
||
|
!isSupportedByLangService(cssLang)) {
|
||
|
return [];
|
||
|
}
|
||
|
return langService
|
||
|
.getColorPresentations(cssDoc, cssDoc.stylesheet, color, (0, documents_1.mapRangeToGenerated)(cssDoc, range))
|
||
|
.map((colorPres) => (0, documents_1.mapColorPresentationToOriginal)(cssDoc, colorPres));
|
||
|
});
|
||
|
return allColorPres;
|
||
|
}
|
||
|
prepareRename(document, position) {
|
||
|
const styleTag = this.getStyleTagForPosition(document, position);
|
||
|
if (!styleTag) {
|
||
|
return null;
|
||
|
}
|
||
|
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
|
||
|
const cssLang = extractLanguage(cssDocument);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
const range = langService.prepareRename(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
|
||
|
if (!range) {
|
||
|
return null;
|
||
|
}
|
||
|
return (0, documents_1.mapRangeToOriginal)(cssDocument, range);
|
||
|
}
|
||
|
rename(document, position, newName) {
|
||
|
const styleTag = this.getStyleTagForPosition(document, position);
|
||
|
if (!styleTag) {
|
||
|
return null;
|
||
|
}
|
||
|
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
|
||
|
const cssLang = extractLanguage(cssDocument);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
const renames = langService.doRename(cssDocument, cssDocument.getGeneratedPosition(position), newName, cssDocument.stylesheet);
|
||
|
if (renames?.changes?.[document.uri]) {
|
||
|
renames.changes[document.uri] = renames?.changes?.[document.uri].map((edit) => (0, documents_1.mapObjWithRangeToOriginal)(cssDocument, edit));
|
||
|
}
|
||
|
return renames;
|
||
|
}
|
||
|
getFoldingRanges(document) {
|
||
|
const allFoldingRanges = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
|
||
|
const cssLang = extractLanguage(cssDoc);
|
||
|
const langService = (0, language_service_1.getLanguageService)(cssLang);
|
||
|
return langService.getFoldingRanges(cssDoc).map((foldingRange) => (0, documents_1.mapFoldingRangeToParent)(cssDoc, foldingRange));
|
||
|
});
|
||
|
return allFoldingRanges;
|
||
|
}
|
||
|
async getDocumentSymbols(document) {
|
||
|
if (!(await this.featureEnabled(document, 'documentSymbols'))) {
|
||
|
return [];
|
||
|
}
|
||
|
const allDocumentSymbols = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
|
||
|
return (0, language_service_1.getLanguageService)(extractLanguage(cssDoc))
|
||
|
.findDocumentSymbols(cssDoc, cssDoc.stylesheet)
|
||
|
.map((symbol) => (0, documents_1.mapSymbolInformationToOriginal)(cssDoc, symbol));
|
||
|
});
|
||
|
return allDocumentSymbols;
|
||
|
}
|
||
|
inStyleAttributeWithoutInterpolation(attrContext, text) {
|
||
|
return (attrContext.name === 'style' &&
|
||
|
!!attrContext.valueRange &&
|
||
|
!text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{'));
|
||
|
}
|
||
|
/**
|
||
|
* Get the associated CSS Document for a style tag
|
||
|
*/
|
||
|
getCSSDocumentForStyleTag(tag, document) {
|
||
|
let cssDoc = this.cssDocuments.get(tag);
|
||
|
if (!cssDoc || cssDoc.version < document.version) {
|
||
|
cssDoc = new CSSDocument_1.CSSDocument(document, tag);
|
||
|
this.cssDocuments.set(tag, cssDoc);
|
||
|
}
|
||
|
return cssDoc;
|
||
|
}
|
||
|
/**
|
||
|
* Get all the CSSDocuments in a document
|
||
|
*/
|
||
|
getCSSDocumentsForDocument(document) {
|
||
|
return document.styleTags.map((tag) => this.getCSSDocumentForStyleTag(tag, document));
|
||
|
}
|
||
|
/**
|
||
|
* Get all the stylesheets (Stylesheet type) in a document
|
||
|
*/
|
||
|
getStylesheetsForDocument(document) {
|
||
|
return this.getCSSDocumentsForDocument(document).map((cssDoc) => cssDoc.stylesheet);
|
||
|
}
|
||
|
/**
|
||
|
* Get style tag at position for a document
|
||
|
*/
|
||
|
getStyleTagForPosition(document, position) {
|
||
|
return document.styleTags.find((styleTag) => {
|
||
|
return (0, documents_1.isInTag)(position, styleTag);
|
||
|
});
|
||
|
}
|
||
|
async featureEnabled(document, feature) {
|
||
|
return ((await this.configManager.isEnabled(document, 'css')) &&
|
||
|
(await this.configManager.isEnabled(document, 'css', feature)));
|
||
|
}
|
||
|
}
|
||
|
exports.CSSPlugin = CSSPlugin;
|
||
|
/**
|
||
|
* Check is a CSSDocument's language is supported by the CSS language service
|
||
|
*/
|
||
|
function isSupportedByLangService(language) {
|
||
|
switch (language) {
|
||
|
case 'css':
|
||
|
case 'scss':
|
||
|
case 'less':
|
||
|
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\//, '');
|
||
|
}
|