code-panel: lexer-driven CodeMirror mode with dynamic keyword sets; C# mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7bcd2d9021
commit
5cd9f8e6c4
|
|
@ -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<string, string | null> =
|
||||
{
|
||||
[ 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 ] );
|
||||
|
|
@ -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<string>;
|
||||
cmClass: string;
|
||||
tokenTypes: string[];
|
||||
}
|
||||
|
||||
export class CodeMirrorLexerMode
|
||||
{
|
||||
private _lexer: Lexer;
|
||||
private _typeToClass: Record<string, string | null>;
|
||||
private _multiLineBlocks: MultiLineBlock[];
|
||||
private _multiLineTypes: Set<string>;
|
||||
private _keywordSets: KeywordSet[] = [];
|
||||
private _cmModeCache: any = null;
|
||||
|
||||
name: string = '';
|
||||
|
||||
constructor(
|
||||
lexer: Lexer,
|
||||
typeToClass: Record<string, string | null>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue