mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
* feat(editor): Drag tabs and files between split panes
HTML5 drag and drop never reaches the webview while native file drop is
enabled, so tab, project-file and changelist drags did nothing. They now
run on pointer events:
- Drag an editor tab onto another pane, or onto a pane's edge to split it.
- Drag files from the Project view into an editor pane (center opens them
there, an edge opens them in a new split).
- Drag files between changelists in the Commit view.
- "Drag-and-drop with Alt pressed only" applies to tabs and stripe buttons.
* fix(lsp): Deliver server settings and start servers on a supported JDK
- workspace/configuration answers a server's own section (basedpyright)
from its settings block; basedpyright runs in "standard" mode and keeps
auto search paths on, so `src/` layouts resolve from `tests/`.
- jdtls: formatting follows the editor indent, calls complete without
guessed arguments, and JDK internals stay out of completion.
- `max-java` in a language server config picks an installed JDK no newer
than that (kotlin-language-server fails on JDK 25).
- Server initialization may take up to 5 minutes (first Gradle import).
- `lsp_resend_diagnostics` re-emits stored diagnostics after a reload.
- Spelling only checks declared names (not called methods like
`substring`) and accepts plurals of known software terms.
* feat(editor): Quick fixes, completion and navigation while typing
Quick fixes
- Errors get a red quick-fix bulb; Apply Quick Fix (⌥⇧↵) fixes the most
severe problem at the caret, and the error hover offers the fix directly.
- Code generated by a code action is re-indented to the file's style.
- Python: Implement abstract methods (⌃I or the bulb) from the bases.
- Duplicate code actions are listed once.
Completion
- Overloads sharing a name, and a name importable from several modules,
are no longer merged into one entry.
- Auto-import entries show the module they import from.
- Call snippets without a stop (`new ArrayList<>()`) leave the caret inside
the parentheses with parameter info, or past them when nothing takes
arguments; parameter info also opens after server call snippets.
- The selected item is resolved ahead of acceptance, so its auto-import
is applied (rust-analyzer).
Navigation and diagnostics
- Language server requests wait for pending edits, so Go to Declaration
right after typing answers for the current text; an empty answer right
after an edit is retried while the server catches up.
- Diagnostics come back after a window reload; error underlines stay
visible under typo / unused highlighting.
- Implementation gutter markers no longer refresh in a loop while the
server reports progress.
- A missing alternative server no longer shows as a server error.
* chore(lint): Skip agent worktrees under .claude
* fix(editor): Skip cancelled LSP requests and limit generated-code reformat
- Requests cancelled while waiting for pending edits are not sent.
- Code generated by an action is reformatted per changed hunk, so an added
import doesn't reformat the code between it and the generated lines.
- A long busy spell caused by a settle refresh re-runs it only once.
- Files dropped on a file row of another changelist move to that changelist.
- Python Implement methods: a nested class without bases ends the lookup,
class bodies continue through unindented string lines, and
`@abstractproperty` stubs become `@property`.
- Resend diagnostics goes through the host ops, and custom servers get
`max_java`.
* fix(editor): Rank the exact type first in Java constructor completion
jdtls appends the call or body to filterText (`String(${0})`); matching
on the label instead lets typing `String` rank `String` above
`StringBuilder` and the other types sharing its sort text.
* fix(editor): List identical completion items once
kotlin-language-server repeats a class once per classpath entry declaring
it, so `String()` and `String` were listed three times each.
* fix(editor): Keep the internal cancellation message out of the UI text catalog
155 lines
6.1 KiB
TypeScript
155 lines
6.1 KiB
TypeScript
/**
|
||
* Runtime side of Settings › Appearance and System Settings options that no
|
||
* other feature owns: root attributes read by tokens / global CSS (compact
|
||
* mode, contrast scrollbars, red-green palette, antialiasing, menu icons…),
|
||
* the UI font, IDE zoom (+ ⌃⌥= / ⌃⌥- / ⌃⌥0 actions), "Drag-and-drop with Alt
|
||
* pressed only" and "Save files if the IDE is idle".
|
||
*/
|
||
import { executeAction, type Action } from "@/platform/actions";
|
||
import { call, commands, isTauri } from "@/platform/ipc";
|
||
import { useProject } from "@/platform/project";
|
||
import { setDragRequiresAlt } from "@/ui";
|
||
import { ZOOM_LIMITS } from "./model/behaviorSchema";
|
||
import { DEFAULT_SETTINGS, validateSettings, type ValidSettings } from "./model/schema";
|
||
|
||
const FONT_VARS = [
|
||
["--fs-ui", 0],
|
||
["--fs-small", -1],
|
||
["--fs-tiny", -2],
|
||
] as const;
|
||
|
||
function flag(root: HTMLElement, name: string, on: boolean, value = "") {
|
||
if (on) root.dataset[name] = value;
|
||
else delete root.dataset[name];
|
||
}
|
||
|
||
/** Root attributes and CSS variables for the Appearance options. */
|
||
export function applyAppearanceOptions(s: ValidSettings, root: HTMLElement = document.documentElement): void {
|
||
const a = s.appearance;
|
||
// "Use custom font": family + size; otherwise the theme's.
|
||
const size = a.useCustomFont ? a.uiFontSize : DEFAULT_SETTINGS.appearance.uiFontSize;
|
||
for (const [name, delta] of FONT_VARS) {
|
||
if (size === DEFAULT_SETTINGS.appearance.uiFontSize) root.style.removeProperty(name);
|
||
else root.style.setProperty(name, `${size + delta}px`);
|
||
}
|
||
if (a.useCustomFont)
|
||
root.style.setProperty("--font-ui", `"${a.uiFontFamily.replace(/"/g, "")}", system-ui, sans-serif`);
|
||
else root.style.removeProperty("--font-ui");
|
||
|
||
flag(root, "compact", a.compactMode);
|
||
flag(root, "contrastScrollbars", a.contrastScrollbars);
|
||
flag(root, "redGreen", a.redGreenVision);
|
||
flag(root, "toolWindowBg", a.differentToolWindowBackground, "different");
|
||
flag(root, "projectColors", !a.projectColorsInToolbar, "off");
|
||
flag(root, "menuIcons", !a.menuIcons, "off");
|
||
root.dataset.aaIde = a.ideAntialiasing;
|
||
root.dataset.aaEditor = a.editorAntialiasing;
|
||
applyZoom(a.zoom, root);
|
||
}
|
||
|
||
let lastZoom = 100;
|
||
|
||
function applyZoom(percent: number, root: HTMLElement) {
|
||
if (percent === lastZoom && (isTauri() || !root.style.zoom)) return;
|
||
lastZoom = percent;
|
||
if (isTauri()) {
|
||
void import("@tauri-apps/api/webview")
|
||
.then(({ getCurrentWebview }) => getCurrentWebview().setZoom(percent / 100))
|
||
.catch((e) => console.warn("cannot set zoom", e));
|
||
} else if (percent === 100) root.style.removeProperty("zoom");
|
||
else root.style.setProperty("zoom", String(percent / 100));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Zoom actions (IDEA: View › Appearance › Zoom IDE)
|
||
|
||
const ZOOM_STEPS = [50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300];
|
||
|
||
export function nextZoom(current: number, direction: 1 | -1): number {
|
||
const steps = direction > 0 ? ZOOM_STEPS : [...ZOOM_STEPS].reverse();
|
||
const hit = steps.find((z) => (direction > 0 ? z > current : z < current));
|
||
return Math.min(ZOOM_LIMITS.max, Math.max(ZOOM_LIMITS.min, hit ?? current));
|
||
}
|
||
|
||
async function setZoom(percent: number) {
|
||
if (!isTauri()) {
|
||
const settings = useProject.getState().settings;
|
||
if (settings)
|
||
useProject.setState({ settings: { ...settings, appearance: { ...settings.appearance, zoom: percent } } });
|
||
else applyZoom(percent, document.documentElement);
|
||
return;
|
||
}
|
||
const settings = await call(commands.settingsSet("global", "appearance.zoom", JSON.stringify(percent)));
|
||
useProject.setState({ settings });
|
||
}
|
||
|
||
const currentZoom = () => validateSettings(useProject.getState().settings).appearance.zoom;
|
||
|
||
export function zoomActions(): Action[] {
|
||
return [
|
||
{
|
||
id: "view.zoomIn",
|
||
title: "Zoom In IDE",
|
||
category: "View",
|
||
keywords: ["zoom", "scale", "bigger", "accessibility"],
|
||
run: () => setZoom(nextZoom(currentZoom(), 1)),
|
||
},
|
||
{
|
||
id: "view.zoomOut",
|
||
title: "Zoom Out IDE",
|
||
category: "View",
|
||
keywords: ["zoom", "scale", "smaller", "accessibility"],
|
||
run: () => setZoom(nextZoom(currentZoom(), -1)),
|
||
},
|
||
{
|
||
id: "view.resetZoom",
|
||
title: "Reset IDE Zoom",
|
||
category: "View",
|
||
keywords: ["zoom", "scale", "100%", "accessibility"],
|
||
run: () => setZoom(100),
|
||
},
|
||
{
|
||
// View › Appearance › Compact Mode (Settings › Appearance › "Compact mode").
|
||
id: "view.toggleCompactMode",
|
||
title: "Compact Mode",
|
||
category: "View",
|
||
keywords: ["appearance", "density", "smaller"],
|
||
toggled: () => validateSettings(useProject.getState().settings).appearance.compactMode,
|
||
run: async () => {
|
||
if (!isTauri()) return;
|
||
const next = !validateSettings(useProject.getState().settings).appearance.compactMode;
|
||
useProject.setState({ settings: await call(commands.settingsSet("global", "appearance.compactMode", JSON.stringify(next))) });
|
||
},
|
||
},
|
||
];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Drag-and-drop with Alt pressed only (tool window buttons and editor tabs)
|
||
|
||
export function installAltDragGuard(enabled: () => boolean): () => void {
|
||
setDragRequiresAlt(enabled);
|
||
return () => setDragRequiresAlt(() => false);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Save files if the IDE is idle for N seconds
|
||
|
||
const INPUT_EVENTS = ["keydown", "pointerdown", "wheel"] as const;
|
||
|
||
export function installIdleSave(config: () => { enabled: boolean; seconds: number }): () => void {
|
||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||
const arm = () => {
|
||
clearTimeout(timer);
|
||
const { enabled, seconds } = config();
|
||
if (!enabled || !useProject.getState().project) return;
|
||
// Only after input: an idle IDE with nothing typed has nothing to save.
|
||
timer = setTimeout(() => void executeAction("file.save"), seconds * 1000);
|
||
};
|
||
for (const ev of INPUT_EVENTS) window.addEventListener(ev, arm, { capture: true, passive: true });
|
||
return () => {
|
||
clearTimeout(timer);
|
||
for (const ev of INPUT_EVENTS) window.removeEventListener(ev, arm, { capture: true });
|
||
};
|
||
}
|