Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint": "biome lint --write",
"format": "biome format --write",
"check": "biome check --write",
"list:modes": "node ./utils/listSupportedModes.mjs",
"typecheck": "tsc --noEmit",
"updateAce": "node ./utils/updateAce.js"
},
Expand Down Expand Up @@ -107,21 +108,25 @@
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-jinja": "^6.0.0",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-liquid": "^6.3.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.3",
Expand Down
151 changes: 122 additions & 29 deletions src/cm/modelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,38 @@ import type { Extension } from "@codemirror/state";

export type LanguageExtensionProvider = () => Extension | Promise<Extension>;

export interface AddModeOptions {
aliases?: string[];
filenameMatchers?: RegExp[];
}

export interface ModesByName {
[name: string]: Mode;
}

const modesByName: ModesByName = {};
const modes: Mode[] = [];

function normalizeModeKey(value: string): string {
return String(value ?? "")
.trim()
.toLowerCase();
}

function normalizeAliases(aliases: string[] = [], name: string): string[] {
const normalized = new Set<string>();
for (const alias of aliases) {
const key = normalizeModeKey(alias);
if (!key || key === name) continue;
normalized.add(key);
}
return [...normalized];
}

function escapeRegExp(value: string): string {
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
* Initialize CodeMirror mode list functionality
*/
Expand All @@ -25,20 +50,43 @@ export function addMode(
extensions: string | string[],
caption?: string,
languageExtension: LanguageExtensionProvider | null = null,
options: AddModeOptions = {},
): void {
const filename = name.toLowerCase();
const mode = new Mode(filename, caption, extensions, languageExtension);
const filename = normalizeModeKey(name);
const mode = new Mode(
filename,
caption,
extensions,
languageExtension,
options,
);
modesByName[filename] = mode;
mode.aliases.forEach((alias) => {
if (!modesByName[alias]) {
modesByName[alias] = mode;
}
});
modes.push(mode);
}

/**
* Remove language mode from CodeMirror editor
*/
export function removeMode(name: string): void {
const filename = name.toLowerCase();
delete modesByName[filename];
const modeIndex = modes.findIndex((mode) => mode.name === filename);
const filename = normalizeModeKey(name);
const mode = modesByName[filename];
if (!mode) return;

delete modesByName[mode.name];
mode.aliases.forEach((alias) => {
if (modesByName[alias] === mode) {
delete modesByName[alias];
}
});

const modeIndex = modes.findIndex(
(registeredMode) => registeredMode === mode,
);
if (modeIndex >= 0) {
modes.splice(modeIndex, 1);
}
Expand Down Expand Up @@ -73,24 +121,32 @@ export function getModeForPath(path: string): Mode {
*/
function getModeSpecificityScore(modeInstance: Mode): number {
const extensionsStr = modeInstance.extensions;
if (!extensionsStr) return 0;

const patterns = extensionsStr.split("|");
let maxScore = 0;

for (const pattern of patterns) {
let currentScore = 0;
if (pattern.startsWith("^")) {
// Exact filename match or anchored pattern
currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^'
} else {
// Extension match
currentScore = pattern.length;
if (extensionsStr) {
const patterns = extensionsStr.split("|");
for (const pattern of patterns) {
let currentScore = 0;
if (pattern.startsWith("^")) {
// Exact filename match or anchored pattern
currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^'
} else {
// Extension match
currentScore = pattern.length;
}
if (currentScore > maxScore) {
maxScore = currentScore;
}
}
if (currentScore > maxScore) {
maxScore = currentScore;
}

for (const matcher of modeInstance.filenameMatchers) {
const score = 1000 + matcher.source.length;
if (score > maxScore) {
maxScore = score;
}
}

return maxScore;
}

Expand All @@ -108,19 +164,26 @@ export function getModes(): Mode[] {
return modes;
}

export function getMode(name: string): Mode | null {
return modesByName[normalizeModeKey(name)] || null;
}

export class Mode {
extensions: string;
caption: string;
name: string;
mode: string;
extRe: RegExp;
aliases: string[];
extRe: RegExp | null;
filenameMatchers: RegExp[];
languageExtension: LanguageExtensionProvider | null;

constructor(
name: string,
caption: string | undefined,
extensions: string | string[],
languageExtension: LanguageExtensionProvider | null = null,
options: AddModeOptions = {},
) {
if (Array.isArray(extensions)) {
extensions = extensions.join("|");
Expand All @@ -130,23 +193,53 @@ export class Mode {
this.mode = name; // CodeMirror uses different mode naming
this.extensions = extensions;
this.caption = caption || this.name.replace(/_/g, " ");
this.aliases = normalizeAliases(options.aliases, this.name);
this.filenameMatchers = Array.isArray(options.filenameMatchers)
? options.filenameMatchers.filter((matcher) => matcher instanceof RegExp)
: [];
this.languageExtension = languageExtension;
let re: string;

if (/\^/.test(extensions)) {
re =
extensions.replace(/\|(\^)?/g, function (_a: string, b: string) {
return "$|" + (b ? "^" : "^.*\\.");
}) + "$";
} else {
re = "^.*\\.(" + extensions + ")$";
let re = "";

if (!extensions) {
this.extRe = null;
return;
}

const patterns = extensions
.split("|")
.map((pattern) => pattern.trim())
.filter(Boolean);
const filenamePatterns = patterns
.filter((pattern) => pattern.startsWith("^"))
.map((pattern) => `^${escapeRegExp(pattern.slice(1))}$`);
const extensionPatterns = patterns
.filter((pattern) => !pattern.startsWith("^"))
.map((pattern) => escapeRegExp(pattern));
const regexParts: string[] = [];

if (extensionPatterns.length) {
regexParts.push(`^.*\\.(${extensionPatterns.join("|")})$`);
}

regexParts.push(...filenamePatterns);

if (!regexParts.length) {
this.extRe = null;
return;
}

re =
regexParts.length === 1 ? regexParts[0] : `(?:${regexParts.join("|")})`;
this.extRe = new RegExp(re, "i");
}

supportsFile(filename: string): boolean {
return this.extRe.test(filename);
if (this.extRe?.test(filename)) return true;

return this.filenameMatchers.some((matcher) => {
matcher.lastIndex = 0;
return matcher.test(filename);
});
}

/**
Expand Down
Loading