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
27 changes: 24 additions & 3 deletions src/cm/modelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type LanguageExtensionProvider = () => Extension | Promise<Extension>;
export interface AddModeOptions {
aliases?: string[];
filenameMatchers?: RegExp[];
/** Mode whose formatter (and Prettier parser) this mode reuses. */
formatAs?: string;
}

export interface ModesByName {
Expand Down Expand Up @@ -352,6 +354,15 @@ export function getMode(name: string): Mode | null {
return modesByName[normalizeModeKey(name)] || null;
}

/**
* Name of the mode that `name` formats as, with aliases resolved, or `null`.
*/
export function getFormatAsMode(name: string): string | null {
const formatAs = getMode(name)?.formatAs;
if (!formatAs) return null;
return getMode(formatAs)?.name ?? formatAs;
}

export class Mode {
extensions: string;
caption: string;
Expand All @@ -360,6 +371,7 @@ export class Mode {
aliases: string[];
extRe: RegExp | null;
filenameMatchers: RegExp[];
formatAs: string | null;
languageExtension: LanguageExtensionProvider | null;

constructor(
Expand All @@ -377,10 +389,19 @@ 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)
// Plugins may pass anything here; ignore what isn't the expected shape
const { aliases, filenameMatchers, formatAs } =
options && typeof options === "object" ? options : {};
this.aliases = Array.isArray(aliases)
? normalizeAliases(aliases, this.name)
: [];
this.filenameMatchers = Array.isArray(filenameMatchers)
? filenameMatchers.filter((matcher) => matcher instanceof RegExp)
: [];
this.formatAs =
typeof formatAs === "string" && normalizeModeKey(formatAs)
? normalizeModeKey(formatAs)
: null;
this.languageExtension = languageExtension;
let re = "";

Expand Down
12 changes: 8 additions & 4 deletions src/lib/acode.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import lspClientManager from "cm/lsp/clientManager";
import { registerLspFormatter } from "cm/lsp/formatter";
import {
addMode,
getFormatAsMode,
getModeForPath,
getModes,
getModesByName,
Expand Down Expand Up @@ -275,9 +276,10 @@ class Acode {
// Preferred CodeMirror language registration API for plugins
const editorLanguages = {
// name: string, extensions: string|Array<string>, caption?: string,
// loader?: () => Extension | Promise<Extension>
register: (name, extensions, caption, loader) =>
addMode(name, extensions, caption, loader),
// loader?: () => Extension | Promise<Extension>,
// options?: { aliases?, filenameMatchers?, formatAs? }
register: (name, extensions, caption, loader, options) =>
addMode(name, extensions, caption, loader, options),
unregister: (name) => removeMode(name),
add: (name, extensions, caption, loader) =>
addMode(name, extensions, caption, loader),
Expand Down Expand Up @@ -844,7 +846,9 @@ class Acode {
}
const modeName = resolvedMode || "text";
const formatterMap = appSettings.value.formatter || {};
const formatterId = formatterMap[modeName];
const formatAs = getFormatAsMode(modeName);
const formatterId =
formatterMap[modeName] ?? (formatAs && formatterMap[formatAs]);
const formatter = this.#formatter.find(({ id }) => id === formatterId);

if (!formatter) {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/prettierFormatter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fsOperation from "fileSystem";
import { parse } from "acorn";
import { getDocText } from "cm/editorUtils";
import { getFormatAsMode } from "cm/modelist";
import toast from "components/toast";
import appSettings from "lib/settings";
import prettierPluginBabel from "prettier/plugins/babel";
Expand Down Expand Up @@ -77,7 +78,9 @@ export async function formatActiveFileWithPrettier() {
if (!file || file.type !== "editor" || !editor) return false;

const modeName = (file.currentMode || "text").toLowerCase();
const parser = getParserForMode(modeName);
const formatAs = getFormatAsMode(modeName);
const parser =
getParserForMode(modeName) || (formatAs && getParserForMode(formatAs));
if (!parser) {
toast("Prettier does not support this file type yet");
return false;
Expand Down
24 changes: 20 additions & 4 deletions src/settings/formatterSettings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getModes } from "cm/modelist";
import { getFormatAsMode, getMode, getModes } from "cm/modelist";
import settingsPage from "components/settingsPage";
import appSettings from "lib/settings";

Expand All @@ -21,8 +21,20 @@ export default function formatterSettings(languageName) {
const extList = String(extensions)
.split("|")
.filter((e) => e && !e.startsWith("^"));
const options = acode.getFormatterFor(extList);
// Also offer formatters of the mode this one formats as
const baseMode = getMode(getFormatAsMode(name) || "");
const baseExts = String(baseMode?.extensions || "")
.split("|")
.filter((e) => e && !e.startsWith("^"));
const options = acode.getFormatterFor([...extList, ...baseExts]);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const sampleExt = extList[0] || name;
// Unset falls back to the base mode's formatter, so say which one
const inheritedText = () => {
const inherited = formatters.find(
({ id }) => id === values.formatter[baseMode.name],
);
return inherited ? `${inherited.name} (${baseMode.caption})` : null;
};

return {
key: name,
Expand All @@ -34,9 +46,13 @@ export default function formatterSettings(languageName) {
if (formatter) {
return formatter.name;
}
return strings.none;
return (baseMode && inheritedText()) || strings.none;
},
// Read when the picker opens, so the inherited label stays current
get select() {
if (baseMode) options[0] = [null, inheritedText() || strings.none];
return options;
},
select: options,
chevron: true,
category: languagesLabel,
};
Expand Down
45 changes: 44 additions & 1 deletion tests/unit/modelist.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { afterEach, describe, expect, it } from "vitest";
import "cm/supportedModes";
import { addMode, getModeForPath, getModes, removeMode } from "cm/modelist";
import {
addMode,
getFormatAsMode,
getModeForPath,
getModes,
removeMode,
} from "cm/modelist";

function getModeSpecificityScore(modeInstance) {
if (modeInstance.name.toLowerCase() === "text") {
Expand Down Expand Up @@ -235,6 +241,43 @@ describe("getModeForPath", () => {
expect(getModeForPath("acodepluginfile").name).toBe("acode-regex-foo");
});

it("stores the normalized formatAs mode", () => {
registerTestMode("acode-json-like", "acodejsonlike", {
formatAs: " JSON ",
});
registerTestMode("acode-plain", "acodeplain");

expect(getModeForPath("a.acodejsonlike").formatAs).toBe("json");
expect(getModeForPath("a.acodeplain").formatAs).toBeNull();
});

it("resolves formatAs aliases to the base mode name", () => {
registerTestMode("acode-json5-like", "acodejson5like", {
formatAs: "json5",
});
registerTestMode("acode-unknown-base", "acodeunknownbase", {
formatAs: "acode-missing",
});

expect(getFormatAsMode("acode-json5-like")).toBe("json");
expect(getFormatAsMode("acode-unknown-base")).toBe("acode-missing");
expect(getFormatAsMode("json")).toBeNull();
});

it("ignores malformed mode options from plugins", () => {
expect(() =>
registerTestMode("acode-null-options", "acodenullopts", null),
).not.toThrow();
registerTestMode("acode-bad-options", "acodebadopts", {
aliases: "abc",
formatAs: 42,
});

const mode = getModeForPath("a.acodebadopts");
expect(mode.aliases).toEqual([]);
expect(mode.formatAs).toBeNull();
});

it("invalidates plugin registrations immediately", () => {
registerTestMode("acodepluginmode", "acodepluginmode");

Expand Down
Loading