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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
import { CompletionsProvider, FoldingRangeProvider } from '../interfaces';
import { getEmmetCompletionParticipants, VSCodeEmmetConfig } from 'vscode-emmet-helper';
import { getLanguageService, HTMLDocument, CompletionItem as HtmlCompletionItem, Node, FoldingRange } from 'vscode-html-languageservice';
import { CompletionList, Position, CompletionItem, CompletionItemKind, TextEdit } from 'vscode-languageserver';
import type { Document, DocumentManager } from '../../core/documents';
import { isInsideExpression, isInsideFrontmatter } from '../../core/documents/utils';
import type { ConfigManager } from '../../core/config';
export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
private lang = getLanguageService();
private documents = new WeakMap<Document, HTMLDocument>();
private styleScriptTemplate = new Set(['template', 'style', 'script']);
private configManager: ConfigManager;
constructor(docManager: DocumentManager, configManager: ConfigManager) {
docManager.on('documentChange', (document) => {
this.documents.set(document, document.html);
});
this.configManager = configManager;
}
getCompletions(document: Document, position: Position): CompletionList | null {
const html = this.documents.get(document);
if (!html) {
return null;
}
if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) {
return null;
}
const emmetResults: CompletionList = {
isIncomplete: true,
items: [],
};
this.lang.setCompletionParticipants([
getEmmetCompletionParticipants(
document,
position,
'html',
this.configManager.getEmmetConfig(),
emmetResults
)
]);
const results = this.lang.doComplete(document, position, html);
const items = this.toCompletionItems(results.items);
return CompletionList.create(
[...this.toCompletionItems(items), ...this.getLangCompletions(items), ...emmetResults.items],
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0
);
}
getFoldingRanges(document: Document): FoldingRange[]|null {
const html = this.documents.get(document);
if (!html) {
return null;
}
return this.lang.getFoldingRanges(document);
}
doTagComplete(document: Document, position: Position): string | null {
const html = this.documents.get(document);
if (!html) {
return null;
}
if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) {
return null;
}
return this.lang.doTagComplete(document, position, html);
}
/**
* The HTML language service uses newer types which clash
* without the stable ones. Transform to the stable types.
*/
private toCompletionItems(items: HtmlCompletionItem[]): CompletionItem[] {
return items.map((item) => {
if (!item.textEdit || TextEdit.is(item.textEdit)) {
return item as CompletionItem;
}
return {
...item,
textEdit: TextEdit.replace(item.textEdit.replace, item.textEdit.newText),
};
});
}
private getLangCompletions(completions: CompletionItem[]): CompletionItem[] {
const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === CompletionItemKind.Property && this.styleScriptTemplate.has(completion.label));
const langCompletions: CompletionItem[] = [];
addLangCompletion('style', ['scss', 'sass']);
return langCompletions;
/** Add language completions */
function addLangCompletion(tag: string, languages: string[]) {
const existingCompletion = styleScriptTemplateCompletions.find((completion) => completion.label === tag);
if (!existingCompletion) {
return;
}
languages.forEach((lang) =>
langCompletions.push({
...existingCompletion,
label: `${tag} (lang="${lang}")`,
insertText: existingCompletion.insertText && `${existingCompletion.insertText} lang="${lang}"`,
textEdit:
existingCompletion.textEdit && TextEdit.is(existingCompletion.textEdit)
? {
range: existingCompletion.textEdit.range,
newText: `${existingCompletion.textEdit.newText} lang="${lang}"`,
}
: undefined,
})
);
}
}
private isInsideExpression(html: HTMLDocument, document: Document, position: Position) {
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
return isInsideExpression(document.getText(), node.start, offset);
}
private isInsideFrontmatter(document: Document, position: Position) {
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
}
}
|