дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(cargo): dependency version inspections and updates (#684)
Cargo.toml had no way to tell that a dependency had a newer release.
Dependencies in [dependencies], [dev-dependencies], [build-dependencies], target tables and [workspace.dependencies] now get a weak warning when the newest stable release is outside the version requirement, with a quick fix that rewrites the requirement at its original precision ("0.7" → "0.10") in both string and inline-table forms. Versions complete inside the version string, newest first, without yanked releases. Path, git and workspace-inherited dependencies are skipped, prereleases are only suggested to prerelease requirements, and failures stay silent.
Versions come from the crates.io sparse index through the backend's HTTP client, with an in-memory and on-disk cache and coalesced requests, behind a dependency version source shared by file-type parsers so other manifests can be added later. semver is now used directly; it was already linked into the app.
Fixes #479
Цей коміт міститься в:
1 джерело
4034853f91
коміт
b5dbd3c020
26 files changed
+1257
-25
No files matched your search
@@ -21,6 +21,7 @@ import {
|
||||
Annotation,
|
||||
Compartment,
|
||||
Facet,
|
||||
StateEffect,
|
||||
type EditorState,
|
||||
type Extension,
|
||||
type RangeSet,
|
||||
@@ -332,14 +333,25 @@ export function contributedAt<T>(state: EditorState, key: string, pos: number):
|
||||
return state.languageDataAt<T>(key, pos)[0];
|
||||
}
|
||||
|
||||
/** A context action a feature offers at the caret, listed by Show Context Actions (⌥↵) before the language server's. */
|
||||
/** A context action a feature offers at the caret, listed by Show Context Actions (⌥↵). */
|
||||
export interface ContributedIntention {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
quickFix?: boolean;
|
||||
run: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/** A background provider has new context actions for the current document. */
|
||||
export const intentionsChanged = StateEffect.define<null>();
|
||||
|
||||
/** Local fixes of a diagnostic range (hover link and Apply Quick Fix). */
|
||||
export const quickFixProviders = Facet.define<(view: EditorView, from: number, to: number) => ContributedIntention[]>();
|
||||
|
||||
export function contributedQuickFixes(view: EditorView, from: number, to: number): ContributedIntention[] {
|
||||
return view.state.facet(quickFixProviders).flatMap((provider) => provider(view, from, to));
|
||||
}
|
||||
|
||||
/** Providers of `ContributedIntention`s for the caret of `view` (cheap and synchronous: also drives the light bulb). */
|
||||
export const intentionProviders = Facet.define<(view: EditorView) => ContributedIntention[]>();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Diagnostic, Severity } from "@/bindings";
|
||||
import { commands, ideEventsReady, isTauri, onIdeEvent } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { publishLocalDiagnostics } from "@/platform/localDiagnostics";
|
||||
import { scrollbarMarks, type ScrollbarMark } from "@/editor/api";
|
||||
import { intentionsChanged, scrollbarMarks, type ScrollbarMark } from "@/editor/api";
|
||||
import { rangeToOffsets } from "./positions";
|
||||
import { lspDoc } from "./lsp";
|
||||
import { showHint } from "./hint";
|
||||
@@ -199,7 +199,10 @@ const diagnosticsSync = ViewPlugin.fromClass(
|
||||
if (!path || !this.view.dom.isConnected) return;
|
||||
const lint = toLintDiagnostics(this.view.state.doc, diagnosticsFor(path));
|
||||
const tr = setDiagnostics(this.view.state, lint);
|
||||
this.view.dispatch({ ...tr, effects: [...[tr.effects ?? []].flat(), setMarks.of(diagnosticMarks(lint))] });
|
||||
this.view.dispatch({
|
||||
...tr,
|
||||
effects: [...[tr.effects ?? []].flat(), setMarks.of(diagnosticMarks(lint)), intentionsChanged.of(null)],
|
||||
});
|
||||
});
|
||||
}
|
||||
destroy() {
|
||||
|
||||
@@ -98,7 +98,7 @@ function renderDiagnostics(view: EditorView, diags: PlacedDiagnostic[]): HTMLEle
|
||||
fix.className = "cm-lsp-link-button";
|
||||
const keys = shortcutLabel("code.showIntentions");
|
||||
fix.textContent = keys ? `More actions… ${keys}` : "More actions…";
|
||||
if (d.severity === "error" || d.severity === "warning") offerQuickFix(view, meta, fix, from, to);
|
||||
if (d.severity !== "hint") offerQuickFix(view, meta, fix, from, to);
|
||||
fix.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
fix.addEventListener("click", () => {
|
||||
view.dispatch({ selection: EditorSelection.cursor(from) });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { setDiagnostics } from "@codemirror/lint";
|
||||
import type { CodeAction } from "@/bindings";
|
||||
|
||||
const resolved: CodeAction = {
|
||||
@@ -35,6 +36,7 @@ const {
|
||||
intentionPreview,
|
||||
lightsBulb,
|
||||
listedCodeActions,
|
||||
listedIntentions,
|
||||
withInspectionControls,
|
||||
} = await import("./intentions");
|
||||
const { setFileDiagnostics } = await import("./diagnostics");
|
||||
@@ -90,6 +92,27 @@ describe("inspection controls in ⌥↵ (issue #42)", () => {
|
||||
setFileDiagnostics("/p/two.py", "ruff", []);
|
||||
expect(withInspectionControls(view, [])).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["info", "warning", "error"] as const)(
|
||||
"uses the effective %s severity for local and LSP fixes",
|
||||
(severity) => {
|
||||
const path = `/p/severity-${severity}.py`;
|
||||
setFileDiagnostics(path, "ruff", [{ ...unused, severity }]);
|
||||
const view = editorAt(path);
|
||||
view.dispatch(setDiagnostics(view.state, [{ from: 7, to: 9, severity, message: unused.message }]));
|
||||
const items = listedIntentions(view, [
|
||||
{ id: "refactor", label: "Extract method", icon: "bulb", run: vi.fn() },
|
||||
{ id: "local", label: "Local fix", quickFix: true, run: vi.fn() },
|
||||
{ id: "lsp", label: "LSP fix", quickFix: true, emphasis: true, run: vi.fn() },
|
||||
]);
|
||||
expect(items.map((i) => i.id)).toEqual(["lsp", "local", "refactor"]);
|
||||
expect(items.slice(0, 2).map((i) => i.icon)).toEqual(Array(2).fill(severity === "error" ? "bulb-fix" : "bulb"));
|
||||
const controls = withInspectionControls(view, items);
|
||||
expect(controls.slice(0, 2).every((i) => i.submenu?.length === 2)).toBe(true);
|
||||
expect(controls[2]!.submenu).toBeUndefined();
|
||||
setFileDiagnostics(path, "ruff", []);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("intention preview", () => {
|
||||
|
||||
@@ -9,7 +9,13 @@ import { forEachDiagnostic } from "@codemirror/lint";
|
||||
import { indentUnit } from "@codemirror/language";
|
||||
import { diff } from "@codemirror/merge";
|
||||
import { commands, isCancelled, type CodeAction } from "@/platform/ipc";
|
||||
import { contributedIntentions, intentionProviders, whenSynced } from "@/editor/api";
|
||||
import {
|
||||
contributedIntentions,
|
||||
contributedQuickFixes,
|
||||
intentionProviders,
|
||||
intentionsChanged,
|
||||
whenSynced,
|
||||
} from "@/editor/api";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { debounce, docOf, isUnavailable, lspRequest, reportLspError } from "./lsp";
|
||||
import { offsetsToRange } from "./positions";
|
||||
@@ -32,6 +38,27 @@ const codeActionBackoff = featureBackoff();
|
||||
|
||||
const isQuickFix = (a: CodeAction) => !!a.kind && (a.kind === "quickfix" || a.kind.startsWith("quickfix."));
|
||||
|
||||
/** The red bulb is reserved for errors; other inspections use the ordinary bulb. */
|
||||
export function quickFixIcon(view: EditorView): "bulb" | "bulb-fix" {
|
||||
const sel = view.state.selection.main;
|
||||
const line = view.state.doc.lineAt(sel.head);
|
||||
let error = false;
|
||||
forEachDiagnostic(view.state, (d, from, to) => {
|
||||
if (d.severity === "error" && from <= line.to && to >= line.from) error = true;
|
||||
});
|
||||
return error ? "bulb-fix" : "bulb";
|
||||
}
|
||||
|
||||
const isFixItem = (item: ListPopupItem) => item.quickFix ?? item.icon === "bulb-fix";
|
||||
|
||||
/** Local and language-server fixes share icons and the same stable ordering. */
|
||||
export function listedIntentions(view: EditorView, items: ListPopupItem[]): ListPopupItem[] {
|
||||
const rank = (item: ListPopupItem) => (item.disabled ? 4 : 0) + (isFixItem(item) ? 0 : 2) + (item.emphasis ? 0 : 1);
|
||||
return items
|
||||
.map((item) => (isFixItem(item) ? { ...item, quickFix: true, icon: quickFixIcon(view) } : item))
|
||||
.sort((a, b) => rank(a) - rank(b));
|
||||
}
|
||||
|
||||
/** IDEA ordering: quick fixes first, preferred before others, disabled last; stable otherwise. */
|
||||
export function sortCodeActions(actions: readonly CodeAction[]): CodeAction[] {
|
||||
const rank = (a: CodeAction) => (a.disabledReason ? 4 : 0) + (isQuickFix(a) ? 0 : 2) + (a.isPreferred ? 0 : 1);
|
||||
@@ -104,15 +131,23 @@ async function fetchIntentions(view: EditorView): Promise<CodeAction[]> {
|
||||
return actions;
|
||||
}
|
||||
|
||||
/** Enabled quick fixes of the problem at `from..to`, preferred first. */
|
||||
export async function quickFixesAt(view: EditorView, from: number, to: number): Promise<CodeAction[]> {
|
||||
export type EditorQuickFix = CodeAction | { title: string; run: () => void | Promise<void> };
|
||||
|
||||
/** Enabled quick fixes of the problem at `from..to`, including local inspections. */
|
||||
export async function quickFixesAt(view: EditorView, from: number, to: number): Promise<EditorQuickFix[]> {
|
||||
const local = contributedQuickFixes(view, from, to).map((fix) => ({ title: fix.label, run: fix.run }));
|
||||
const doc = docOf(view);
|
||||
if (!doc) return [];
|
||||
await whenSynced(view);
|
||||
const actions = await lspRequest(() =>
|
||||
commands.lspCodeActions(doc.doc, offsetsToRange(view.state.doc, from, to), ["quickfix"]),
|
||||
);
|
||||
return sortCodeActions(actions.filter((a) => isQuickFix(a) && !a.disabledReason));
|
||||
if (!doc) return local;
|
||||
try {
|
||||
await whenSynced(view);
|
||||
const actions = await lspRequest(() =>
|
||||
commands.lspCodeActions(doc.doc, offsetsToRange(view.state.doc, from, to), ["quickfix"]),
|
||||
);
|
||||
return [...local, ...sortCodeActions(actions.filter((a) => isQuickFix(a) && !a.disabledReason))];
|
||||
} catch (e) {
|
||||
if (local.length) return local;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +179,8 @@ export async function applyQuickFix(view: EditorView) {
|
||||
showHint(view, "No quick fixes available");
|
||||
}
|
||||
|
||||
export async function applyCodeAction(view: EditorView, action: CodeAction) {
|
||||
export async function applyCodeAction(view: EditorView, action: EditorQuickFix) {
|
||||
if ("run" in action) return action.run();
|
||||
const doc = docOf(view);
|
||||
if (!doc) return;
|
||||
if (action.disabledReason) {
|
||||
@@ -276,9 +312,10 @@ function actionItems(
|
||||
resolved?: Map<CodeAction, Promise<CodeAction>>,
|
||||
): ListPopupItem[] {
|
||||
return listedCodeActions(actions).map((a, i) => ({
|
||||
id: `${i}`,
|
||||
id: `lsp:${i}`,
|
||||
label: a.title,
|
||||
icon: isQuickFix(a) ? "bulb-fix" : "bulb",
|
||||
icon: isQuickFix(a) ? quickFixIcon(view) : "bulb",
|
||||
quickFix: isQuickFix(a),
|
||||
emphasis: a.isPreferred,
|
||||
disabled: a.disabledReason,
|
||||
// The edit the preview already resolved is applied as is.
|
||||
@@ -344,7 +381,7 @@ export function withInspectionControls(view: EditorView, items: ListPopupItem[])
|
||||
(i) => i.id !== SPELLING_INSPECTION,
|
||||
);
|
||||
if (!inspections.length) return items;
|
||||
const fixes = items.filter((i) => i.icon === "bulb-fix" && !i.submenu);
|
||||
const fixes = items.filter((i) => isFixItem(i) && !i.submenu);
|
||||
if (inspections.length === 1 && fixes.length) {
|
||||
const submenu = intentionControls(inspections[0]!);
|
||||
return items.map((i) => (fixes.includes(i) ? { ...i, submenu } : i));
|
||||
@@ -364,7 +401,7 @@ export function withInspectionControls(view: EditorView, items: ListPopupItem[])
|
||||
/** ⌥↵ */
|
||||
export async function showIntentions(view: EditorView) {
|
||||
try {
|
||||
// Typo fixes and feature-contributed actions come first and need no language server.
|
||||
// Local inspections need no language server.
|
||||
const local: ListPopupItem[] = [...spellingIntentions(view), ...contributedIntentions(view)];
|
||||
let actions: CodeAction[] = [];
|
||||
let failure: { error: unknown } | null = null;
|
||||
@@ -376,7 +413,9 @@ export async function showIntentions(view: EditorView) {
|
||||
}
|
||||
const sorted = listedCodeActions(actions);
|
||||
const resolved = new Map<CodeAction, Promise<CodeAction>>();
|
||||
const items = withInspectionControls(view, [...local, ...actionItems(view, sorted, resolved)]);
|
||||
const serverItems = actionItems(view, sorted, resolved);
|
||||
const previews = new Map(serverItems.map((item, i) => [item.id, sorted[i]!]));
|
||||
const items = withInspectionControls(view, listedIntentions(view, [...local, ...serverItems]));
|
||||
if (!items.length) {
|
||||
if (failure) throw failure.error;
|
||||
showHint(view, "No intentions available");
|
||||
@@ -388,7 +427,7 @@ export async function showIntentions(view: EditorView) {
|
||||
title: "Intentions",
|
||||
items,
|
||||
preview: (item, signal) => {
|
||||
const action = sorted[items.indexOf(item) - local.length];
|
||||
const action = previews.get(item.id);
|
||||
return action ? intentionPreview(view, action, resolved, signal) : Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
@@ -500,7 +539,13 @@ const bulbPlugin = ViewPlugin.fromClass(
|
||||
this.lookup();
|
||||
}
|
||||
update(u: ViewUpdate) {
|
||||
if (u.docChanged || u.selectionSet || (u.focusChanged && u.view.hasFocus)) this.lookup();
|
||||
if (
|
||||
u.docChanged ||
|
||||
u.selectionSet ||
|
||||
(u.focusChanged && u.view.hasFocus) ||
|
||||
u.transactions.some((tr) => tr.effects.some((e) => e.is(intentionsChanged)))
|
||||
)
|
||||
this.lookup();
|
||||
else if (u.focusChanged) {
|
||||
this.lookup.cancel();
|
||||
// A blur dispatched from inside the update is not allowed.
|
||||
@@ -516,16 +561,16 @@ const bulbPlugin = ViewPlugin.fromClass(
|
||||
if (!doc || doc.readOnly || !this.view.dom.isConnected || !this.view.hasFocus) return;
|
||||
const state = this.view.state;
|
||||
const typoFix = spellingIntentions(this.view).length > 0;
|
||||
const local = typoFix || contributedIntentions(this.view).length > 0;
|
||||
const selection = state.selection.main;
|
||||
const ownFix = contributedQuickFixes(this.view, selection.from, selection.to).length > 0;
|
||||
const local = typoFix || ownFix || contributedIntentions(this.view).length > 0;
|
||||
const backoff = codeActionBackoff.active(doc.doc);
|
||||
if (backoff && !local) return;
|
||||
let actions: CodeAction[] = [];
|
||||
if (!backoff) {
|
||||
try {
|
||||
actions = await fetchIntentions(this.view);
|
||||
} catch (e) {
|
||||
if (isUnavailable(e)) codeActionBackoff.mark(doc.doc);
|
||||
if (!local) return;
|
||||
}
|
||||
}
|
||||
if (this.view.state.doc !== state.doc || !this.view.state.selection.eq(state.selection) || !this.view.hasFocus)
|
||||
@@ -533,7 +578,14 @@ const bulbPlugin = ViewPlugin.fromClass(
|
||||
const line = state.doc.lineAt(state.selection.main.head).number;
|
||||
const any = actions.some(lightsBulb) || local;
|
||||
this.view.dispatch({
|
||||
effects: setBulb.of(any ? { line, quickFix: typoFix || actions.some(isQuickFix) } : null),
|
||||
effects: setBulb.of(
|
||||
any
|
||||
? {
|
||||
line,
|
||||
quickFix: (typoFix || ownFix || actions.some(isQuickFix)) && quickFixIcon(this.view) === "bulb-fix",
|
||||
}
|
||||
: null,
|
||||
),
|
||||
});
|
||||
}
|
||||
destroy() {
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface ListPopupItem {
|
||||
/** Second line / muted text after the label (code preview). */
|
||||
preview?: string;
|
||||
icon?: string;
|
||||
/** Inspection fix, independent of the icon chosen for its severity. */
|
||||
quickFix?: boolean;
|
||||
/** Symbol badge kind (`ui/icons` `SymbolBadge`) shown instead of an icon. */
|
||||
symbol?: string;
|
||||
emphasis?: boolean;
|
||||
|
||||
Reference in new issue
Заблокувати користувача