diff --git a/bun.lock b/bun.lock index d8d8a80b5..d0ed439b1 100644 --- a/bun.lock +++ b/bun.lock @@ -4,21 +4,25 @@ "": { "name": "com.foxdebug.acode", "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", @@ -363,7 +367,7 @@ "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], - "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="], @@ -371,7 +375,7 @@ "@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="], - "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="], + "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="], "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="], @@ -2057,6 +2061,16 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@codemirror/lang-angular/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/lang-html/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/lang-vue/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/language-data/@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/language-data/@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw=="], + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.65.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.65.0", "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0", "@jsonjoy.com/json-pointer": "17.65.0", "@jsonjoy.com/util": "17.65.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ=="], "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.65.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.65.0", "@jsonjoy.com/codegen": "17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w=="], diff --git a/package.json b/package.json index 2b76ad418..9963a8dc6 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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", diff --git a/src/cm/modelist.ts b/src/cm/modelist.ts index fe9be60ad..faa6b5a84 100644 --- a/src/cm/modelist.ts +++ b/src/cm/modelist.ts @@ -2,6 +2,11 @@ import type { Extension } from "@codemirror/state"; export type LanguageExtensionProvider = () => Extension | Promise; +export interface AddModeOptions { + aliases?: string[]; + filenameMatchers?: RegExp[]; +} + export interface ModesByName { [name: string]: Mode; } @@ -9,6 +14,26 @@ export interface ModesByName { 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(); + 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 */ @@ -25,10 +50,22 @@ 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); } @@ -36,9 +73,20 @@ export function addMode( * 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); } @@ -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; } @@ -108,12 +164,18 @@ 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( @@ -121,6 +183,7 @@ export class Mode { caption: string | undefined, extensions: string | string[], languageExtension: LanguageExtensionProvider | null = null, + options: AddModeOptions = {}, ) { if (Array.isArray(extensions)) { extensions = extensions.join("|"); @@ -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); + }); } /** diff --git a/src/cm/supportedModes.ts b/src/cm/supportedModes.ts index ed2d724f0..5b6e3f519 100644 --- a/src/cm/supportedModes.ts +++ b/src/cm/supportedModes.ts @@ -2,26 +2,77 @@ import { languages } from "@codemirror/language-data"; import type { Extension } from "@codemirror/state"; import { addMode } from "./modelist"; +type FilenameMatcher = string | RegExp; + interface LanguageDescription { name?: string; + alias?: readonly string[]; extensions?: readonly string[]; - filenames?: readonly string[]; - filename?: string; + filenames?: readonly FilenameMatcher[]; + filename?: FilenameMatcher; load?: () => Promise; } +function normalizeModeKey(value: string): string { + return String(value ?? "") + .trim() + .toLowerCase(); +} + +function isSafeModeId(value: string): boolean { + return /^[a-z0-9][a-z0-9._-]*$/.test(value); +} + +function slugifyModeId(value: string): string { + return normalizeModeKey(value) + .replace(/\+\+/g, "pp") + .replace(/#/g, "sharp") + .replace(/&/g, "and") + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function collectAliases( + name: string, + aliases: readonly string[] | undefined, +): string[] { + return [ + ...new Set( + [name, ...(aliases || [])].map(normalizeModeKey).filter(Boolean), + ), + ]; +} + +function getModeId(name: string, aliases: string[]): string { + const normalizedName = normalizeModeKey(name); + if (isSafeModeId(normalizedName)) return normalizedName; + + const safeAlias = aliases.find( + (alias) => alias !== normalizedName && isSafeModeId(alias), + ); + return safeAlias || slugifyModeId(name) || normalizedName || "text"; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + // 1) Always register a plain text fallback addMode("Text", "txt|text|log|plain", "Plain Text", () => []); // 2) Register all languages provided by @codemirror/language-data // We convert extensions like [".js", ".mjs"] into a modelist pattern: "js|mjs" -// and include anchored filename patterns like "^Dockerfile" when present. +// and preserve aliases and filename regexes for languages like C++ and Dockerfile. for (const lang of languages as readonly LanguageDescription[]) { try { const name = String(lang?.name || "").trim(); if (!name) continue; + const aliases = collectAliases(name, lang.alias); + const modeId = getModeId(name, aliases); const parts: string[] = []; + const filenameMatchers: RegExp[] = []; + // File extensions if (Array.isArray(lang.extensions)) { for (const e of lang.extensions) { @@ -30,20 +81,26 @@ for (const lang of languages as readonly LanguageDescription[]) { if (cleaned) parts.push(cleaned); } } - // Exact filenames (dockerfile, makefile, etc.) + + // Exact filenames / filename regexes (Dockerfile, PKGBUILD, nginx*.conf, etc.) const filenames = Array.isArray(lang.filenames) ? lang.filenames : lang.filename ? [lang.filename] : []; for (const fn of filenames) { - if (typeof fn !== "string") continue; - const cleaned = fn.trim(); - if (cleaned) parts.push(`^${cleaned}`); - } + if (typeof fn === "string") { + const cleaned = fn.trim(); + if (cleaned) { + filenameMatchers.push(new RegExp(`^${escapeRegExp(cleaned)}$`, "i")); + } + continue; + } - // Skip if we have no way to match the language - if (parts.length === 0) continue; + if (fn instanceof RegExp) { + filenameMatchers.push(new RegExp(fn.source, fn.flags)); + } + } const pattern = parts.join("|"); @@ -51,7 +108,10 @@ for (const lang of languages as readonly LanguageDescription[]) { // lang.load() returns a Promise; we let the editor handle async loading const loader = typeof lang.load === "function" ? () => lang.load!() : null; - addMode(name, pattern, name, loader); + addMode(modeId, pattern, name, loader, { + aliases, + filenameMatchers, + }); } catch (_) { // Ignore faulty entries to avoid breaking the whole registration } diff --git a/src/components/inputhints/index.js b/src/components/inputhints/index.js index a340730a9..23bba3cfe 100644 --- a/src/components/inputhints/index.js +++ b/src/components/inputhints/index.js @@ -4,6 +4,7 @@ import "./style.scss"; * @typedef {Object} HintObj * @property {string} value * @property {string} text + * @property {boolean} [active] */ /** @@ -139,7 +140,7 @@ export default function inputhints($input, hints, onSelect) { function oninput() { const { value: toTest } = this; const matched = []; - const regexp = new RegExp(toTest, "i"); + const regexp = new RegExp(escapeRegExp(toTest), "i"); hints.forEach((hint) => { const { value, text } = hint; if (regexp.test(value) || regexp.test(text)) { @@ -351,6 +352,7 @@ export default function inputhints($input, hints, onSelect) { function Hint({ hint }) { let value = ""; let text = ""; + let active = false; if (typeof hint === "string") { value = hint; @@ -358,9 +360,17 @@ function Hint({ hint }) { } else { value = hint.value; text = hint.text; + active = !!hint.active; } - return
  • ; + return ( +
  • + ); } /** @@ -378,3 +388,7 @@ function Ul({ hints = [] }) { ); } + +function escapeRegExp(value) { + return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index 2863027e5..cce47eb4c 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -7,7 +7,7 @@ import { restoreSelection, setScrollPosition, } from "cm/editorUtils"; -import { getModeForPath } from "cm/modelist"; +import { getMode, getModeForPath } from "cm/modelist"; import Sidebar from "components/sidebar"; import tile from "components/tile"; import confirm from "dialogs/confirm"; @@ -1030,17 +1030,18 @@ export default class EditorFile { const modes = helpers.parseJSON(localStorage.modeassoc); if (modes?.[ext]) { mode = modes[ext]; - } else { - const modeInfo = getModeForPath(this.filename); - mode = modeInfo?.name || "text"; } } + let modeInfo = mode ? getMode(mode) : null; + if (!modeInfo) { + modeInfo = getModeForPath(this.filename); + } + mode = modeInfo?.name || String(mode || "text").toLowerCase(); + // Store mode info for later use when creating editor view this.currentMode = mode; - this.currentLanguageExtension = getModeForPath( - this.filename, - )?.getExtension(); + this.currentLanguageExtension = modeInfo?.getExtension() || null; // sets file icon this.#tab.lead( diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 2170f6a10..4d2db8dca 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -47,6 +47,7 @@ import { import { stopManagedServer } from "cm/lsp/serverLauncher"; // CodeMirror mode management import { + getMode, getModeForPath, getModes, getModesByName, @@ -631,7 +632,11 @@ async function EditorManager($header, $body) { function getFileLanguageId(file) { if (!file) return "plaintext"; const mode = file.currentMode || file.mode; - if (mode) return String(mode).toLowerCase(); + if (mode) { + const modeInfo = getMode(String(mode)); + if (modeInfo?.name) return String(modeInfo.name).toLowerCase(); + return String(mode).toLowerCase(); + } try { const guess = getModeForPath(file.filename || file.name || ""); if (guess?.name) return String(guess.name).toLowerCase(); diff --git a/src/palettes/changeMode/index.js b/src/palettes/changeMode/index.js index 47c783804..2d26a8742 100644 --- a/src/palettes/changeMode/index.js +++ b/src/palettes/changeMode/index.js @@ -8,15 +8,31 @@ export default function changeMode() { } function generateHints() { - const modes = getModes(); + const modes = [...getModes()].sort((a, b) => + a.caption.localeCompare(b.caption), + ); + const activeMode = editorManager.activeFile?.currentMode || ""; + const activeIndex = modes.findIndex(({ mode }) => mode === activeMode); + + if (activeIndex > 0) { + const [activeEntry] = modes.splice(activeIndex, 1); + modes.unshift(activeEntry); + } + + return modes.map(({ aliases = [], caption, extensions, mode }) => { + const searchTerms = [caption, mode, extensions, ...aliases] + .filter(Boolean) + .join(" "); + const title = + caption.toLowerCase() === mode ? caption : `${caption} (${mode})`; - return modes.map(({ caption, mode, extensions }) => { return { + active: mode === activeMode, value: mode, text: `
    - ${caption} - ${mode} -
    `, + ${title} + +
    `, }; }); }