2023-07-09 13:17:22 +03:00
import tinycolor from 'tinycolor2' ;
2024-07-07 18:32:30 +03:00
import { basename , extname , isObject , isDarkTheme } from '../utils.ts' ;
import { onInputDebounce } from '../utils/dom.ts' ;
2024-12-09 20:03:36 +03:00
import type MonacoNamespace from 'monaco-editor' ;
type Monaco = typeof MonacoNamespace ;
type IStandaloneCodeEditor = MonacoNamespace . editor . IStandaloneCodeEditor ;
type IEditorOptions = MonacoNamespace . editor . IEditorOptions ;
type IGlobalEditorOptions = MonacoNamespace . editor . IGlobalEditorOptions ;
type ITextModelUpdateOptions = MonacoNamespace . editor . ITextModelUpdateOptions ;
type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOptions ;
type EditorConfig = {
indent_style ? : 'tab' | 'space' ,
indent_size? : string | number , // backend emits this as string
tab_width? : string | number , // backend emits this as string
end_of_line ? : 'lf' | 'cr' | 'crlf' ,
charset ? : 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le' ,
trim_trailing_whitespace? : boolean ,
insert_final_newline? : boolean ,
root? : boolean ,
}
2020-05-14 19:06:01 +03:00
2024-12-09 20:03:36 +03:00
const languagesByFilename : Record < string , string > = { } ;
const languagesByExt : Record < string , string > = { } ;
2020-05-14 19:06:01 +03:00
2024-12-09 20:03:36 +03:00
const baseOptions : MonacoOpts = {
2021-04-08 12:53:00 +03:00
fontFamily : 'var(--fonts-monospace)' ,
fontSize : 14 , // https://github.com/microsoft/monaco-editor/issues/2242
2021-10-19 10:23:58 +03:00
guides : { bracketPairs : false , indentation : false } ,
2021-04-08 12:53:00 +03:00
links : false ,
minimap : { enabled : false } ,
2023-12-30 08:29:03 +03:00
occurrencesHighlight : 'off' ,
2021-04-08 12:53:00 +03:00
overviewRulerLanes : 0 ,
renderLineHighlight : 'all' ,
renderLineHighlightOnlyWhenFocus : true ,
2024-12-09 20:03:36 +03:00
rulers : [ ] ,
2021-04-08 12:53:00 +03:00
scrollbar : { horizontalScrollbarSize : 6 , verticalScrollbarSize : 6 } ,
scrollBeyondLastLine : false ,
2022-10-20 04:54:18 +03:00
automaticLayout : true ,
2021-04-08 12:53:00 +03:00
} ;
2024-12-09 20:03:36 +03:00
function getEditorconfig ( input : HTMLInputElement ) : EditorConfig | null {
const json = input . getAttribute ( 'data-editorconfig' ) ;
if ( ! json ) return null ;
2020-05-14 19:06:01 +03:00
try {
2024-12-09 20:03:36 +03:00
return JSON . parse ( json ) ;
2020-06-10 00:31:15 +03:00
} catch {
2020-05-14 19:06:01 +03:00
return null ;
}
}
2024-12-09 20:03:36 +03:00
function initLanguages ( monaco : Monaco ) : void {
2020-05-14 19:06:01 +03:00
for ( const { filenames , extensions , id } of monaco . languages . getLanguages ( ) ) {
for ( const filename of filenames || [ ] ) {
languagesByFilename [ filename ] = id ;
}
for ( const extension of extensions || [ ] ) {
languagesByExt [ extension ] = id ;
}
2024-12-14 06:10:20 +03:00
if ( id === 'typescript' ) {
monaco . languages . typescript . typescriptDefaults . setCompilerOptions ( {
// this is needed to suppress error annotations in tsx regarding missing --jsx flag.
jsx : monaco.languages.typescript.JsxEmit.Preserve ,
} ) ;
}
2020-05-14 19:06:01 +03:00
}
}
2024-12-09 20:03:36 +03:00
function getLanguage ( filename : string ) : string {
2020-05-14 19:06:01 +03:00
return languagesByFilename [ filename ] || languagesByExt [ extname ( filename ) ] || 'plaintext' ;
}
2024-12-09 20:03:36 +03:00
function updateEditor ( monaco : Monaco , editor : IStandaloneCodeEditor , filename : string , lineWrapExts : string [ ] ) : void {
2021-04-08 12:53:00 +03:00
editor . updateOptions ( getFileBasedOptions ( filename , lineWrapExts ) ) ;
2020-05-14 19:06:01 +03:00
const model = editor . getModel ( ) ;
2024-12-09 20:03:36 +03:00
if ( ! model ) return ;
2021-11-11 04:52:16 +03:00
const language = model . getLanguageId ( ) ;
2020-11-14 06:57:34 +03:00
const newLanguage = getLanguage ( filename ) ;
2020-05-14 19:06:01 +03:00
if ( language !== newLanguage ) monaco . editor . setModelLanguage ( model , newLanguage ) ;
2024-12-14 06:10:20 +03:00
// TODO: Need to update the model uri with the new filename, but there is no easy way currently, see
// https://github.com/microsoft/monaco-editor/discussions/3751
2020-05-14 19:06:01 +03:00
}
2020-06-03 14:19:32 +03:00
// export editor for customization - https://github.com/go-gitea/gitea/issues/10409
2024-12-09 20:03:36 +03:00
function exportEditor ( editor : IStandaloneCodeEditor ) : void {
2020-06-03 14:19:32 +03:00
if ( ! window . codeEditors ) window . codeEditors = [ ] ;
if ( ! window . codeEditors . includes ( editor ) ) window . codeEditors . push ( editor ) ;
}
2024-12-09 20:03:36 +03:00
function updateTheme ( monaco : Monaco ) : void {
2021-04-08 12:53:00 +03:00
// https://github.com/microsoft/monaco-editor/issues/2427
2023-07-09 13:17:22 +03:00
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
2021-04-08 12:53:00 +03:00
const styles = window . getComputedStyle ( document . documentElement ) ;
2024-11-21 16:57:42 +03:00
const getColor = ( name : string ) = > tinycolor ( styles . getPropertyValue ( name ) . trim ( ) ) . toString ( 'hex6' ) ;
2021-04-08 12:53:00 +03:00
monaco . editor . defineTheme ( 'gitea' , {
base : isDarkTheme ( ) ? 'vs-dark' : 'vs' ,
inherit : true ,
rules : [
{
2023-07-09 13:17:22 +03:00
background : getColor ( '--color-code-bg' ) ,
2024-12-09 20:03:36 +03:00
token : '' ,
2024-03-22 17:06:53 +03:00
} ,
2021-04-08 12:53:00 +03:00
] ,
colors : {
2023-07-09 13:17:22 +03:00
'editor.background' : getColor ( '--color-code-bg' ) ,
'editor.foreground' : getColor ( '--color-text' ) ,
'editor.inactiveSelectionBackground' : getColor ( '--color-primary-light-4' ) ,
'editor.lineHighlightBackground' : getColor ( '--color-editor-line-highlight' ) ,
'editor.selectionBackground' : getColor ( '--color-primary-light-3' ) ,
'editor.selectionForeground' : getColor ( '--color-primary-light-3' ) ,
'editorLineNumber.background' : getColor ( '--color-code-bg' ) ,
'editorLineNumber.foreground' : getColor ( '--color-secondary-dark-6' ) ,
'editorWidget.background' : getColor ( '--color-body' ) ,
'editorWidget.border' : getColor ( '--color-secondary' ) ,
'input.background' : getColor ( '--color-input-background' ) ,
'input.border' : getColor ( '--color-input-border' ) ,
'input.foreground' : getColor ( '--color-input-text' ) ,
2024-07-15 02:22:48 +03:00
'scrollbar.shadow' : getColor ( '--color-shadow-opaque' ) ,
2023-07-09 13:17:22 +03:00
'progressBar.background' : getColor ( '--color-primary' ) ,
2024-04-29 23:53:15 +03:00
'focusBorder' : '#0000' , // prevent blue border
2024-03-22 17:06:53 +03:00
} ,
2021-04-08 12:53:00 +03:00
} ) ;
2024-12-09 20:03:36 +03:00
}
type CreateMonacoOpts = MonacoOpts & { language? : string } ;
export async function createMonaco ( textarea : HTMLTextAreaElement , filename : string , opts : CreateMonacoOpts ) : Promise < { monaco : Monaco , editor : IStandaloneCodeEditor } > {
const monaco = await import ( /* webpackChunkName: "monaco" */ 'monaco-editor' ) ;
initLanguages ( monaco ) ;
let { language , . . . other } = opts ;
if ( ! language ) language = getLanguage ( filename ) ;
const container = document . createElement ( 'div' ) ;
container . className = 'monaco-editor-container' ;
if ( ! textarea . parentNode ) throw new Error ( 'Parent node absent' ) ;
textarea . parentNode . append ( container ) ;
window . matchMedia ( '(prefers-color-scheme: dark)' ) . addEventListener ( 'change' , ( ) = > {
updateTheme ( monaco ) ;
} ) ;
updateTheme ( monaco ) ;
2021-04-08 12:53:00 +03:00
2024-12-14 06:10:20 +03:00
const model = monaco . editor . createModel ( textarea . value , language , monaco . Uri . file ( filename ) ) ;
2020-05-14 19:06:01 +03:00
const editor = monaco . editor . create ( container , {
2024-12-14 06:10:20 +03:00
model ,
2021-04-08 12:53:00 +03:00
theme : 'gitea' ,
2020-11-14 06:57:34 +03:00
. . . other ,
2020-05-14 19:06:01 +03:00
} ) ;
2024-04-18 11:06:56 +03:00
monaco . editor . addKeybindingRules ( [
{ keybinding : monaco.KeyCode.Enter , command : null } , // disable enter from accepting code completion
] ) ;
2020-05-14 19:06:01 +03:00
model . onDidChangeContent ( ( ) = > {
2024-12-09 20:03:36 +03:00
textarea . value = editor . getValue ( {
preserveBOM : true ,
lineEnding : '' ,
} ) ;
2020-05-14 19:06:01 +03:00
textarea . dispatchEvent ( new Event ( 'change' ) ) ; // seems to be needed for jquery-are-you-sure
} ) ;
2020-11-14 06:57:34 +03:00
exportEditor ( editor ) ;
2020-05-14 19:06:01 +03:00
const loading = document . querySelector ( '.editor-loading' ) ;
if ( loading ) loading . remove ( ) ;
2020-11-14 06:57:34 +03:00
return { monaco , editor } ;
}
2020-06-03 14:19:32 +03:00
2024-12-09 20:03:36 +03:00
function getFileBasedOptions ( filename : string , lineWrapExts : string [ ] ) : MonacoOpts {
2020-11-14 06:57:34 +03:00
return {
wordWrap : ( lineWrapExts || [ ] ) . includes ( extname ( filename ) ) ? 'on' : 'off' ,
} ;
2020-05-14 19:06:01 +03:00
}
2024-12-09 20:03:36 +03:00
function togglePreviewDisplay ( previewable : boolean ) : void {
2024-12-04 12:26:54 +03:00
const previewTab = document . querySelector < HTMLElement > ( 'a[data-tab="preview"]' ) ;
2023-03-26 08:25:41 +03:00
if ( ! previewTab ) return ;
if ( previewable ) {
previewTab . style . display = '' ;
} else {
previewTab . style . display = 'none' ;
// If the "preview" tab was active, user changes the filename to a non-previewable one,
// then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
if ( previewTab . classList . contains ( 'active' ) ) {
2024-12-04 12:26:54 +03:00
const writeTab = document . querySelector < HTMLElement > ( 'a[data-tab="write"]' ) ;
2024-12-09 20:03:36 +03:00
writeTab ? . click ( ) ;
2023-03-26 08:25:41 +03:00
}
}
}
2024-12-09 20:03:36 +03:00
export async function createCodeEditor ( textarea : HTMLTextAreaElement , filenameInput : HTMLInputElement ) : Promise < IStandaloneCodeEditor > {
2020-11-14 06:57:34 +03:00
const filename = basename ( filenameInput . value ) ;
2023-03-26 08:25:41 +03:00
const previewableExts = new Set ( ( textarea . getAttribute ( 'data-previewable-extensions' ) || '' ) . split ( ',' ) ) ;
2021-11-22 11:19:01 +03:00
const lineWrapExts = ( textarea . getAttribute ( 'data-line-wrap-extensions' ) || '' ) . split ( ',' ) ;
2024-12-09 20:03:36 +03:00
const isPreviewable = previewableExts . has ( extname ( filename ) ) ;
2020-11-14 06:57:34 +03:00
const editorConfig = getEditorconfig ( filenameInput ) ;
2024-12-09 20:03:36 +03:00
togglePreviewDisplay ( isPreviewable ) ;
2020-05-14 19:06:01 +03:00
2020-11-14 06:57:34 +03:00
const { monaco , editor } = await createMonaco ( textarea , filename , {
2021-04-08 12:53:00 +03:00
. . . baseOptions ,
2020-11-14 06:57:34 +03:00
. . . getFileBasedOptions ( filenameInput . value , lineWrapExts ) ,
. . . getEditorConfigOptions ( editorConfig ) ,
} ) ;
2023-05-10 18:50:58 +03:00
filenameInput . addEventListener ( 'input' , onInputDebounce ( ( ) = > {
2020-11-14 06:57:34 +03:00
const filename = filenameInput . value ;
2023-03-26 08:25:41 +03:00
const previewable = previewableExts . has ( extname ( filename ) ) ;
togglePreviewDisplay ( previewable ) ;
2020-11-14 06:57:34 +03:00
updateEditor ( monaco , editor , filename , lineWrapExts ) ;
2023-03-26 08:25:41 +03:00
} ) ) ;
2020-11-14 06:57:34 +03:00
return editor ;
}
2024-12-09 20:03:36 +03:00
function getEditorConfigOptions ( ec : EditorConfig | null ) : MonacoOpts {
if ( ! ec || ! isObject ( ec ) ) return { } ;
2020-11-14 06:57:34 +03:00
2024-12-09 20:03:36 +03:00
const opts : MonacoOpts = { } ;
2020-11-14 06:57:34 +03:00
opts . detectIndentation = ! ( 'indent_style' in ec ) || ! ( 'indent_size' in ec ) ;
2024-12-09 20:03:36 +03:00
if ( 'indent_size' in ec ) {
opts . indentSize = Number ( ec . indent_size ) ;
}
if ( 'tab_width' in ec ) {
opts . tabSize = Number ( ec . tab_width ) || Number ( ec . indent_size ) ;
}
if ( 'max_line_length' in ec ) {
opts . rulers = [ Number ( ec . max_line_length ) ] ;
}
2020-11-14 06:57:34 +03:00
opts . trimAutoWhitespace = ec . trim_trailing_whitespace === true ;
opts . insertSpaces = ec . indent_style === 'space' ;
opts . useTabStops = ec . indent_style === 'tab' ;
2020-05-14 19:06:01 +03:00
return opts ;
}