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
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
/** Language server status: `lsp_servers` snapshot + `languageServer` events. */
|
|
import { getAction } from "@/platform/actions";
|
|
import { create } from "zustand";
|
|
import type { ServerId, ServerInfo, ServerStatus } from "@/bindings";
|
|
import { call, commands, isTauri, onIdeEvent } from "@/platform/ipc";
|
|
import { useProject } from "@/platform/project";
|
|
import { notifications } from "@/workbench/contributions";
|
|
|
|
export interface ServerEntry {
|
|
id: ServerId;
|
|
name: string;
|
|
status: ServerStatus;
|
|
message: string | null;
|
|
languages: string[];
|
|
root: string | null;
|
|
}
|
|
|
|
interface ServersState {
|
|
servers: ReadonlyMap<ServerId, ServerEntry>;
|
|
}
|
|
|
|
export const useServers = create<ServersState>(() => ({ servers: new Map() }));
|
|
|
|
const notifiedMissing = new Set<string>();
|
|
|
|
/** Event count at each server's latest event, so a snapshot taken earlier doesn't undo it. */
|
|
let eventSeq = 0;
|
|
const lastEvent = new Map<ServerId, number>();
|
|
|
|
function notifyMissing(e: ServerEntry) {
|
|
if (notifiedMissing.has(e.name)) return;
|
|
notifiedMissing.add(e.name);
|
|
// features/tools shows the same notification with an Install button.
|
|
if (getAction("tools.install")) return;
|
|
notifications().notify({
|
|
group: "lsp",
|
|
level: "warning",
|
|
title: `Language server '${e.name}' is not installed`,
|
|
body: e.message ?? "Code insight features are unavailable for these files until it is installed.",
|
|
key: `lsp-missing-${e.name}`,
|
|
});
|
|
}
|
|
|
|
export function upsertServer(entry: Partial<ServerEntry> & { id: ServerId; name: string; status: ServerStatus }) {
|
|
const prev = useServers.getState().servers.get(entry.id);
|
|
const next: ServerEntry = {
|
|
languages: prev?.languages ?? [],
|
|
root: prev?.root ?? null,
|
|
message: null,
|
|
...prev,
|
|
...entry,
|
|
};
|
|
useServers.setState((s) => ({ servers: new Map(s.servers).set(entry.id, next) }));
|
|
if (next.status === "missing") notifyMissing(next);
|
|
}
|
|
|
|
export async function refreshServers() {
|
|
if (!useProject.getState().project) {
|
|
useServers.setState({ servers: new Map() });
|
|
return;
|
|
}
|
|
if (!isTauri()) return;
|
|
try {
|
|
const since = eventSeq;
|
|
const list: ServerInfo[] = await call(commands.lspServers());
|
|
const prev = useServers.getState().servers;
|
|
const map = new Map<ServerId, ServerEntry>();
|
|
for (const s of list) {
|
|
const p = prev.get(s.id);
|
|
const newer = p && (lastEvent.get(s.id) ?? 0) > since;
|
|
map.set(s.id, {
|
|
id: s.id,
|
|
name: s.name,
|
|
status: newer ? p.status : s.status,
|
|
languages: s.languages,
|
|
root: s.root,
|
|
message: p?.message ?? null,
|
|
});
|
|
}
|
|
useServers.setState({ servers: map });
|
|
for (const e of map.values()) if (e.status === "missing") notifyMissing(e);
|
|
} catch {
|
|
// No project / backend not ready; events will fill in.
|
|
}
|
|
}
|
|
|
|
export async function restartServer(id: ServerId) {
|
|
try {
|
|
await call(commands.lspRestart(id));
|
|
} catch (e) {
|
|
notifications().notify({
|
|
group: "lsp",
|
|
level: "error",
|
|
title: "Failed to restart language server",
|
|
body: e instanceof Error ? e.message : String(e),
|
|
});
|
|
}
|
|
}
|
|
|
|
export function installServerEvents(): () => void {
|
|
const off = onIdeEvent("languageServer", (e) => {
|
|
const known = useServers.getState().servers.has(e.server);
|
|
lastEvent.set(e.server, ++eventSeq);
|
|
upsertServer({ id: e.server, name: e.name, status: e.status, message: e.message });
|
|
if (!known) void refreshServers();
|
|
});
|
|
let root = useProject.getState().project?.root ?? null;
|
|
void refreshServers();
|
|
const unsub = useProject.subscribe((s) => {
|
|
const r = s.project?.root ?? null;
|
|
if (r === root) return;
|
|
root = r;
|
|
notifiedMissing.clear();
|
|
void refreshServers();
|
|
});
|
|
return () => {
|
|
off();
|
|
unsub();
|
|
};
|
|
}
|
|
|
|
export const STATUS_LABEL: Record<ServerStatus, string> = {
|
|
starting: "Starting…",
|
|
ready: "Running",
|
|
busy: "Busy",
|
|
crashed: "Crashed",
|
|
stopped: "Stopped",
|
|
missing: "Not installed",
|
|
misconfigured: "Not started",
|
|
};
|
|
|
|
/**
|
|
* A missing server whose languages another installed one serves (vtsls missing,
|
|
* typescript-language-server running as its fallback) is not a problem.
|
|
*/
|
|
export function servedByOther(server: ServerEntry, servers: readonly ServerEntry[]): boolean {
|
|
return (
|
|
server.languages.length > 0 &&
|
|
server.languages.every((l) => servers.some((o) => o !== server && o.status !== "missing" && o.languages.includes(l)))
|
|
);
|
|
}
|
|
|
|
/** One-line summary for the status bar. */
|
|
export function serversSummary(servers: readonly ServerEntry[]): { text: string; tone: "ok" | "busy" | "error" | "idle" } | null {
|
|
if (!servers.length) return null;
|
|
const bad = servers.filter(
|
|
(s) => s.status === "crashed" || s.status === "misconfigured" || (s.status === "missing" && !servedByOther(s, servers)),
|
|
);
|
|
const busy = servers.filter((s) => s.status === "starting" || s.status === "busy");
|
|
const tone = bad.length ? "error" : busy.length ? "busy" : servers.some((s) => s.status === "ready") ? "ok" : "idle";
|
|
const names = [...new Set(servers.map((s) => s.name))];
|
|
const text = names.length === 1 ? names[0]! : `${names[0]} +${names.length - 1}`;
|
|
return { text, tone };
|
|
}
|