дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(python): support Ruff formatting and config directories (#1573)
Ruff is now an external formatter for Python next to the Ruff language server: Settings › Languages & Frameworks › Ruff has the executable path, a configuration directory (or file), the provider preference, enabled languages, run-on-save, and Reformat with Ruff. Formatting runs on the unsaved document through stdin (with Ruff's --range for selections, in 1-based code-point columns) and never writes the file first. The configuration directory is shared with Ruff LSP, which can then start without a root marker. Tools that can't format a language are rejected even when requested. Co-authored-by: muutot <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
Цей коміт міститься в:
24 files changed
+643
-69
No files matched your search
@@ -124,12 +124,12 @@ function editorAction(
|
||||
return { id, title, category, when: EDITOR_WHEN, run: withEditor(run), ...extra };
|
||||
}
|
||||
|
||||
/** Whether saving a `language` file formats it: Format on save, or Run on save of Prettier or oxfmt. */
|
||||
/** Whether saving a `language` file formats it: Format on save, or Run on save of an external formatter. */
|
||||
export function formatsOnSave(settings: Settings | null, language: string): boolean {
|
||||
const { prettier, oxfmt } = validateLanguages(settings?.languages);
|
||||
const { prettier, oxfmt, ruff } = validateLanguages(settings?.languages);
|
||||
return (
|
||||
!!settings?.editor?.formatOnSave ||
|
||||
[prettier, oxfmt].some((formatter) => formatter.runOnSave && formatter.languages.includes(language))
|
||||
[prettier, oxfmt, ruff].some((formatter) => formatter.runOnSave && formatter.languages.includes(language))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,13 @@ export function registerLspActions(): () => void {
|
||||
(v) => formatDocument(v, { explicit: true, tool: "oxfmt" }),
|
||||
{ keywords: ["format", "oxfmt", "oxc"] },
|
||||
),
|
||||
editorAction(
|
||||
"code.reformatWithRuff",
|
||||
"Reformat with Ruff",
|
||||
"Code",
|
||||
(v) => formatDocument(v, { explicit: true, tool: "ruff" }),
|
||||
{ keywords: ["format", "ruff", "python"] },
|
||||
),
|
||||
editorAction("code.reformatDialog", "Reformat File…", "Code", (v) =>
|
||||
reformatDialog(v, (view) => applyKindSilently(view, "source.organizeImports")),
|
||||
),
|
||||
|
||||
@@ -177,6 +177,8 @@ describe("format on save provider isolation", () => {
|
||||
expect(formatsOnSave(oxfmt, "typescript")).toBe(true);
|
||||
expect(formatsOnSave(oxfmt, "markdown")).toBe(false);
|
||||
expect(formatsOnSave(settings({ prettier: { runOnSave: true } }), "markdown")).toBe(true);
|
||||
expect(formatsOnSave(settings({ ruff: { runOnSave: true } }), "python")).toBe(true);
|
||||
expect(formatsOnSave(settings({ ruff: { runOnSave: true } }), "typescript")).toBe(false);
|
||||
expect(formatsOnSave(settings({}), "typescript")).toBe(false);
|
||||
expect(formatsOnSave(settings({}, true), "rust")).toBe(true);
|
||||
expect(formatsOnSave(null, "typescript")).toBe(false);
|
||||
|
||||
@@ -136,20 +136,26 @@ async function format(view: EditorView, opts: FormatDocumentOptions): Promise<Fo
|
||||
commands.lspFormat(doc.doc, range, { ...options, onSave: opts.onSave, tool: opts.tool }),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof IpcError && e.data?.type === "tool" && (e.data.tool === "prettier" || e.data.tool === "oxfmt")) {
|
||||
const formatter =
|
||||
e instanceof IpcError && e.data?.type === "tool"
|
||||
? {
|
||||
prettier: { failed: "Prettier formatting failed", timedOut: "Prettier formatting timed out" },
|
||||
oxfmt: { failed: "oxfmt formatting failed", timedOut: "oxfmt formatting timed out" },
|
||||
ruff: { failed: "Ruff formatting failed", timedOut: "Ruff formatting timed out" },
|
||||
}[e.data.tool]
|
||||
: undefined;
|
||||
if (e instanceof IpcError && e.data?.type === "tool" && formatter) {
|
||||
const tool = e.data.tool;
|
||||
if (e.kind === "notFound" && e.data.missing) {
|
||||
await executeAction("tools.promptInstall", { name: tool, sticky: !opts.onSave });
|
||||
await executeAction("tools.promptInstall", { name: tool, sticky: !opts.onSave, formatter: true });
|
||||
} else if (e.kind !== "cancelled") {
|
||||
notifications().notify({
|
||||
group: "tools",
|
||||
level: "error",
|
||||
title: tool === "oxfmt" ? tx("oxfmt formatting failed") : tx("Prettier formatting failed"),
|
||||
title: tx(formatter.failed),
|
||||
body:
|
||||
e.kind === "timedOut"
|
||||
? tool === "oxfmt"
|
||||
? tx("oxfmt formatting timed out")
|
||||
: tx("Prettier formatting timed out")
|
||||
? tx(formatter.timedOut)
|
||||
: createElement("span", { className: styles.diagnostics }, e.message),
|
||||
key: `${tool}-format-error:${doc.path}`,
|
||||
sticky: false,
|
||||
|
||||
@@ -44,9 +44,9 @@ afterEach(() => {
|
||||
_resetI18nForTests();
|
||||
});
|
||||
|
||||
describe.each(["prettier", "oxfmt"])("%s formatter errors", (name) => {
|
||||
describe.each(["prettier", "oxfmt", "ruff"])("%s formatter errors", (name) => {
|
||||
const tool = { type: "tool", tool: name, missing: false } as const;
|
||||
const display = name === "prettier" ? "Prettier" : name;
|
||||
const display = name === "prettier" ? "Prettier" : name === "ruff" ? "Ruff" : name;
|
||||
it("uses structured missing-tool information to request the shared install prompt", async () => {
|
||||
const prompt = vi.fn();
|
||||
const dispose = registerAction({
|
||||
@@ -57,9 +57,9 @@ describe.each(["prettier", "oxfmt"])("%s formatter errors", (name) => {
|
||||
try {
|
||||
failWith({ kind: "notFound", message: "unrelated diagnostic", data: { ...tool, missing: true } });
|
||||
await formatDocument(viewFor());
|
||||
expect(prompt).toHaveBeenCalledWith({ name, sticky: true });
|
||||
expect(prompt).toHaveBeenCalledWith({ name, sticky: true, formatter: true });
|
||||
await formatDocument(viewFor(), { explicit: false, onSave: true });
|
||||
expect(prompt).toHaveBeenLastCalledWith({ name, sticky: false });
|
||||
expect(prompt).toHaveBeenLastCalledWith({ name, sticky: false, formatter: true });
|
||||
expect(useNotifications.getState().items).toHaveLength(0);
|
||||
} finally {
|
||||
dispose();
|
||||
@@ -126,10 +126,11 @@ describe.each(["prettier", "oxfmt"])("%s formatter errors", (name) => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reformat with Prettier / oxfmt", () => {
|
||||
describe("Reformat with an external formatter", () => {
|
||||
it.each([
|
||||
["code.reformatWithPrettier", "prettier"],
|
||||
["code.reformatWithOxfmt", "oxfmt"],
|
||||
["code.reformatWithRuff", "ruff"],
|
||||
])("%s asks the backend for %s whatever the settings select", async (id, tool) => {
|
||||
vi.spyOn(lsp, "lspRequest").mockImplementation(async (request) => {
|
||||
await request();
|
||||
|
||||
Reference in new issue
Заблокувати користувача