Editor: quick fixes, completion, navigation while typing, and split-pane drag (#109)

* 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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-28 09:07:50 +08:00
1 джерело 48ac2094fc
коміт 357b728f1d
71 files changed
+1630 -197

No files matched your search

+13
Переглянути файл
@@ -28,14 +28,17 @@ import { useEffectiveRoots } from "@/platform/projectRoots";
import { macStroke, strokeOf } from "@/platform/keymap";
import { prepareMenuGroups, useFileDecoration } from "@/workbench/contributions";
import {
FILES_DRAG,
HighlightedText,
Icon,
Popup,
dragSession,
Tree,
acquirePopup,
fileIconName,
folderIconName,
showContextMenu,
type DragSession,
type TreeHandle,
type TreeRowState,
} from "@/ui";
@@ -443,6 +446,8 @@ export function ProjectView() {
startY = e.clientY;
let active = false;
let paths: string[] = [];
// Files (not folders) can also be dropped on an editor to open them there.
let files: DragSession | null = null;
const move = (ev: PointerEvent) => {
if (!active) {
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) < DRAG_THRESHOLD) return;
@@ -454,10 +459,14 @@ export function ProjectView() {
const nodes = keys.map((k) => m.byKey.get(k)).filter((n): n is ProjectNode => !!n && !n.root && !n.placeholder);
paths = nodes.map((n) => n.path);
if (!paths.length) return cleanup();
const filePaths = nodes.filter((n) => !n.dir).map((n) => n.path);
if (filePaths.length) files = dragSession({ type: FILES_DRAG, payload: filePaths.join("\n") });
}
const target = folderAt(containerRef.current, ev.clientX, ev.clientY);
const valid = target && !paths.some((p) => target.path === p || isInside(target.path, p));
setDropTarget(valid ? target.key : null);
if (target) files?.cancel();
else files?.move(ev.clientX, ev.clientY);
setDrag({
paths,
label: paths.length === 1 ? nameOf(paths[0]!) : tx`${paths.length} items`,
@@ -469,8 +478,11 @@ export function ProjectView() {
const up = (ev: PointerEvent) => {
const target = active ? folderAt(containerRef.current, ev.clientX, ev.clientY) : null;
const valid = target && !paths.some((p) => target.path === p || isInside(target.path, p));
const session = files;
files = null;
cleanup();
if (active && valid) void dropPaths(paths, target.path, ev.altKey);
else if (active && !target) session?.drop(ev.clientX, ev.clientY);
};
const key = (ev: KeyboardEvent) => {
if (ev.key === "Escape" && active) {
@@ -483,6 +495,7 @@ export function ProjectView() {
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
window.removeEventListener("keydown", key, true);
files?.cancel();
setDropTarget(null);
setDrag(null);
};