1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
import type { CompletionsProvider } from '../interfaces';
import type { Document, DocumentManager } from '../../core/documents';
import type { ConfigManager } from '../../core/config';
import { getEmmetCompletionParticipants, doComplete as doEmmetComplete } from 'vscode-emmet-helper';
import { CompletionContext, CompletionList, CompletionTriggerKind, Position } from 'vscode-languageserver';
import { isInsideFrontmatter } from '../../core/documents/utils';
import { CSSDocument, CSSDocumentBase } from './CSSDocument';
import { getLanguage, getLanguageService } from './service';
import { StyleAttributeDocument } from './StyleAttributeDocument';
import { mapCompletionItemToOriginal } from '../../core/documents';
import { AttributeContext, getAttributeContextAtPosition } from '../../core/documents/parseHtml';
import { getIdClassCompletion } from './features/getIdClassCompletion';
export class CSSPlugin implements CompletionsProvider {
private docManager: DocumentManager;
private configManager: ConfigManager;
private documents = new WeakMap<Document, CSSDocument>();
private triggerCharacters = new Set(['.', ':', '-', '/']);
constructor(docManager: DocumentManager, configManager: ConfigManager) {
this.docManager = docManager;
this.configManager = configManager;
this.docManager.on('documentChange', (document) => {
this.documents.set(document, new CSSDocument(document));
});
}
getCompletions(document: Document, position: Position, completionContext?: CompletionContext): CompletionList | null {
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
return null;
}
if (this.isInsideFrontmatter(document, position)) {
return null;
}
const cssDocument = this.getCSSDoc(document);
if (cssDocument.isInGenerated(position)) {
return this.getCompletionsInternal(document, position, cssDocument);
}
const attributeContext = 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(document, start, end));
} else {
return getIdClassCompletion(cssDocument, attributeContext);
}
}
private getCompletionsInternal(document: Document, position: Position, cssDocument: CSSDocumentBase) {
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 doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig());
}
const type = extractLanguage(cssDocument);
const lang = getLanguageService(type);
const emmetResults: CompletionList = {
isIncomplete: true,
items: [],
};
if (false /* this.configManager.getConfig().css.completions.emmet */) {
lang.setCompletionParticipants([
getEmmetCompletionParticipants(cssDocument, cssDocument.getGeneratedPosition(position), getLanguage(type), this.configManager.getEmmetConfig(), emmetResults),
]);
}
const results = lang.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
return CompletionList.create(
[...(results ? results.items : []), ...emmetResults.items].map((completionItem) => mapCompletionItemToOriginal(cssDocument, completionItem)),
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0
);
}
private inStyleAttributeWithoutInterpolation(attrContext: AttributeContext, text: string): attrContext is Required<AttributeContext> {
return attrContext.name === 'style' && !!attrContext.valueRange && !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{');
}
private getCSSDoc(document: Document) {
let cssDoc = this.documents.get(document);
if (!cssDoc || cssDoc.version < document.version) {
cssDoc = new CSSDocument(document);
this.documents.set(document, cssDoc);
}
return cssDoc;
}
private isInsideFrontmatter(document: Document, position: Position) {
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
}
}
function isSASS(document: CSSDocumentBase) {
switch (extractLanguage(document)) {
case 'sass':
return true;
default:
return false;
}
}
function extractLanguage(document: CSSDocumentBase): string {
const lang = document.languageId;
return lang.replace(/^text\//, '');
}
|