1
1
import tinycolor from 'tinycolor2' ;
2
2
import { basename , extname , isObject , isDarkTheme } from '../utils.ts' ;
3
3
import { onInputDebounce } from '../utils/dom.ts' ;
4
+ import type MonacoNamespace from 'monaco-editor' ;
5
+
6
+ type Monaco = typeof MonacoNamespace ;
7
+ type IStandaloneCodeEditor = MonacoNamespace . editor . IStandaloneCodeEditor ;
8
+ type IEditorOptions = MonacoNamespace . editor . IEditorOptions ;
9
+ type IGlobalEditorOptions = MonacoNamespace . editor . IGlobalEditorOptions ;
10
+ type ITextModelUpdateOptions = MonacoNamespace . editor . ITextModelUpdateOptions ;
11
+ type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOptions ;
12
+
13
+ type EditorConfig = {
14
+ indent_style ?: 'tab' | 'space' ,
15
+ indent_size ?: string | number , // backend emits this as string
16
+ tab_width ?: string | number , // backend emits this as string
17
+ end_of_line ?: 'lf' | 'cr' | 'crlf' ,
18
+ charset ?: 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le' ,
19
+ trim_trailing_whitespace ?: boolean ,
20
+ insert_final_newline ?: boolean ,
21
+ root ?: boolean ,
22
+ }
4
23
5
- const languagesByFilename = { } ;
6
- const languagesByExt = { } ;
24
+ const languagesByFilename : Record < string , string > = { } ;
25
+ const languagesByExt : Record < string , string > = { } ;
7
26
8
- const baseOptions = {
27
+ const baseOptions : MonacoOpts = {
9
28
fontFamily : 'var(--fonts-monospace)' ,
10
29
fontSize : 14 , // https://github.com/microsoft/monaco-editor/issues/2242
11
30
guides : { bracketPairs : false , indentation : false } ,
@@ -15,21 +34,23 @@ const baseOptions = {
15
34
overviewRulerLanes : 0 ,
16
35
renderLineHighlight : 'all' ,
17
36
renderLineHighlightOnlyWhenFocus : true ,
18
- rulers : false ,
37
+ rulers : [ ] ,
19
38
scrollbar : { horizontalScrollbarSize : 6 , verticalScrollbarSize : 6 } ,
20
39
scrollBeyondLastLine : false ,
21
40
automaticLayout : true ,
22
41
} ;
23
42
24
- function getEditorconfig ( input : HTMLInputElement ) {
43
+ function getEditorconfig ( input : HTMLInputElement ) : EditorConfig | null {
44
+ const json = input . getAttribute ( 'data-editorconfig' ) ;
45
+ if ( ! json ) return null ;
25
46
try {
26
- return JSON . parse ( input . getAttribute ( 'data-editorconfig' ) ) ;
47
+ return JSON . parse ( json ) ;
27
48
} catch {
28
49
return null ;
29
50
}
30
51
}
31
52
32
- function initLanguages ( monaco ) {
53
+ function initLanguages ( monaco : Monaco ) : void {
33
54
for ( const { filenames, extensions, id} of monaco . languages . getLanguages ( ) ) {
34
55
for ( const filename of filenames || [ ] ) {
35
56
languagesByFilename [ filename ] = id ;
@@ -40,35 +61,26 @@ function initLanguages(monaco) {
40
61
}
41
62
}
42
63
43
- function getLanguage ( filename ) {
64
+ function getLanguage ( filename : string ) : string {
44
65
return languagesByFilename [ filename ] || languagesByExt [ extname ( filename ) ] || 'plaintext' ;
45
66
}
46
67
47
- function updateEditor ( monaco , editor , filename , lineWrapExts ) {
68
+ function updateEditor ( monaco : Monaco , editor : IStandaloneCodeEditor , filename : string , lineWrapExts : string [ ] ) : void {
48
69
editor . updateOptions ( getFileBasedOptions ( filename , lineWrapExts ) ) ;
49
70
const model = editor . getModel ( ) ;
71
+ if ( ! model ) return ;
50
72
const language = model . getLanguageId ( ) ;
51
73
const newLanguage = getLanguage ( filename ) ;
52
74
if ( language !== newLanguage ) monaco . editor . setModelLanguage ( model , newLanguage ) ;
53
75
}
54
76
55
77
// export editor for customization - https://github.com/go-gitea/gitea/issues/10409
56
- function exportEditor ( editor ) {
78
+ function exportEditor ( editor : IStandaloneCodeEditor ) : void {
57
79
if ( ! window . codeEditors ) window . codeEditors = [ ] ;
58
80
if ( ! window . codeEditors . includes ( editor ) ) window . codeEditors . push ( editor ) ;
59
81
}
60
82
61
- export async function createMonaco ( textarea : HTMLTextAreaElement , filename : string , editorOpts : Record < string , any > ) {
62
- const monaco = await import ( /* webpackChunkName: "monaco" */ 'monaco-editor' ) ;
63
-
64
- initLanguages ( monaco ) ;
65
- let { language, ...other } = editorOpts ;
66
- if ( ! language ) language = getLanguage ( filename ) ;
67
-
68
- const container = document . createElement ( 'div' ) ;
69
- container . className = 'monaco-editor-container' ;
70
- textarea . parentNode . append ( container ) ;
71
-
83
+ function updateTheme ( monaco : Monaco ) : void {
72
84
// https://github.com/microsoft/monaco-editor/issues/2427
73
85
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
74
86
const styles = window . getComputedStyle ( document . documentElement ) ;
@@ -80,6 +92,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
80
92
rules : [
81
93
{
82
94
background : getColor ( '--color-code-bg' ) ,
95
+ token : '' ,
83
96
} ,
84
97
] ,
85
98
colors : {
@@ -101,6 +114,26 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
101
114
'focusBorder' : '#0000' , // prevent blue border
102
115
} ,
103
116
} ) ;
117
+ }
118
+
119
+ type CreateMonacoOpts = MonacoOpts & { language ?: string } ;
120
+
121
+ export async function createMonaco ( textarea : HTMLTextAreaElement , filename : string , opts : CreateMonacoOpts ) : Promise < { monaco : Monaco , editor : IStandaloneCodeEditor } > {
122
+ const monaco = await import ( /* webpackChunkName: "monaco" */ 'monaco-editor' ) ;
123
+
124
+ initLanguages ( monaco ) ;
125
+ let { language, ...other } = opts ;
126
+ if ( ! language ) language = getLanguage ( filename ) ;
127
+
128
+ const container = document . createElement ( 'div' ) ;
129
+ container . className = 'monaco-editor-container' ;
130
+ if ( ! textarea . parentNode ) throw new Error ( 'Parent node absent' ) ;
131
+ textarea . parentNode . append ( container ) ;
132
+
133
+ window . matchMedia ( '(prefers-color-scheme: dark)' ) . addEventListener ( 'change' , ( ) => {
134
+ updateTheme ( monaco ) ;
135
+ } ) ;
136
+ updateTheme ( monaco ) ;
104
137
105
138
const editor = monaco . editor . create ( container , {
106
139
value : textarea . value ,
@@ -114,8 +147,12 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
114
147
] ) ;
115
148
116
149
const model = editor . getModel ( ) ;
150
+ if ( ! model ) throw new Error ( 'Unable to get editor model' ) ;
117
151
model . onDidChangeContent ( ( ) => {
118
- textarea . value = editor . getValue ( { preserveBOM : true } ) ;
152
+ textarea . value = editor . getValue ( {
153
+ preserveBOM : true ,
154
+ lineEnding : '' ,
155
+ } ) ;
119
156
textarea . dispatchEvent ( new Event ( 'change' ) ) ; // seems to be needed for jquery-are-you-sure
120
157
} ) ;
121
158
@@ -127,13 +164,13 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
127
164
return { monaco, editor} ;
128
165
}
129
166
130
- function getFileBasedOptions ( filename : string , lineWrapExts : string [ ] ) {
167
+ function getFileBasedOptions ( filename : string , lineWrapExts : string [ ] ) : MonacoOpts {
131
168
return {
132
169
wordWrap : ( lineWrapExts || [ ] ) . includes ( extname ( filename ) ) ? 'on' : 'off' ,
133
170
} ;
134
171
}
135
172
136
- function togglePreviewDisplay ( previewable : boolean ) {
173
+ function togglePreviewDisplay ( previewable : boolean ) : void {
137
174
const previewTab = document . querySelector < HTMLElement > ( 'a[data-tab="preview"]' ) ;
138
175
if ( ! previewTab ) return ;
139
176
@@ -145,19 +182,19 @@ function togglePreviewDisplay(previewable: boolean) {
145
182
// then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
146
183
if ( previewTab . classList . contains ( 'active' ) ) {
147
184
const writeTab = document . querySelector < HTMLElement > ( 'a[data-tab="write"]' ) ;
148
- writeTab . click ( ) ;
185
+ writeTab ? .click ( ) ;
149
186
}
150
187
}
151
188
}
152
189
153
- export async function createCodeEditor ( textarea : HTMLTextAreaElement , filenameInput : HTMLInputElement ) {
190
+ export async function createCodeEditor ( textarea : HTMLTextAreaElement , filenameInput : HTMLInputElement ) : Promise < IStandaloneCodeEditor > {
154
191
const filename = basename ( filenameInput . value ) ;
155
192
const previewableExts = new Set ( ( textarea . getAttribute ( 'data-previewable-extensions' ) || '' ) . split ( ',' ) ) ;
156
193
const lineWrapExts = ( textarea . getAttribute ( 'data-line-wrap-extensions' ) || '' ) . split ( ',' ) ;
157
- const previewable = previewableExts . has ( extname ( filename ) ) ;
194
+ const isPreviewable = previewableExts . has ( extname ( filename ) ) ;
158
195
const editorConfig = getEditorconfig ( filenameInput ) ;
159
196
160
- togglePreviewDisplay ( previewable ) ;
197
+ togglePreviewDisplay ( isPreviewable ) ;
161
198
162
199
const { monaco, editor} = await createMonaco ( textarea , filename , {
163
200
...baseOptions ,
@@ -175,14 +212,22 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn
175
212
return editor ;
176
213
}
177
214
178
- function getEditorConfigOptions ( ec : Record < string , any > ) : Record < string , any > {
179
- if ( ! isObject ( ec ) ) return { } ;
215
+ function getEditorConfigOptions ( ec : EditorConfig | null ) : MonacoOpts {
216
+ if ( ! ec || ! isObject ( ec ) ) return { } ;
180
217
181
- const opts : Record < string , any > = { } ;
218
+ const opts : MonacoOpts = { } ;
182
219
opts . detectIndentation = ! ( 'indent_style' in ec ) || ! ( 'indent_size' in ec ) ;
183
- if ( 'indent_size' in ec ) opts . indentSize = Number ( ec . indent_size ) ;
184
- if ( 'tab_width' in ec ) opts . tabSize = Number ( ec . tab_width ) || opts . indentSize ;
185
- if ( 'max_line_length' in ec ) opts . rulers = [ Number ( ec . max_line_length ) ] ;
220
+
221
+ if ( 'indent_size' in ec ) {
222
+ opts . indentSize = Number ( ec . indent_size ) ;
223
+ }
224
+ if ( 'tab_width' in ec ) {
225
+ opts . tabSize = Number ( ec . tab_width ) || Number ( ec . indent_size ) ;
226
+ }
227
+ if ( 'max_line_length' in ec ) {
228
+ opts . rulers = [ Number ( ec . max_line_length ) ] ;
229
+ }
230
+
186
231
opts . trimAutoWhitespace = ec . trim_trailing_whitespace === true ;
187
232
opts . insertSpaces = ec . indent_style === 'space' ;
188
233
opts . useTabStops = ec . indent_style === 'tab' ;
0 commit comments