diff --git a/source/components/code-panel/CSharpMode.ts b/source/components/code-panel/CSharpMode.ts new file mode 100644 index 0000000..25f980d --- /dev/null +++ b/source/components/code-panel/CSharpMode.ts @@ -0,0 +1,50 @@ +import { CLikeLexer } from '../../library-ts/browser/text/lexer/CLikeLexer.js'; +import { LexerTypes } from '../../library-ts/browser/text/lexer/LexerType.js'; +import { CodeMirrorLexerMode } from './CodeMirrorLexerMode.js'; + +const TYPE_TO_CLASS: Record = +{ + [ LexerTypes.SINGLE_LINE_COMMENT ]: 'comment', + [ LexerTypes.MULTI_LINE_COMMENT ]: 'comment', + [ LexerTypes.DOUBLE_QUOTED_STRING ]: 'string', + [ LexerTypes.SINGLE_QUOTED_STRING ]: 'string', + [ LexerTypes.NUMBER ]: 'number', + [ LexerTypes.BOOL ]: 'atom', + [ LexerTypes.NULL ]: 'atom', + [ LexerTypes.LOGIC ]: 'keyword', + [ LexerTypes.CLASS ]: 'keyword', + [ LexerTypes.ACCESS_MODIFIER ]: 'keyword', + [ LexerTypes.C_INSTRUCTION ]: 'meta', + [ LexerTypes.CFUNCTION ]: 'variable-2', + [ LexerTypes.CWORD ]: 'variable', + [ LexerTypes.OPERATOR ]: 'operator', + [ LexerTypes.BRACKET ]: 'bracket', + [ LexerTypes.WHITESPACE ]: null, + [ LexerTypes.BREAK ]: null, + [ LexerTypes.ANY_SYMBOL ]: null, +}; + +const CS_KEYWORDS = +[ + 'abstract', 'as', 'async', 'await', 'base', 'byte', + 'catch', 'char', 'checked', 'const', 'decimal', 'default', + 'delegate', 'double', 'dynamic', 'enum', 'event', 'explicit', + 'extern', 'finally', 'fixed', 'float', 'foreach', 'get', + 'goto', 'implicit', 'in', 'int', 'interface', 'internal', + 'is', 'lock', 'long', 'nameof', 'namespace', 'new', + 'object', 'operator', 'out', 'override', 'params', 'partial', + 'readonly', 'ref', 'remove', 'sbyte', 'sealed', 'set', + 'short', 'sizeof', 'stackalloc', 'static', 'string', 'struct', + 'this', 'throw', 'try', 'typeof', 'uint', 'ulong', + 'unchecked','unsafe', 'ushort', 'using', 'value', 'var', + 'virtual', 'void', 'volatile', 'when', 'where', 'with', + 'yield', +]; + +export const csharpMode = new CodeMirrorLexerMode( + new CLikeLexer(), + TYPE_TO_CLASS, + [ { type: LexerTypes.MULTI_LINE_COMMENT, start: /\/\*/, end: /\*\//, cmClass: 'comment' } ] +); + +csharpMode.setKeywordSet( 'keywords', CS_KEYWORDS, 'keyword', [ LexerTypes.CWORD ] ); diff --git a/source/components/code-panel/CodeMirrorLexerMode.ts b/source/components/code-panel/CodeMirrorLexerMode.ts new file mode 100644 index 0000000..5287d4a --- /dev/null +++ b/source/components/code-panel/CodeMirrorLexerMode.ts @@ -0,0 +1,167 @@ +import { Lexer } from '../../library-ts/browser/text/lexer/Lexer.js'; + +export interface MultiLineBlock +{ + type: string; + start: RegExp; + end: RegExp; + cmClass: string; +} + +interface KeywordSet +{ + id: string; + words: Set; + cmClass: string; + tokenTypes: string[]; +} + +export class CodeMirrorLexerMode +{ + private _lexer: Lexer; + private _typeToClass: Record; + private _multiLineBlocks: MultiLineBlock[]; + private _multiLineTypes: Set; + private _keywordSets: KeywordSet[] = []; + private _cmModeCache: any = null; + + name: string = ''; + + constructor( + lexer: Lexer, + typeToClass: Record, + multiLineBlocks: MultiLineBlock[] = [] + ) + { + this._lexer = lexer; + this._typeToClass = typeToClass; + this._multiLineBlocks = multiLineBlocks; + this._multiLineTypes = new Set( multiLineBlocks.map( b => b.type ) ); + } + + setKeywordSet( id: string, words: string[], cmClass: string, tokenTypes: string[] = [ 'CWORD' ] ): this + { + const existing = this._keywordSets.find( ks => ks.id === id ); + + if ( existing ) + { + existing.words = new Set( words ); + existing.cmClass = cmClass; + existing.tokenTypes = tokenTypes; + } + else + { + this._keywordSets.push( { id, words: new Set( words ), cmClass, tokenTypes } ); + } + + return this; + } + + removeKeywordSet( id: string ): this + { + this._keywordSets = this._keywordSets.filter( ks => ks.id !== id ); + return this; + } + + refresh( cm: any ): void + { + cm.setOption( 'mode', this.name ); + } + + get cmMode(): any + { + if ( this._cmModeCache ) return this._cmModeCache; + + const self = this; + + this._cmModeCache = + { + startState: () => ( { multiLine: null as string | null } ), + + token( stream: any, state: any ): string | null + { + // Ongoing multi-line block + if ( state.multiLine !== null ) + { + const block = self._multiLineBlocks.find( b => b.type === state.multiLine )!; + const remaining = stream.string.slice( stream.pos ); + const endMatch = block.end.exec( remaining ); + + if ( endMatch !== null ) + { + const len = endMatch.index + endMatch[ 0 ].length; + for ( let i = 0; i < len; i++ ) stream.next(); + state.multiLine = null; + } + else + { + stream.skipToEnd(); + } + + return block.cmClass; + } + + // Multi-line block starts + for ( const block of self._multiLineBlocks ) + { + const remaining = stream.string.slice( stream.pos ); + const startMatch = block.start.exec( remaining ); + + if ( startMatch && startMatch.index === 0 ) + { + const startLen = startMatch[ 0 ].length; + const afterStart = remaining.slice( startLen ); + const endMatch = block.end.exec( afterStart ); + + if ( endMatch !== null ) + { + const totalLen = startLen + endMatch.index + endMatch[ 0 ].length; + for ( let i = 0; i < totalLen; i++ ) stream.next(); + } + else + { + for ( let i = 0; i < startLen; i++ ) stream.next(); + stream.skipToEnd(); + state.multiLine = block.type; + } + + return block.cmClass; + } + } + + // Regular matchers + const matchers = self._lexer.modes.get( 'default' ) ?? []; + + for ( const matcher of matchers ) + { + if ( self._multiLineTypes.has( matcher.type ) ) continue; + + const startPos = stream.pos; + const len = matcher.matchLength( stream.string, startPos ); + + if ( len > 0 ) + { + for ( let i = 0; i < len; i++ ) stream.next(); + + const word = stream.string.slice( startPos, stream.pos ); + + for ( const ks of self._keywordSets ) + { + if ( ks.tokenTypes.includes( matcher.type ) && ks.words.has( word ) ) + { + return ks.cmClass; + } + } + + return self._typeToClass[ matcher.type ] ?? null; + } + } + + stream.next(); + return null; + } + }; + + return this._cmModeCache; + } +} diff --git a/source/components/code-panel/code-panel.ts b/source/components/code-panel/code-panel.ts index f5bed3c..0aaeae2 100644 --- a/source/components/code-panel/code-panel.ts +++ b/source/components/code-panel/code-panel.ts @@ -1,8 +1,12 @@ import { Editor } from '../../editor/Editor.js'; import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js'; +import { csharpMode } from './CSharpMode.js'; declare const CodeMirror: any; +csharpMode.name = 'rokojori-cs'; +CodeMirror.defineMode( 'rokojori-cs', () => csharpMode.cmMode ); + class CodePanel extends HTMLElement { currentPath: string | null = null; @@ -94,7 +98,8 @@ class CodePanel extends HTMLElement if ( name.endsWith( '.yaml' ) || name.endsWith( '.yml' ) ) return 'yaml'; if ( name.endsWith( '.sh' ) ) return 'shell'; if ( name.endsWith( '.gd' ) ) return 'python'; - if ( name.endsWith( '.cs' ) || name.endsWith( '.glsl' ) || name.endsWith( '.gdshader' ) || name.endsWith( '.gdshaderinc' ) ) return 'clike'; + if ( name.endsWith( '.cs' ) ) return 'rokojori-cs'; + if ( name.endsWith( '.glsl' ) || name.endsWith( '.gdshader' ) || name.endsWith( '.gdshaderinc' ) ) return 'clike'; return 'null'; }