11import tinycolor from 'tinycolor2' ;
22import { basename , extname , isObject , isDarkTheme } from '../utils.ts' ;
33import { 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+ }
423
5- const languagesByFilename = { } ;
6- const languagesByExt = { } ;
24+ const languagesByFilename : Record < string , string > = { } ;
25+ const languagesByExt : Record < string , string > = { } ;
726
8- const baseOptions = {
27+ const baseOptions : MonacoOpts = {
928 fontFamily : 'var(--fonts-monospace)' ,
1029 fontSize : 14 , // https://github.com/microsoft/monaco-editor/issues/2242
1130 guides : { bracketPairs : false , indentation : false } ,
@@ -15,21 +34,23 @@ const baseOptions = {
1534 overviewRulerLanes : 0 ,
1635 renderLineHighlight : 'all' ,
1736 renderLineHighlightOnlyWhenFocus : true ,
18- rulers : false ,
37+ rulers : [ ] ,
1938 scrollbar : { horizontalScrollbarSize : 6 , verticalScrollbarSize : 6 } ,
2039 scrollBeyondLastLine : false ,
2140 automaticLayout : true ,
2241} ;
2342
24- function getEditorconfig ( input : HTMLInputElement ) {
43+ function getEditorconfig ( input : HTMLInputElement ) : EditorConfig | null {
44+ const json = input . getAttribute ( 'data-editorconfig' ) ;
45+ if ( ! json ) return null ;
2546 try {
26- return JSON . parse ( input . getAttribute ( 'data-editorconfig' ) ) ;
47+ return JSON . parse ( json ) ;
2748 } catch {
2849 return null ;
2950 }
3051}
3152
32- function initLanguages ( monaco ) {
53+ function initLanguages ( monaco : Monaco ) : void {
3354 for ( const { filenames, extensions, id} of monaco . languages . getLanguages ( ) ) {
3455 for ( const filename of filenames || [ ] ) {
3556 languagesByFilename [ filename ] = id ;
@@ -40,35 +61,26 @@ function initLanguages(monaco) {
4061 }
4162}
4263
43- function getLanguage ( filename ) {
64+ function getLanguage ( filename : string ) : string {
4465 return languagesByFilename [ filename ] || languagesByExt [ extname ( filename ) ] || 'plaintext' ;
4566}
4667
47- function updateEditor ( monaco , editor , filename , lineWrapExts ) {
68+ function updateEditor ( monaco : Monaco , editor : IStandaloneCodeEditor , filename : string , lineWrapExts : string [ ] ) : void {
4869 editor . updateOptions ( getFileBasedOptions ( filename , lineWrapExts ) ) ;
4970 const model = editor . getModel ( ) ;
71+ if ( ! model ) return ;
5072 const language = model . getLanguageId ( ) ;
5173 const newLanguage = getLanguage ( filename ) ;
5274 if ( language !== newLanguage ) monaco . editor . setModelLanguage ( model , newLanguage ) ;
5375}
5476
5577// export editor for customization - https://github.com/go-gitea/gitea/issues/10409
56- function exportEditor ( editor ) {
78+ function exportEditor ( editor : IStandaloneCodeEditor ) : void {
5779 if ( ! window . codeEditors ) window . codeEditors = [ ] ;
5880 if ( ! window . codeEditors . includes ( editor ) ) window . codeEditors . push ( editor ) ;
5981}
6082
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 {
7284 // https://github.com/microsoft/monaco-editor/issues/2427
7385 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
7486 const styles = window . getComputedStyle ( document . documentElement ) ;
@@ -80,6 +92,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
8092 rules : [
8193 {
8294 background : getColor ( '--color-code-bg' ) ,
95+ token : '' ,
8396 } ,
8497 ] ,
8598 colors : {
@@ -101,6 +114,26 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
101114 'focusBorder' : '#0000' , // prevent blue border
102115 } ,
103116 } ) ;
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 ) ;
104137
105138 const editor = monaco . editor . create ( container , {
106139 value : textarea . value ,
@@ -114,8 +147,12 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
114147 ] ) ;
115148
116149 const model = editor . getModel ( ) ;
150+ if ( ! model ) throw new Error ( 'Unable to get editor model' ) ;
117151 model . onDidChangeContent ( ( ) => {
118- textarea . value = editor . getValue ( { preserveBOM : true } ) ;
152+ textarea . value = editor . getValue ( {
153+ preserveBOM : true ,
154+ lineEnding : '' ,
155+ } ) ;
119156 textarea . dispatchEvent ( new Event ( 'change' ) ) ; // seems to be needed for jquery-are-you-sure
120157 } ) ;
121158
@@ -127,13 +164,13 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
127164 return { monaco, editor} ;
128165}
129166
130- function getFileBasedOptions ( filename : string , lineWrapExts : string [ ] ) {
167+ function getFileBasedOptions ( filename : string , lineWrapExts : string [ ] ) : MonacoOpts {
131168 return {
132169 wordWrap : ( lineWrapExts || [ ] ) . includes ( extname ( filename ) ) ? 'on' : 'off' ,
133170 } ;
134171}
135172
136- function togglePreviewDisplay ( previewable : boolean ) {
173+ function togglePreviewDisplay ( previewable : boolean ) : void {
137174 const previewTab = document . querySelector < HTMLElement > ( 'a[data-tab="preview"]' ) ;
138175 if ( ! previewTab ) return ;
139176
@@ -145,19 +182,19 @@ function togglePreviewDisplay(previewable: boolean) {
145182 // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
146183 if ( previewTab . classList . contains ( 'active' ) ) {
147184 const writeTab = document . querySelector < HTMLElement > ( 'a[data-tab="write"]' ) ;
148- writeTab . click ( ) ;
185+ writeTab ? .click ( ) ;
149186 }
150187 }
151188}
152189
153- export async function createCodeEditor ( textarea : HTMLTextAreaElement , filenameInput : HTMLInputElement ) {
190+ export async function createCodeEditor ( textarea : HTMLTextAreaElement , filenameInput : HTMLInputElement ) : Promise < IStandaloneCodeEditor > {
154191 const filename = basename ( filenameInput . value ) ;
155192 const previewableExts = new Set ( ( textarea . getAttribute ( 'data-previewable-extensions' ) || '' ) . split ( ',' ) ) ;
156193 const lineWrapExts = ( textarea . getAttribute ( 'data-line-wrap-extensions' ) || '' ) . split ( ',' ) ;
157- const previewable = previewableExts . has ( extname ( filename ) ) ;
194+ const isPreviewable = previewableExts . has ( extname ( filename ) ) ;
158195 const editorConfig = getEditorconfig ( filenameInput ) ;
159196
160- togglePreviewDisplay ( previewable ) ;
197+ togglePreviewDisplay ( isPreviewable ) ;
161198
162199 const { monaco, editor} = await createMonaco ( textarea , filename , {
163200 ...baseOptions ,
@@ -175,14 +212,22 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn
175212 return editor ;
176213}
177214
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 { } ;
180217
181- const opts : Record < string , any > = { } ;
218+ const opts : MonacoOpts = { } ;
182219 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+
186231 opts . trimAutoWhitespace = ec . trim_trailing_whitespace === true ;
187232 opts . insertSpaces = ec . indent_style === 'space' ;
188233 opts . useTabStops = ec . indent_style === 'tab' ;
0 commit comments