mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-12 18:36:53 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae14a61eec |
No files matched your search
@@ -1,104 +0,0 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { java } from "@codemirror/lang-java";
|
||||
import { history, undo } from "@codemirror/commands";
|
||||
import { renderDocComments, renderedDocsExtension, renderedRanges } from "./renderedDocs";
|
||||
|
||||
let view: EditorView | null = null;
|
||||
beforeEach(() => void vi.useFakeTimers());
|
||||
afterEach(() => {
|
||||
view?.destroy();
|
||||
view = null;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const DOC = ["class Rooms {", " int n;", "", " /** Books a room. */", " void book() {}", "}"].join("\n");
|
||||
|
||||
function open(doc = DOC, extra: ReturnType<typeof history>[] = []) {
|
||||
view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
extensions: [java(), renderedDocsExtension, renderDocComments.of(true), ...extra],
|
||||
}),
|
||||
parent: document.body,
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
/** Types `text` at `at` the way a keystroke does. */
|
||||
const type = (v: EditorView, at: number, text: string) =>
|
||||
v.dispatch({ changes: { from: at, insert: text }, userEvent: "input.type" });
|
||||
|
||||
/** What a full rebuild of the same text renders, in a fresh editor state. */
|
||||
function fullRebuild(doc: string): Array<[number, number]> {
|
||||
return renderedRanges(
|
||||
EditorState.create({ doc, extensions: [java(), renderedDocsExtension, renderDocComments.of(true)] }),
|
||||
);
|
||||
}
|
||||
|
||||
const pause = () => vi.advanceTimersByTime(300);
|
||||
|
||||
it("moves rendered comments along with text typed in code, and rebuilds once typing pauses", () => {
|
||||
const v = open();
|
||||
const [before] = renderedRanges(v.state);
|
||||
// `/*` in code makes everything after it a comment: a full rebuild would drop the doc
|
||||
// comment at once. Typing only moves it along until the pause.
|
||||
type(v, v.state.doc.line(2).from + 2, "/*");
|
||||
expect(renderedRanges(v.state)).toEqual([[before![0] + 2, before![1] + 2]]);
|
||||
pause();
|
||||
expect(renderedRanges(v.state)).toEqual(fullRebuild(v.state.doc.toString()));
|
||||
expect(renderedRanges(v.state)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders a pasted comment at once", () => {
|
||||
const v = open();
|
||||
v.dispatch({
|
||||
changes: { from: v.state.doc.line(5).to + 1, insert: " /** Pasted. */\n void paste() {}\n" },
|
||||
userEvent: "input.paste",
|
||||
});
|
||||
expect(renderedRanges(v.state)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("brings a deleted comment back rendered at once on undo", () => {
|
||||
const v = open(DOC, [history()]);
|
||||
const line = v.state.doc.line(4);
|
||||
v.dispatch({ changes: { from: line.from, to: line.to + 1 }, userEvent: "delete.selection" });
|
||||
expect(renderedRanges(v.state)).toHaveLength(0);
|
||||
undo(v);
|
||||
expect(renderedRanges(v.state)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rebuilds at once for a keystroke on a comment's line", () => {
|
||||
const v = open();
|
||||
// Code after the closing marker: the comment no longer has its line to itself.
|
||||
type(v, v.state.doc.line(4).to, " int x;");
|
||||
expect(renderedRanges(v.state)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("still shows the source of a comment the caret lands in after fast typing", () => {
|
||||
const v = open();
|
||||
type(v, v.state.doc.line(2).from + 2, "xx");
|
||||
const at = v.state.doc.toString().indexOf("Books");
|
||||
v.dispatch({ selection: EditorSelection.cursor(at) });
|
||||
expect(renderedRanges(v.state)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("after the pause, matches a full rebuild of the same text, whatever was typed", () => {
|
||||
// A seeded generator, so a failure replays.
|
||||
let seed = 7;
|
||||
const next = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
|
||||
const lines = ["class A {"];
|
||||
for (let i = 0; i < 12; i++) lines.push(` /** Doc ${i}. */`, ` void m${i}() {}`, " /// line doc", "");
|
||||
lines.push("}");
|
||||
const v = open(lines.join("\n"));
|
||||
const keys = ["/", "*", "\n", "x", " ", "{", "}"];
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const at = Math.floor(next() * (v.state.doc.length + 1));
|
||||
if (next() < 0.3 && at > 0) v.dispatch({ changes: { from: at - 1, to: at }, userEvent: "delete.backward" });
|
||||
else type(v, at, keys[Math.floor(next() * keys.length)]!);
|
||||
if (next() < 0.05) pause();
|
||||
}
|
||||
pause();
|
||||
expect(renderedRanges(v.state)).toEqual(fullRebuild(v.state.doc.toString()));
|
||||
});
|
||||
@@ -25,13 +25,8 @@
|
||||
* Off, which is the default, the icon still shows over a comment, so one comment can be
|
||||
* read rendered, as in IDEA. That costs typing nothing: while the file renders no
|
||||
* comments and none is flipped, its comments are found only once typing pauses
|
||||
* (`deferredScanner`), the Markdown parser is not loaded, and the gutter has a column
|
||||
* (`passiveScanner`), the Markdown parser is not loaded, and the gutter has a column
|
||||
* only if the file has doc comments.
|
||||
*
|
||||
* On, typing costs about as little: a keystroke away from every comment's lines only
|
||||
* moves the rendered comments along with the text, and the file is scanned again once
|
||||
* typing pauses. Anything else that changes the text (paste, undo, a reload) rebuilds at
|
||||
* once, so the comments it brings in render at once.
|
||||
*/
|
||||
import {
|
||||
EditorState,
|
||||
@@ -41,7 +36,6 @@ import {
|
||||
RangeSet,
|
||||
StateEffect,
|
||||
StateField,
|
||||
type ChangeSet,
|
||||
type Extension,
|
||||
type Transaction,
|
||||
} from "@codemirror/state";
|
||||
@@ -375,55 +369,11 @@ type PlacedComment = DocComment & { line: number; end: number; rendered: boolean
|
||||
|
||||
interface DocsView {
|
||||
decorations: DecorationSet;
|
||||
/** Every doc comment, rendered or not, in document order. */
|
||||
/** Every doc comment, rendered or not. */
|
||||
comments: ReadonlyArray<PlacedComment>;
|
||||
/**
|
||||
* Moved along with keystrokes since the last scan, instead of found again: what the
|
||||
* parser made of them may differ, until `deferredScanner` rebuilds.
|
||||
*/
|
||||
stale: boolean;
|
||||
}
|
||||
|
||||
const INACTIVE: DocsView = { decorations: Decoration.none, comments: [], stale: false };
|
||||
|
||||
/** Asks for the scan a stale view is waiting for. */
|
||||
const rescanDocs = StateEffect.define<null>();
|
||||
|
||||
/** Comments moved along with a change, as an edit elsewhere moves them; their text is unchanged. */
|
||||
function mapComments(comments: ReadonlyArray<PlacedComment>, changes: ChangeSet): PlacedComment[] {
|
||||
return comments.map((c) => ({
|
||||
...c,
|
||||
from: changes.mapPos(c.from, 1),
|
||||
to: changes.mapPos(c.to, -1),
|
||||
line: changes.mapPos(c.line, 1),
|
||||
end: changes.mapPos(c.end, -1),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a change can skip the scan: a keystroke (typed or deleted characters) that
|
||||
* touches no comment's lines. Everything else (paste, undo, formatting, a reload) brings
|
||||
* comments in or out at once, so it rebuilds at once; a keystroke on a comment's line
|
||||
* moves its edges. A keystroke elsewhere can still change comments (`/*` makes the rest of
|
||||
* the file one), which the scan after the pause takes in.
|
||||
*/
|
||||
function typedOutsideComments(view: DocsView, tr: Transaction): boolean {
|
||||
if (!(tr.isUserEvent("input.type") || tr.isUserEvent("delete"))) return false;
|
||||
const { comments } = view;
|
||||
let touches = false;
|
||||
tr.changes.iterChangedRanges((fromA, toA) => {
|
||||
// The first comment that does not end before the change.
|
||||
let lo = 0;
|
||||
let hi = comments.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (comments[mid]!.end < fromA) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo < comments.length && comments[lo]!.line <= toA) touches = true;
|
||||
});
|
||||
return !touches;
|
||||
}
|
||||
const INACTIVE: DocsView = { decorations: Decoration.none, comments: [] };
|
||||
|
||||
/**
|
||||
* Rendered comments by their source, shared by every editor, least recently used first.
|
||||
@@ -492,7 +442,7 @@ function build(state: EditorState): DocsView {
|
||||
),
|
||||
);
|
||||
}
|
||||
return { decorations: Decoration.set(decorations), comments, stale: false };
|
||||
return { decorations: Decoration.set(decorations), comments };
|
||||
}
|
||||
|
||||
/** Start of the comment the mouse is over, or null. Cleared by any edit, which moves the text under it. */
|
||||
@@ -526,7 +476,7 @@ const setPassive = StateEffect.define<readonly PlacedComment[]>();
|
||||
/**
|
||||
* The doc comments of a file that renders none, for their icons. Rescanning the file on
|
||||
* each keystroke would make typing in a large file several times slower for a feature
|
||||
* that is off, so `deferredScanner` rescans once typing pauses; meanwhile the comments
|
||||
* that is off, so `passiveScanner` rescans once typing pauses; meanwhile the comments
|
||||
* found are mapped through the edits. Empty while the file renders comments: `docsField`
|
||||
* has them then.
|
||||
*/
|
||||
@@ -537,29 +487,32 @@ const passiveDocs = StateField.define<readonly PlacedComment[]>({
|
||||
// Switched off: the comments rendered a moment ago are known only to the last scan.
|
||||
if (active(tr.startState)) return placeComments(tr.state);
|
||||
for (const e of tr.effects) if (e.is(setPassive)) return e.value;
|
||||
return tr.docChanged ? mapComments(value, tr.changes) : value;
|
||||
if (!tr.docChanged) return value;
|
||||
const map = (pos: number, assoc: number) => tr.changes.mapPos(pos, assoc);
|
||||
return value.map((c) => ({
|
||||
...c,
|
||||
from: map(c.from, 1),
|
||||
to: map(c.to, -1),
|
||||
line: map(c.line, 1),
|
||||
end: map(c.end, -1),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Scans the file once edits and the parser have paused: a file that renders no comments,
|
||||
* for their icons; one that does, when keystrokes have only moved its comments along.
|
||||
*/
|
||||
const deferredScanner = ViewPlugin.fromClass(
|
||||
/** Scans a file that renders no comments once the edits and the parser have paused. */
|
||||
const passiveScanner = ViewPlugin.fromClass(
|
||||
class {
|
||||
timer: ReturnType<typeof setTimeout> | null = null;
|
||||
constructor(readonly view: EditorView) {}
|
||||
update(update: ViewUpdate) {
|
||||
if (!update.docChanged && syntaxTree(update.startState) === syntaxTree(update.state)) return;
|
||||
// Rendering and up to date: the view was just built, nothing is waiting.
|
||||
if (active(update.state) && !update.state.field(docsField).stale) return this.cancel();
|
||||
this.cancel();
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = null;
|
||||
const { state } = this.view;
|
||||
if (!active(state)) this.view.dispatch({ effects: setPassive.of(placeComments(state)) });
|
||||
else if (state.field(docsField).stale) this.view.dispatch({ effects: rescanDocs.of(null) });
|
||||
}, RESCAN_DELAY);
|
||||
if (active(update.state)) return this.cancel();
|
||||
if (update.docChanged || syntaxTree(update.startState) !== syntaxTree(update.state)) {
|
||||
this.cancel();
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = null;
|
||||
if (!active(this.view.state)) this.view.dispatch({ effects: setPassive.of(placeComments(this.view.state)) });
|
||||
}, PASSIVE_SCAN_DELAY);
|
||||
}
|
||||
}
|
||||
cancel() {
|
||||
if (this.timer !== null) clearTimeout(this.timer);
|
||||
@@ -571,8 +524,8 @@ const deferredScanner = ViewPlugin.fromClass(
|
||||
},
|
||||
);
|
||||
|
||||
/** How long edits and parsing must pause before the file is scanned again. */
|
||||
const RESCAN_DELAY = 300;
|
||||
/** How long edits and parsing must pause before a file that renders no comments is scanned. */
|
||||
const PASSIVE_SCAN_DELAY = 300;
|
||||
|
||||
/** Every doc comment of the file, as last found. */
|
||||
function placedComments(state: EditorState): ReadonlyArray<PlacedComment> {
|
||||
@@ -634,26 +587,13 @@ const docsField = StateField.define<DocsView>({
|
||||
create: build,
|
||||
update(value, tr) {
|
||||
if (tr.effects.some((e) => e.is(markdownLoaded))) renderCache.clear();
|
||||
if (
|
||||
const rebuild =
|
||||
tr.docChanged ||
|
||||
tr.reconfigured ||
|
||||
tr.effects.some((e) => e.is(toggleRenderedView) || e.is(markdownLoaded) || e.is(relabel) || e.is(rescanDocs))
|
||||
)
|
||||
return build(tr.state);
|
||||
if (tr.docChanged) {
|
||||
// A keystroke away from every comment moves them along; the scan waits for a pause.
|
||||
if (active(tr.state) && typedOutsideComments(value, tr))
|
||||
return {
|
||||
decorations: value.decorations.map(tr.changes),
|
||||
comments: mapComments(value.comments, tr.changes),
|
||||
stale: true,
|
||||
};
|
||||
return build(tr.state);
|
||||
}
|
||||
// The parser finishing more of the file can bring comments it had not reached: built
|
||||
// at once, so a file opens with the comments on screen rendered, unless keystrokes
|
||||
// already have the scan waiting for a pause.
|
||||
if (syntaxTree(tr.startState) !== syntaxTree(tr.state)) return value.stale ? value : build(tr.state);
|
||||
return value;
|
||||
tr.effects.some((e) => e.is(toggleRenderedView) || e.is(markdownLoaded) || e.is(relabel)) ||
|
||||
// The parser finishing more of the file can bring comments it had not reached.
|
||||
syntaxTree(tr.startState) !== syntaxTree(tr.state);
|
||||
return rebuild ? build(tr.state) : value;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f, (v) => v.decorations),
|
||||
});
|
||||
@@ -898,7 +838,7 @@ export const renderedDocsExtension: Extension = [
|
||||
flipped,
|
||||
docsField,
|
||||
passiveDocs,
|
||||
deferredScanner,
|
||||
passiveScanner,
|
||||
hovered,
|
||||
hoverTracker,
|
||||
showSourceAtCaret,
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import type { Branch, ChangedFile, Commit } from "@/bindings";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { maybeEditorService, useGitLogChecks, type EditorInput } from "@/workbench/contributions";
|
||||
import {
|
||||
EmptyState,
|
||||
@@ -2628,7 +2627,6 @@ export function CommitDetails({ commits }: { commits: Commit[] }) {
|
||||
},
|
||||
[scope],
|
||||
);
|
||||
useEffect(() => () => setContext("vcsLogChangesFocus", false), []);
|
||||
|
||||
if (!commit)
|
||||
return (
|
||||
@@ -2669,13 +2667,7 @@ export function CommitDetails({ commits }: { commits: Commit[] }) {
|
||||
const details = (
|
||||
<SplitGroup orientation="vertical" id="git-log-details">
|
||||
<SplitPanel id="files" defaultSize="60%" minSize="60px">
|
||||
<div
|
||||
className={styles.filesPane}
|
||||
onFocusCapture={() => setContext("vcsLogChangesFocus", true)}
|
||||
onBlurCapture={(e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setContext("vcsLogChangesFocus", false);
|
||||
}}
|
||||
>
|
||||
<div className={styles.filesPane}>
|
||||
<div className={styles.detailsBar} role="toolbar" aria-label={tx("Changed files")}>
|
||||
<IconButton
|
||||
icon="diff"
|
||||
|
||||
@@ -373,8 +373,7 @@ export function registerVcsActions(): () => void {
|
||||
icon: "diff",
|
||||
when: VCS,
|
||||
// ⌘D duplicates the line inside a text editor; Git › Show Diff still works there.
|
||||
// In the Git Log's changed files it shows the commit's diff instead.
|
||||
keyWhen: "!editorTextFocus && !vcsLogChangesFocus",
|
||||
keyWhen: "!editorTextFocus",
|
||||
run: async ({ arg, ctx }) => {
|
||||
if (ctx.shelfFocus && arg === undefined) return showShelfDiff();
|
||||
const line =
|
||||
|
||||
@@ -152,7 +152,6 @@ export function registerLogPaneActions(): () => void {
|
||||
icon: "diff",
|
||||
when: LOG,
|
||||
hidden: true,
|
||||
keyWhen: "vcsLogChangesFocus",
|
||||
run: ({ arg }) => void showChangesDiff(changesArg(arg)),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ const shared = [
|
||||
["code.quickDocumentation", "db.quickDoc", "debug.setValue", "vcs.changelist.edit"],
|
||||
["code.reformat", "project.reformatCode", "db.reformatValue"],
|
||||
["code.optimizeImports", "project.optimizeImports"],
|
||||
["code.deleteLine", "db.duplicateSource", "project.compareWith", "vcs.showDiff", "vcs.log.changes.showDiff"],
|
||||
["code.deleteLine", "db.duplicateSource", "project.compareWith", "vcs.showDiff"],
|
||||
["code.moveStatementDown", "find.field.recentSearches", "find.popup.recentSearches"],
|
||||
["code.foldAll", "project.collapseAll"],
|
||||
["refactor.rename", "project.rename", "db.rename", "vcs.changelist.edit", "vcs.log.editMessage", "vcs.shelf.rename"],
|
||||
|
||||
@@ -303,7 +303,6 @@ export const intellijMacos: Record<string, string[]> = {
|
||||
"vcs.rollback": ["Meta+Alt+Z"],
|
||||
"vcs.addToVcs": ["Meta+Alt+A"],
|
||||
"vcs.showDiff": ["Meta+D"],
|
||||
"vcs.log.changes.showDiff": ["Meta+D"],
|
||||
"vcs.showDiffInNewWindow": [],
|
||||
"vcs.nextChange": ["Ctrl+Alt+Shift+Down"],
|
||||
"vcs.prevChange": ["Ctrl+Alt+Shift+Up"],
|
||||
|
||||
@@ -297,7 +297,6 @@ export const intellijWindows: Record<string, string[]> = {
|
||||
"vcs.rollback": ["Ctrl+Alt+Z"],
|
||||
"vcs.addToVcs": ["Ctrl+Alt+A"],
|
||||
"vcs.showDiff": ["Ctrl+D"],
|
||||
"vcs.log.changes.showDiff": ["Ctrl+D"],
|
||||
"vcs.showDiffInNewWindow": [],
|
||||
"vcs.nextChange": ["Ctrl+Alt+Shift+Down"],
|
||||
"vcs.prevChange": ["Ctrl+Alt+Shift+Up"],
|
||||
|
||||
Reference in new issue
Block a user