Skip to content

Commit 821195d

Browse files
committed
feat: Add html tag autoclose feature
1 parent 010bdcd commit 821195d

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed

src/features/htmlContribution.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,111 @@
1+
import * as monaco from 'monaco-editor'
12
import 'monaco-editor/esm/vs/language/html/monaco.contribution'
3+
import { getConfiguration, onConfigurationChanged, registerConfigurations } from '../configuration'
24
import { registerWorkerLoader } from '../worker'
35

46
const workerLoader = async () => (await import(/* webpackChunkName: "MonacoHtmlWorker" */'monaco-editor/esm/vs/language/html/html.worker?worker')).default
57
registerWorkerLoader('html', workerLoader)
68
registerWorkerLoader('handlebars', workerLoader)
79
registerWorkerLoader('razor', workerLoader)
10+
11+
/**
12+
* Start autoclosing html tags
13+
* Inspired by https://github.com/microsoft/monaco-editor/issues/221
14+
* and https://github.com/microsoft/vscode/blob/5943dac8ba32df1963a1760c660b3f16f5fcdc51/extensions/html-language-features/client/src/autoInsertion.ts
15+
*/
16+
registerConfigurations([{
17+
id: 'html',
18+
order: 20,
19+
type: 'object',
20+
title: 'HTML',
21+
properties: {
22+
'html.autoClosingTags': {
23+
type: 'boolean',
24+
scope: monaco.extra.ConfigurationScope.RESOURCE,
25+
default: true,
26+
description: 'Enable/disable autoclosing of HTML tags.'
27+
}
28+
}
29+
}])
30+
31+
let autoClosingTags = getConfiguration<boolean>(undefined, 'html.autoClosingTags')!
32+
onConfigurationChanged(e => {
33+
if (e.affectsConfiguration('html.autoClosingTags')) {
34+
autoClosingTags = getConfiguration<boolean>(undefined, 'html.autoClosingTags')!
35+
}
36+
})
37+
38+
function autoCloseHtmlTags (editor: monaco.editor.ICodeEditor): monaco.IDisposable {
39+
const disposableStore = new monaco.DisposableStore()
40+
41+
let timeout: number | undefined
42+
disposableStore.add({
43+
dispose: () => {
44+
if (timeout != null) {
45+
window.clearTimeout(timeout)
46+
}
47+
}
48+
})
49+
50+
disposableStore.add(editor.onDidChangeModelContent(e => {
51+
if (timeout != null) {
52+
window.clearTimeout(timeout)
53+
timeout = undefined
54+
}
55+
56+
if (!autoClosingTags || e.isRedoing || e.isUndoing) {
57+
return
58+
}
59+
const model = editor.getModel()
60+
if (model == null || model.getLanguageId() !== 'html') {
61+
return
62+
}
63+
64+
timeout = window.setTimeout(() => {
65+
for (const { range, rangeLength, text } of e.changes) {
66+
if (!text.endsWith('>')) {
67+
continue
68+
}
69+
const untilLine = model.getValueInRange({
70+
startLineNumber: 1,
71+
startColumn: 1,
72+
endLineNumber: range.endLineNumber,
73+
endColumn: range.endColumn + rangeLength + 1
74+
})
75+
76+
const enclosingTag = untilLine.match(/.*<(\w+)>$/)?.[1]
77+
78+
if (enclosingTag == null || enclosingTag.includes('/')) {
79+
return
80+
}
81+
82+
const newRange = new monaco.Range(
83+
range.endLineNumber,
84+
range.endColumn + rangeLength + 1,
85+
range.endLineNumber,
86+
range.endColumn + rangeLength + enclosingTag.length + 1
87+
)
88+
89+
const rest = model.getValueInRange(newRange)
90+
model.applyEdits([{
91+
range: newRange,
92+
text: `</${enclosingTag}>${rest}`,
93+
forceMoveMarkers: true
94+
}])
95+
}
96+
}, 100)
97+
}))
98+
99+
return disposableStore
100+
}
101+
102+
const codeEditors = monaco.extra.StandaloneServices.get(monaco.extra.ICodeEditorService).listCodeEditors()
103+
for (const editor of codeEditors) {
104+
autoCloseHtmlTags(editor)
105+
}
106+
monaco.editor.onDidCreateEditor(editor => {
107+
autoCloseHtmlTags(editor)
108+
})
109+
/**
110+
* End autoclosing html tags
111+
*/

0 commit comments

Comments
 (0)