feat(editor): Implemented / overridden gutter markers (#106)

Adds gutter markers for type hierarchies: ↓ "Is implemented / Has subclasses / Is overridden" on types and methods with implementations, ↑ "Implements / Overrides method in X" on methods that implement or override a supertype method. One target navigates, several open the Choose Implementation popup. Settings › Editor › General › Gutter Icons gets two toggles.

Markers come from the language server: a new batched `lsp_implementations` command (up to 4 concurrent, cancellable, sharing the goto query path) for ↓, and the Super Method lookup for ↑, which now prefers the overload with matching parameters (also fixes ⌘U on overloads). Only declarations near the viewport are queried, debounced, cached per document version and cancelled when superseded. Works for any server with implementation / type hierarchy support; servers without type hierarchy show ↓ only.

Fixes #99

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
This commit is contained in:
l0ng-ai
2026-10-27 21:02:54 +08:00
committed by GitHub
parent 6de6b33449
commit 2fe6b7b87b
27 changed files with 1252 additions and 22 deletions
+54 -12
View File
@@ -29,6 +29,7 @@ pub use uri::{is_virtual_uri, path_to_uri_string, uri_target, uri_to_path};
use caps::{ServerCaps, SyncKind};
use convert::{Conv, lsp_range};
use futures::StreamExt;
use futures::future::join_all;
use ide_core::error::ErrorKind;
use ide_core::{
@@ -228,6 +229,18 @@ impl<T> Outcome<T> {
}
}
/// Implementation requests of one line-marker pass in flight at once.
const IMPLEMENTATION_CONCURRENCY: usize = 4;
fn goto_method(kind: GotoKind) -> (&'static str, &'static str, &'static str) {
match kind {
GotoKind::Definition => ("textDocument/definition", "definitionProvider", "go to definition"),
GotoKind::Declaration => ("textDocument/declaration", "declarationProvider", "go to declaration"),
GotoKind::TypeDefinition => ("textDocument/typeDefinition", "typeDefinitionProvider", "go to type definition"),
GotoKind::Implementation => ("textDocument/implementation", "implementationProvider", "go to implementation"),
}
}
impl LspManager {
pub fn new(project_root: PathBuf, languages: Arc<LanguageRegistry>, sink: SharedSink) -> Arc<Self> {
Self::with_launcher(project_root, languages, sink, Arc::new(ProcessLauncher))
@@ -925,25 +938,54 @@ impl LspManager {
}
pub async fn goto(&self, doc: DocId, pos: Pos, kind: GotoKind) -> IdeResult<Vec<Location>> {
let (method, cap, feature) = match kind {
GotoKind::Definition => ("textDocument/definition", "definitionProvider", "go to definition"),
GotoKind::Declaration => ("textDocument/declaration", "declarationProvider", "go to declaration"),
GotoKind::TypeDefinition => {
("textDocument/typeDefinition", "typeDefinitionProvider", "go to type definition")
}
GotoKind::Implementation => {
("textDocument/implementation", "implementationProvider", "go to implementation")
}
};
let (method, cap, feature) = goto_method(kind);
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
self.goto_in(&snap, &targets, method, pos, None).await
}
/// Implementations of the symbols at each of `positions` (gutter line
/// markers), one list per position in order. A position whose request
/// fails gets an empty list; when every one fails, the first error is
/// returned (an unavailable server is not "no implementations").
pub async fn implementations(
&self,
doc: DocId,
positions: &[Pos],
cancel: CancellationToken,
) -> IdeResult<Vec<Vec<Location>>> {
let (method, cap, feature) = goto_method(GotoKind::Implementation);
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
let requests = positions.iter().copied().map(|pos| {
let (snap, targets, cancel) = (&snap, &targets, &cancel);
async move { self.goto_in(snap, targets, method, pos, Some(cancel)).await }
});
let results: Vec<IdeResult<Vec<Location>>> =
futures::stream::iter(requests).buffered(IMPLEMENTATION_CONCURRENCY).collect().await;
if cancel.is_cancelled() {
return Err(IdeError::cancelled());
}
if !results.is_empty() && results.iter().all(Result::is_err) {
return Err(results.into_iter().find_map(Result::err).expect("all failed"));
}
Ok(results.into_iter().map(Result::unwrap_or_default).collect())
}
async fn goto_in(
&self,
snap: &DocSnap,
targets: &[Target],
method: &str,
pos: Pos,
cancel: Option<&CancellationToken>,
) -> IdeResult<Vec<Location>> {
let mut out = Outcome::new();
for t in &targets {
for t in targets {
match self
.call::<_, Option<lsp::GotoDefinitionResponse>>(
t,
method,
snap.pos_params(t, pos),
None,
cancel,
DEFAULT_TIMEOUT,
)
.await
+33
View File
@@ -792,6 +792,39 @@ async fn fallback_servers_only_run_when_the_preferred_one_is_missing() {
assert!(h.mgr.hover(doc, Pos::new(0, 0), Default::default()).await.is_ok());
}
#[tokio::test(flavor = "multi_thread")]
async fn implementations_per_position() {
let caps = json!({ "textDocumentSync": 1, "implementationProvider": true });
let h = harness(&[("mock", caps)]);
let (doc, path) = h.open("a.mk", "iface A {}\nimpl B {}\nimpl C {}\n");
h.wait_status("mock", ServerStatus::Ready).await;
let s = h.launcher.state("mock");
let uri = uri_of(&path);
*s.responder.lock() = Some(Arc::new(move |_, method, params| {
if method != "textDocument/implementation" {
return None;
}
// Line 0 is implemented on lines 1 and 2; nothing else is.
Some(match params["position"]["line"].as_u64() {
Some(0) => json!([
{ "uri": uri, "range": {"start":{"line":1,"character":5},"end":{"line":1,"character":6}} },
{ "uri": uri, "range": {"start":{"line":2,"character":5},"end":{"line":2,"character":6}} },
]),
_ => json!([]),
})
}));
let found = h.mgr.implementations(doc, &[Pos::new(0, 6), Pos::new(1, 5)], Default::default()).await.unwrap();
assert_eq!(found.len(), 2);
assert_eq!(found[0].iter().map(|l| l.range.start.line).collect::<Vec<_>>(), vec![1, 2]);
assert!(found[1].is_empty());
assert!(h.mgr.implementations(doc, &[], Default::default()).await.unwrap().is_empty());
let cancel = ide_core::CancellationToken::new();
cancel.cancel();
let err = h.mgr.implementations(doc, &[Pos::new(0, 6)], cancel).await.unwrap_err();
assert_eq!(err.kind, ide_core::error::ErrorKind::Cancelled);
}
#[tokio::test(flavor = "multi_thread")]
async fn navigation_rename_format_and_decorations() {
let h = harness(&[("mock", incremental_caps())]);
+19
View File
@@ -94,6 +94,25 @@ pub async fn lsp_goto(
project!(window, state).lsp.goto(doc, pos, kind).await
}
/// Implementations of the symbols at `positions`, one list per position (the
/// editor's "is implemented / overridden" gutter markers). Cancel with
/// `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn lsp_implementations(
window: Window,
state: State<'_, AppState>,
request: RequestId,
doc: DocId,
positions: Vec<Pos>,
) -> IdeResult<Vec<Vec<Location>>> {
let lsp = project!(window, state).lsp.clone();
let cancel = state.begin_request(request);
let result = lsp.implementations(doc, &positions, cancel).await;
state.end_request(request);
result
}
/// Contents of a virtual document (library class, decompiled code) that a
/// language server returned a non-`file:` URI for. Cached per project.
#[tauri::command]
+1
View File
@@ -144,6 +144,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
lsp::lsp_hover,
lsp::lsp_signature_help,
lsp::lsp_goto,
lsp::lsp_implementations,
lsp::lsp_virtual_document,
lsp::lsp_references,
lsp::lsp_document_highlights,
+6
View File
@@ -287,6 +287,12 @@ export const commands = {
activeParameter: number,
} | null, IdeError>(__TAURI_INVOKE("lsp_signature_help", { doc, pos })),
lspGoto: (doc: DocId, pos: Pos, kind: GotoKind) => typedError<Location[], IdeError>(__TAURI_INVOKE("lsp_goto", { doc, pos, kind })),
/**
* Implementations of the symbols at `positions`, one list per position (the
* editor's "is implemented / overridden" gutter markers). Cancel with
* `cancel_request(request)`.
*/
lspImplementations: (request: RequestId, doc: DocId, positions: Pos[]) => typedError<Location[][], IdeError>(__TAURI_INVOKE("lsp_implementations", { request, doc, positions })),
/**
* Contents of a virtual document (library class, decompiled code) that a
* language server returned a non-`file:` URI for. Cached per project.
+2
View File
@@ -402,6 +402,8 @@ const noSurround = EditorView.inputHandler.of((view, from, to, text) => {
export const GUTTER_ICON_GROUPS = [
{ id: "run", gutterClass: "cm-openidea-gutter-run" },
{ id: "bookmarks", gutterClass: "cm-openidea-bookmarks" },
{ id: "inheritors", gutterClass: "cm-openidea-gutter-lineMarkers" },
{ id: "supers", gutterClass: "cm-openidea-gutter-lineMarkers" },
] as const;
export interface PrefsOptions {
+3
View File
@@ -57,6 +57,9 @@ export const openideaTheme = EditorView.theme({
boxShadow: "inset 0 1px 0 var(--border-method-separator, var(--border))",
},
"&.cm-openidea-hideGutter-run .cm-openidea-gutter-run, &.cm-openidea-hideGutter-bookmarks .cm-openidea-bookmarks": { display: "none" },
// Inheritance markers share one gutter: each group hides its own icons.
"&.cm-openidea-hideGutter-inheritors :is(.cm-openidea-lineMarker-implemented, .cm-openidea-lineMarker-overridden), &.cm-openidea-hideGutter-supers :is(.cm-openidea-lineMarker-implementing, .cm-openidea-lineMarker-overriding)":
{ display: "none" },
// Distraction Free Mode: no gutter; the text column (n chars + line padding) is centred.
"&.cm-openidea-distractionFree": { "--openidea-df-width": "calc(var(--openidea-df-columns, 80) * 1ch + 22px)" },
"&.cm-openidea-distractionFree .cm-gutters": { display: "none" },
+3 -1
View File
@@ -31,7 +31,7 @@ import { spellingExtension } from "./spelling";
import { DocumentationWindow, LspPopups, UsagesStatus } from "./Popups";
import { DOCUMENTATION_WINDOW } from "./docs";
import { prefetchMarkdown } from "./markdown";
import { registerNavigationActions } from "@/editor/navigation";
import { implementationGutter, registerNavigationActions } from "@/editor/navigation";
export function lspExtensions(ctx: EditorDocContext): Extension[] {
return [
@@ -61,6 +61,8 @@ export function activate(): () => void {
installDiagnosticsEvents(),
installAvailabilityEvents(),
registerEditorContribution({ id: "lsp", order: 50, extensions: lspExtensions }),
// After the run icons (order 55), as their own gutter column.
registerEditorContribution({ id: "lsp.lineMarkers", order: 56, extensions: implementationGutter }),
// After the templates (order 90): live template / postfix expansion keeps ⇥ first.
registerEditorContribution({ id: "lsp.parameterTab", order: 95, extensions: () => parameterTabKeys }),
registerOverlay({ id: "editor-lsp.popups", component: LspPopups }),
+1 -1
View File
@@ -121,7 +121,7 @@ export function chooserTitle(kind: GotoKind, name: string | null, count: number,
}
/** Pin button of the targets chooser: the targets as a Find tool window tab (code-insight). */
function pinToFind(symbol: string, locations: readonly Location[]) {
export function pinToFind(symbol: string, locations: readonly Location[]) {
return {
label: "Open in Find Tool Window",
run: () => void executeAction("find.showUsageResults", { symbol, locations }),
@@ -0,0 +1,19 @@
/* Editor gutter inheritance icons (implemented / overridden / implementing / overriding). */
.gutter :global(.cm-gutterElement) {
display: flex;
align-items: center;
justify-content: center;
gap: 1px;
}
.marker {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-small);
cursor: pointer;
}
.marker:hover {
background: var(--bg-hover);
}
@@ -0,0 +1,392 @@
/**
* Gutter icons for inheritance: "is implemented / overridden" (↓) on types and
* methods with implementations or overriders, "implements / overrides" (↑) on
* methods with a super method. Clicking one navigates to its single target or
* opens the targets chooser.
*
* Generic over language servers: ↓ comes from implementation requests (one
* cancellable batch per pass), ↑ from the type hierarchy and the supertypes'
* document symbols (as Super Method, ⌘U). Only declarations in and near the
* viewport are asked, in one debounced pass after edits / scrolling; results
* are cached per document text, and markers follow their lines in between.
*/
import { RangeSet, StateEffect, StateField, type Extension, type Text } from "@codemirror/state";
import { EditorView, GutterMarker, ViewPlugin, gutter, type ViewUpdate } from "@codemirror/view";
import type { EditorDocContext } from "@/editor/api";
import { docOf, debounce, isUnavailable, lspRequest } from "@/editor/lsp/lsp";
import { serverBackoff } from "@/editor/lsp/availability";
import { readManyFileLines, withDocument } from "@/editor/lsp/files";
import { locationItems, navigateTo, pinToFind } from "@/editor/lsp/navigation";
import { openPopup } from "@/editor/lsp/popupStore";
import {
commands,
isCancelled,
nextRequestId,
onIdeEvent,
type HierarchyItem,
type Location,
type OutlineItem,
type Pos,
type RequestId,
} from "@/platform/ipc";
import { editorPrefs, type EditorExtras } from "@/platform/editorSettings";
import { useProject } from "@/platform/project";
import { iconMarkup } from "@/ui";
import {
MARKER_ICONS,
candidateKey,
inheritorsMarker,
markerCandidates,
supersMarker,
withoutSelf,
type LineMarker,
type MarkerCandidate,
type MarkerGroup,
} from "./lineMarkers";
import { superMethodTargets, type SuperDeps, type SuperMethod } from "./structureNav";
import styles from "./LineMarkers.module.css";
/** Lines above / below the viewport that are covered too, so scrolling feels instant. */
const MARGIN_LINES = 60;
const MAX_PER_PASS = 60;
const DEBOUNCE_MS = 500;
/** Settings › Editor › General › Gutter Icons groups that are on. */
export function enabledGroups(prefs: Pick<EditorExtras, "showGutterIcons" | "gutterIconsDisabled">): Set<MarkerGroup> {
if (!prefs.showGutterIcons) return new Set();
return new Set((["inheritors", "supers"] as const).filter((g) => !prefs.gutterIconsDisabled.includes(g)));
}
export interface MarkerDeps {
/** Implementations of the symbols at `positions`, one list each (rejects when the server cannot answer). */
implementations: (positions: Pos[]) => Promise<Location[][]>;
supers: SuperDeps;
readFiles: (paths: readonly string[]) => Promise<Map<string, string[] | null>>;
}
interface CacheEntry {
inheritors?: LineMarker | null;
supers?: LineMarker | null;
}
/** Results for one document text; `unsupported` remembers a server that cannot answer a group. */
export interface MarkerCache {
entries: Map<string, CacheEntry>;
unsupported: Set<MarkerGroup>;
}
export const emptyCache = (): MarkerCache => ({ entries: new Map(), unsupported: new Set() });
/**
* Asks the server about the `candidates` not in `cache` yet (only the enabled
* `groups`) and stores the answers. Cancellation propagates; an unavailable
* server marks its group unsupported for this cache.
*/
export async function fillMarkers(
path: string,
candidates: readonly MarkerCandidate[],
groups: ReadonlySet<MarkerGroup>,
deps: MarkerDeps,
cache: MarkerCache,
): Promise<void> {
const entry = (c: MarkerCandidate) => {
const key = candidateKey(c);
let e = cache.entries.get(key);
if (!e) cache.entries.set(key, (e = {}));
return e;
};
const down =
groups.has("inheritors") && !cache.unsupported.has("inheritors")
? candidates.filter((c) => c.inheritable && cache.entries.get(candidateKey(c))?.inheritors === undefined)
: [];
const up =
groups.has("supers") && !cache.unsupported.has("supers")
? candidates.filter((c) => c.overriding && c.owner && cache.entries.get(candidateKey(c))?.supers === undefined)
: [];
let implementations: Location[][] = [];
if (down.length) {
try {
implementations = (await deps.implementations(down.map((c) => ({ line: c.line, col: c.col })))).map((locs, i) =>
withoutSelf(path, down[i]!, locs),
);
} catch (e) {
if (isCancelled(e)) throw e;
if (isUnavailable(e)) cache.unsupported.add("inheritors");
down.length = 0;
}
}
const supers: SuperMethod[][] = [];
for (const c of up) supers.push(await superMethodTargets(path, c.owner!, c.symbol, deps.supers));
const files = await deps.readFiles([
...implementations.flat().map((l) => l.path),
...supers.flat().map((s) => s.location.path),
]);
down.forEach((c, i) => (entry(c).inheritors = inheritorsMarker(c, implementations[i] ?? [], files)));
up.forEach((c, i) => (entry(c).supers = supersMarker(c, supers[i]!, files)));
}
/** Markers known for `candidates`, in document order (↓ before ↑ on a line). */
export function cachedMarkers(
candidates: readonly MarkerCandidate[],
groups: ReadonlySet<MarkerGroup>,
cache: MarkerCache,
): LineMarker[] {
const out: LineMarker[] = [];
for (const c of candidates) {
const e = cache.entries.get(candidateKey(c));
if (!e) continue;
if (groups.has("inheritors") && e.inheritors) out.push(e.inheritors);
if (groups.has("supers") && e.supers) out.push(e.supers);
}
return out;
}
/** Opens the single target, or the chooser over several. */
export async function openMarkerTargets(marker: LineMarker, anchor: { x: number; y: number }) {
if (marker.targets.length === 1) {
await navigateTo(marker.targets[0]!);
return;
}
openPopup({
kind: "list",
anchor,
title: marker.chooserTitle,
items: await locationItems(marker.targets),
pin: pinToFind(marker.name, marker.targets),
});
}
// ---------------------------------------------------------------------------
// CodeMirror
interface PlacedMarker {
marker: LineMarker;
/** Line start offset in the current state. */
pos: number;
}
const setMarkers = StateEffect.define<PlacedMarker[]>();
const markersField = StateField.define<PlacedMarker[]>({
create: () => [],
update(value, tr) {
for (const e of tr.effects) if (e.is(setMarkers)) return e.value;
if (!tr.docChanged || !value.length) return value;
return value.map((m) => ({ ...m, pos: tr.state.doc.lineAt(tr.changes.mapPos(m.pos, -1)).from }));
},
});
function place(doc: Text, markers: readonly LineMarker[]): PlacedMarker[] {
return markers.filter((m) => m.line < doc.lines).map((marker) => ({ marker, pos: doc.line(marker.line + 1).from }));
}
const sameMarkers = (a: readonly PlacedMarker[], b: readonly PlacedMarker[]) =>
a.length === b.length &&
a.every(
(m, i) => m.pos === b[i]!.pos && m.marker.kind === b[i]!.marker.kind && m.marker.tooltip === b[i]!.marker.tooltip,
);
class InheritanceMarker extends GutterMarker {
constructor(readonly marker: LineMarker) {
super();
}
override eq(other: GutterMarker): boolean {
return (
other instanceof InheritanceMarker &&
other.marker.kind === this.marker.kind &&
other.marker.tooltip === this.marker.tooltip
);
}
override toDOM(): Node {
const el = document.createElement("div");
el.className = `${styles.marker} cm-openidea-lineMarker-${this.marker.kind}`;
el.dataset.kind = this.marker.kind;
el.title = this.marker.tooltip;
el.setAttribute("role", "button");
el.setAttribute("aria-label", this.marker.tooltip);
el.innerHTML = iconMarkup(MARKER_ICONS[this.marker.kind], 14) ?? "";
return el;
}
}
const gutterMarkers = new WeakMap<readonly PlacedMarker[], RangeSet<GutterMarker>>();
function markerSet(placed: readonly PlacedMarker[]): RangeSet<GutterMarker> {
let set = gutterMarkers.get(placed);
if (!set) {
set = RangeSet.of(
placed.map((m) => new InheritanceMarker(m.marker).range(m.pos)),
true,
);
gutterMarkers.set(placed, set);
}
return set;
}
class MarkersPlugin {
private refresh = debounce(() => void this.run(), DEBOUNCE_MS);
private cache = emptyCache();
private cacheDoc: Text | null = null;
private outline: { doc: Text; items: OutlineItem[]; candidates: MarkerCandidate[] } | null = null;
private generation = 0;
private request: RequestId | null = null;
private offSettings: () => void;
private offServers: () => void;
constructor(private view: EditorView) {
this.refresh();
this.offSettings = useProject.subscribe((s, prev) => {
if (s.settings !== prev.settings) this.refresh();
});
// Answers given while a server was starting or indexing may be incomplete.
this.offServers = onIdeEvent("languageServer", (e) => {
if (e.status !== "ready") return;
this.invalidate();
this.refresh();
});
}
update(u: ViewUpdate) {
if (u.docChanged || u.viewportChanged) this.refresh();
// Other files may have changed meanwhile (a new implementation).
else if (u.focusChanged && u.view.hasFocus) {
this.invalidate();
this.refresh();
}
}
private invalidate() {
this.cacheDoc = null;
this.outline = null;
}
private cancelRequest() {
if (this.request === null) return;
void commands.cancelRequest(this.request).catch(() => {});
this.request = null;
}
private show(placed: PlacedMarker[]) {
if (!sameMarkers(this.view.state.field(markersField), placed))
this.view.dispatch({ effects: setMarkers.of(placed) });
}
private async run() {
const view = this.view;
if (!view.dom.isConnected) return;
const ctx = docOf(view);
if (!ctx) return;
const gen = ++this.generation;
this.cancelRequest();
const groups = enabledGroups(editorPrefs());
if (!groups.size || serverBackoff.active(ctx.doc)) {
if (!groups.size) this.show([]);
return;
}
const text = view.state.doc;
const stale = () => gen !== this.generation || view.state.doc !== text;
if (this.cacheDoc !== text) {
this.cache = emptyCache();
this.cacheDoc = text;
}
try {
if (this.outline?.doc !== text) {
const items = await lspRequest(() => commands.docOutline(ctx.doc));
if (stale()) return;
this.outline = {
doc: text,
items,
candidates: markerCandidates(items, (l) => (l < text.lines ? text.line(l + 1).text : "")),
};
}
const { candidates, items } = this.outline;
const { from, to } = view.viewport;
const first = text.lineAt(from).number - 1 - MARGIN_LINES;
const last = text.lineAt(to).number - 1 + MARGIN_LINES;
const visible = candidates.filter((c) => c.line >= first && c.line <= last).slice(0, MAX_PER_PASS);
const request = (this.request = nextRequestId());
await fillMarkers(ctx.path, visible, groups, markerDeps(ctx, items, request, this.cache), this.cache);
if (this.request === request) this.request = null;
if (stale()) return;
this.show(place(text, cachedMarkers(candidates, groups, this.cache)));
} catch (e) {
if (isUnavailable(e)) serverBackoff.mark(ctx.doc);
else if (!isCancelled(e)) console.debug("[lineMarkers]", e);
}
}
destroy() {
this.refresh.cancel();
this.cancelRequest();
this.offSettings();
this.offServers();
this.generation++;
}
}
/** LSP access for one pass: this document's outline as known, others' opened on demand, each asked once. */
function markerDeps(ctx: EditorDocContext, items: OutlineItem[], request: RequestId, cache: MarkerCache): MarkerDeps {
const supertypes = new Map<string, Promise<HierarchyItem[]>>();
const outlines = new Map<string, Promise<OutlineItem[]>>([[ctx.path, Promise.resolve(items)]]);
const hierarchy = (doc: number, pos: Pos) => lspRequest(() => commands.lspTypeHierarchy(doc, pos, "incoming"));
return {
implementations: (positions) => lspRequest(() => commands.lspImplementations(request, ctx.doc, positions)),
supers: {
supertypes: (path, pos) => {
const key = `${path}:${pos.line}:${pos.col}`;
let p = supertypes.get(key);
if (!p) {
p =
path === ctx.path
? hierarchy(ctx.doc, pos).catch((e) => {
// No type hierarchy for this file: no ↑ markers until the server changes.
if (isUnavailable(e)) cache.unsupported.add("supers");
throw e;
})
: withDocument(path, (doc) => hierarchy(doc, pos));
supertypes.set(key, p);
}
return p;
},
outline: (path) => {
let p = outlines.get(path);
if (!p) outlines.set(path, (p = withDocument(path, (doc) => lspRequest(() => commands.docOutline(doc)))));
return p;
},
},
readFiles: readManyFileLines,
};
}
function markerAt(view: EditorView, lineFrom: number, el: Element | null): LineMarker | null {
const onLine = view.state.field(markersField).filter((m) => m.pos === lineFrom);
const kind = (el?.closest(`.${styles.marker}`) as HTMLElement | null)?.dataset.kind;
return (onLine.find((m) => m.marker.kind === kind) ?? (onLine.length === 1 ? onLine[0] : null))?.marker ?? null;
}
export function implementationGutter(ctx: EditorDocContext): Extension {
if (ctx.doc < 0) return [];
return [
markersField,
ViewPlugin.fromClass(MarkersPlugin),
gutter({
class: `${styles.gutter} cm-openidea-gutter-lineMarkers`,
markers: (view) => markerSet(view.state.field(markersField)),
domEventHandlers: {
mousedown: (view, block, event) => {
const e = event as MouseEvent;
if (e.button !== 0) return false;
const target = e.target instanceof Element ? e.target : null;
const marker = markerAt(view, block.from, target);
if (!marker) return false;
e.preventDefault();
const rect = (target?.closest(`.${styles.marker}`) ?? target)?.getBoundingClientRect();
void openMarkerTargets(marker, rect ? { x: rect.left, y: rect.bottom + 2 } : { x: e.clientX, y: e.clientY });
return true;
},
},
}),
];
}
+2
View File
@@ -173,3 +173,5 @@ export function registerNavigationActions(): () => void {
editorAction("navigate.previousMethod", "Previous Method", (v) => gotoMethod(v, -1)),
]);
}
export { implementationGutter } from "./implementationGutter";
+353
View File
@@ -0,0 +1,353 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
IpcError,
type HierarchyItem,
type Location,
type OutlineItem,
type Pos,
type SymbolKind,
} from "@/platform/ipc";
const nav = vi.hoisted(() => ({ navigateTo: vi.fn(async () => {}), openPopup: vi.fn() }));
vi.mock("@/editor/lsp/navigation", async (orig) => ({
...(await orig<typeof import("@/editor/lsp/navigation")>()),
navigateTo: nav.navigateTo,
locationItems: async (locs: Location[]) => locs.map((l, i) => ({ id: `${i}`, label: l.path, run: () => {} })),
}));
vi.mock("@/editor/lsp/popupStore", async (orig) => ({
...(await orig<typeof import("@/editor/lsp/popupStore")>()),
openPopup: nav.openPopup,
}));
import { candidateKey, markerCandidates, type MarkerCandidate } from "./lineMarkers";
import {
cachedMarkers,
emptyCache,
enabledGroups,
fillMarkers,
openMarkerTargets,
type MarkerDeps,
} from "./implementationGutter";
const ALL = enabledGroups({ showGutterIcons: true, gutterIconsDisabled: [] });
function item(
name: string,
kind: SymbolKind,
line: number,
end: number,
children: OutlineItem[] = [],
col = 4,
): OutlineItem {
return {
name,
kind,
detail: null,
range: { start: { line, col: 0 }, end: { line: end, col: 1 } },
selection_range: { start: { line, col }, end: { line, col: col + name.length } },
children,
};
}
const loc = (path: string, line: number, col = 4): Location => ({
path,
range: { start: { line, col }, end: { line, col: col + 3 } },
});
const hier = (name: string, kind: SymbolKind, path: string, line = 0): HierarchyItem => ({
name,
kind,
detail: null,
path,
range: { start: { line, col: 0 }, end: { line: line + 20, col: 1 } },
selectionRange: { start: { line, col: 4 }, end: { line, col: 4 + name.length } },
fromRanges: [],
});
// Shape.java, as jdtls outlines it.
const SHAPE = [
"public interface Shape {",
" double area();",
' default String label() { return ""; }',
" static Shape unit() { return null; }",
"}",
];
const SHAPE_OUTLINE = [
item(
"Shape",
"interface",
0,
4,
[
item("area()", "method", 1, 1, [], 11),
item("label()", "method", 2, 2, [], 19),
item("unit()", "method", 3, 3, [], 17),
],
17,
),
];
const SHAPE_PATH = "/p/Shape.java";
// Circle.java: implements Shape, overrides Base.label().
const CIRCLE = [
"public class Circle extends Base implements Shape {",
" @Override",
" public double area() { return 1; }",
" private void helper() {}",
' public final String label() { return "c"; }',
"}",
];
const CIRCLE_OUTLINE = [
item(
"Circle",
"class",
0,
5,
[item("area()", "method", 1, 2), item("helper()", "method", 3, 3, [], 17), item("label()", "method", 4, 4, [], 24)],
13,
),
];
// Make `area()`'s identifier sit on line 2 (its range starts at the annotation).
CIRCLE_OUTLINE[0]!.children[0]!.selection_range = { start: { line: 2, col: 18 }, end: { line: 2, col: 22 } };
const BASE = [
"public abstract class Base {",
" public abstract void run();",
' public String label() { return ""; }',
"}",
];
const BASE_OUTLINE = [
item("Base", "class", 0, 3, [item("run()", "method", 1, 1, [], 25), item("label()", "method", 2, 2, [], 18)], 22),
];
const FILES: Record<string, string[]> = {
[SHAPE_PATH]: SHAPE,
"/p/Circle.java": CIRCLE,
"/p/Square.java": ["public class Square implements Shape {", " public double area() { return 2; }", "}"],
"/p/Base.java": BASE,
};
const lines = (text: string[]) => (l: number) => text[l] ?? "";
describe("marker candidates", () => {
it("covers inheritable types and their methods, without private / static / final ones", () => {
const shape = markerCandidates(SHAPE_OUTLINE, lines(SHAPE));
expect(shape.map((c) => [c.name, c.abstract, c.inheritable, c.overriding])).toEqual([
["Shape", true, true, false],
["area", true, true, true],
// A default method has a body: overriding it is not implementing it.
["label", false, true, true],
["unit", true, false, false],
]);
const circle = markerCandidates(CIRCLE_OUTLINE, lines(CIRCLE));
expect(circle.map((c) => [c.name, c.line, c.inheritable, c.overriding])).toEqual([
["Circle", 0, true, false],
["area", 2, true, true],
["helper", 3, false, false],
["label", 4, false, true],
]);
expect(markerCandidates(BASE_OUTLINE, lines(BASE)).map((c) => [c.name, c.abstract])).toEqual([
["Base", true],
["run", true],
["label", false],
]);
});
it("skips structs, enums and free functions for implementation requests", () => {
const outline = [item("Point", "struct", 0, 3, [item("len", "method", 1, 1)]), item("main", "function", 5, 6)];
const found = markerCandidates(outline, () => "");
expect(found.map((c) => [c.name, c.inheritable, c.overriding])).toEqual([["len", false, true]]);
});
});
/** A server answering implementation and type hierarchy requests over the files above. */
function server(impls: Record<string, Location[]>, supers: Record<string, HierarchyItem[]>) {
const calls = { implementations: [] as Pos[][], supertypes: 0 };
const outlines: Record<string, OutlineItem[]> = {
[SHAPE_PATH]: SHAPE_OUTLINE,
"/p/Circle.java": CIRCLE_OUTLINE,
"/p/Base.java": BASE_OUTLINE,
};
const deps = (path: string): MarkerDeps => ({
implementations: async (positions) => {
calls.implementations.push(positions);
return positions.map((p) => impls[`${path}:${p.line}`] ?? []);
},
supers: {
supertypes: async (p) => {
calls.supertypes++;
return supers[p] ?? [];
},
outline: async (p) => outlines[p] ?? [],
},
readFiles: async (paths) => new Map(paths.map((p) => [p, FILES[p] ?? null])),
});
return { calls, deps };
}
describe("markers from server answers", () => {
const shapeImpls = {
// The server lists the declaration itself too.
[`${SHAPE_PATH}:0`]: [loc(SHAPE_PATH, 0), loc("/p/Circle.java", 0, 13), loc("/p/Square.java", 0, 13)],
[`${SHAPE_PATH}:1`]: [loc("/p/Circle.java", 2, 18), loc("/p/Square.java", 1, 18)],
};
it("marks implemented interfaces and methods with every implementation as targets", async () => {
const { deps, calls } = server(shapeImpls, {});
const candidates = markerCandidates(SHAPE_OUTLINE, lines(SHAPE));
const cache = emptyCache();
await fillMarkers(SHAPE_PATH, candidates, ALL, deps(SHAPE_PATH), cache);
// One batch for every inheritable declaration.
expect(calls.implementations).toEqual([
[
{ line: 0, col: 17 },
{ line: 1, col: 11 },
{ line: 2, col: 19 },
],
]);
const markers = cachedMarkers(candidates, ALL, cache);
expect(markers.map((m) => [m.line, m.kind, m.targets.length, m.tooltip])).toEqual([
[0, "implemented", 2, "Is implemented by Circle, Square"],
[1, "implemented", 2, "Is implemented in Circle, Square"],
]);
expect(markers[0]!.chooserTitle).toBe("Choose Implementation of Shape (2 found)");
});
it("marks subclassed classes and overridden methods", async () => {
const { deps } = server(
{
"/p/Base.java:0": [loc("/p/Circle.java", 0, 13)],
"/p/Base.java:2": [loc("/p/Circle.java", 4, 25)],
},
{},
);
const candidates = markerCandidates(BASE_OUTLINE, lines(BASE));
const cache = emptyCache();
await fillMarkers("/p/Base.java", candidates, ALL, deps("/p/Base.java"), cache);
const markers = cachedMarkers(candidates, ALL, cache);
// `abstract class Base` is implemented, `label()` overridden.
expect(markers.map((m) => [m.line, m.kind, m.tooltip])).toEqual([
[0, "implemented", "Is implemented by Circle"],
[2, "overridden", "Is overridden in Circle"],
]);
});
it("marks implementing and overriding methods from the type hierarchy", async () => {
const supers = {
"/p/Circle.java": [hier("Base", "class", "/p/Base.java"), hier("Shape", "interface", SHAPE_PATH)],
};
const { deps, calls } = server({}, supers);
const candidates = markerCandidates(CIRCLE_OUTLINE, lines(CIRCLE));
const cache = emptyCache();
await fillMarkers("/p/Circle.java", candidates, ALL, deps("/p/Circle.java"), cache);
const markers = cachedMarkers(candidates, ALL, cache);
expect(
markers.map((m) => [m.line, m.kind, m.tooltip, m.targets.map((t) => `${t.path}:${t.range.start.line}`)]),
).toEqual([
[2, "implementing", "Implements method in Shape", [`${SHAPE_PATH}:1`]],
// Base.label() and the default Shape.label() have bodies.
[4, "overriding", "Overrides method in Base, Shape", ["/p/Base.java:2", `${SHAPE_PATH}:2`]],
]);
expect(markers[1]!.chooserTitle).toBe("Choose Super Method of label (2 found)");
expect(calls.supertypes).toBeGreaterThan(0);
});
it("asks once per document version and only for enabled groups", async () => {
const { deps, calls } = server(shapeImpls, {});
const candidates = markerCandidates(SHAPE_OUTLINE, lines(SHAPE));
const cache = emptyCache();
await fillMarkers(SHAPE_PATH, candidates, ALL, deps(SHAPE_PATH), cache);
await fillMarkers(SHAPE_PATH, candidates, ALL, deps(SHAPE_PATH), cache);
expect(calls.implementations).toHaveLength(1);
// A new document version starts from an empty cache.
await fillMarkers(SHAPE_PATH, candidates, ALL, deps(SHAPE_PATH), emptyCache());
expect(calls.implementations).toHaveLength(2);
const supersOnly = enabledGroups({ showGutterIcons: true, gutterIconsDisabled: ["inheritors"] });
const fresh = emptyCache();
await fillMarkers(SHAPE_PATH, candidates, supersOnly, deps(SHAPE_PATH), fresh);
expect(calls.implementations).toHaveLength(2);
expect(cachedMarkers(candidates, ALL, cache)).toHaveLength(2);
expect(cachedMarkers(candidates, supersOnly, cache)).toHaveLength(0);
});
it("stops asking a server that cannot answer, and caches nothing when cancelled", async () => {
const candidates = markerCandidates(SHAPE_OUTLINE, lines(SHAPE));
const failing = (kind: "unavailable" | "cancelled") => {
const calls = { n: 0 };
const deps: MarkerDeps = {
implementations: async () => {
calls.n++;
throw new IpcError({ kind, message: kind });
},
supers: { supertypes: async () => [], outline: async () => [] },
readFiles: async () => new Map(),
};
return { calls, deps };
};
const off = failing("unavailable");
const cache = emptyCache();
await fillMarkers(SHAPE_PATH, candidates, ALL, off.deps, cache);
await fillMarkers(SHAPE_PATH, candidates, ALL, off.deps, cache);
expect(off.calls.n).toBe(1);
expect(cache.unsupported.has("inheritors")).toBe(true);
const cancelled = failing("cancelled");
const kept = emptyCache();
await expect(fillMarkers(SHAPE_PATH, candidates, ALL, cancelled.deps, kept)).rejects.toThrow();
expect(kept.entries.size).toBe(0);
});
});
describe("settings", () => {
it("follows Settings › Gutter Icons", () => {
expect([...enabledGroups({ showGutterIcons: true, gutterIconsDisabled: [] })]).toEqual(["inheritors", "supers"]);
expect([...enabledGroups({ showGutterIcons: true, gutterIconsDisabled: ["supers", "run"] })]).toEqual([
"inheritors",
]);
expect(enabledGroups({ showGutterIcons: false, gutterIconsDisabled: [] }).size).toBe(0);
});
});
describe("clicking a marker", () => {
beforeEach(() => {
nav.navigateTo.mockClear();
nav.openPopup.mockClear();
});
const candidate = (): MarkerCandidate => markerCandidates(SHAPE_OUTLINE, lines(SHAPE))[0]!;
it("navigates to a single target", async () => {
const target = loc("/p/Circle.java", 0, 13);
await openMarkerTargets(
{ kind: "implemented", line: 0, name: "Shape", targets: [target], tooltip: "", chooserTitle: "" },
{ x: 1, y: 2 },
);
expect(nav.navigateTo).toHaveBeenCalledWith(target);
expect(nav.openPopup).not.toHaveBeenCalled();
});
it("opens the chooser over several targets", async () => {
const targets = [loc("/p/Circle.java", 0, 13), loc("/p/Square.java", 0, 13)];
expect(candidateKey(candidate())).toBe("0:17:Shape");
await openMarkerTargets(
{
kind: "implemented",
line: 0,
name: "Shape",
targets,
tooltip: "",
chooserTitle: "Choose Implementation of Shape (2 found)",
},
{ x: 1, y: 2 },
);
expect(nav.navigateTo).not.toHaveBeenCalled();
expect(nav.openPopup).toHaveBeenCalledWith(
expect.objectContaining({
kind: "list",
anchor: { x: 1, y: 2 },
title: "Choose Implementation of Shape (2 found)",
}),
);
expect(nav.openPopup.mock.calls[0]![0].items).toHaveLength(2);
});
});
+222
View File
@@ -0,0 +1,222 @@
/**
* Inheritance line markers: which declarations of a document get an
* "is implemented / overridden" (↓) or "implements / overrides" (↑) gutter
* icon, from the language server's answers (implementation requests and the
* type hierarchy). Pure; the gutter itself is `implementationGutter`.
*/
import type { Location, OutlineItem, SymbolKind } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { identifierAt } from "@/editor/lsp/files";
import { baseName, type SuperMethod } from "./structureNav";
export type MarkerKind = "implemented" | "overridden" | "implementing" | "overriding";
/** Settings › Gutter Icons groups: ↓ markers and ↑ markers. */
export type MarkerGroup = "inheritors" | "supers";
export const markerGroup = (kind: MarkerKind): MarkerGroup =>
kind === "implemented" || kind === "overridden" ? "inheritors" : "supers";
export const MARKER_ICONS: Record<MarkerKind, string> = {
implemented: "gutter-implemented",
overridden: "gutter-overridden",
implementing: "gutter-implementing",
overriding: "gutter-overriding",
};
export interface MarkerCandidate {
/** Outline name (`eat(String)` with jdtls), matched against supertypes' members. */
symbol: string;
/** Bare name for titles. */
name: string;
/** 0-based line / UTF-16 col of the identifier. */
line: number;
col: number;
method: boolean;
/** Declared abstract: an interface / trait, a member of one, or marked `abstract`. */
abstract: boolean;
/** Can have implementations / overriders (asked with an implementation request). */
inheritable: boolean;
/** Can implement / override a supertype's method (looked up in the type hierarchy). */
overriding: boolean;
/** Identifier position of the enclosing type (methods). */
owner: { line: number; col: number } | null;
}
export interface LineMarker {
kind: MarkerKind;
line: number;
/** Name of the declaration the marker is on. */
name: string;
targets: Location[];
tooltip: string;
/** Title of the targets chooser. */
chooserTitle: string;
}
/** Types whose subtypes / implementations are worth a marker (not structs, enums: they cannot be extended). */
const INHERITABLE_TYPES = new Set<SymbolKind>(["class", "interface", "trait"]);
const ABSTRACT_TYPES = new Set<SymbolKind>(["interface", "trait"]);
/** Types whose methods may implement or override a supertype's. */
const MEMBER_OWNERS = new Set<SymbolKind>(["class", "interface", "trait", "enum", "struct"]);
/** Modifiers that rule out implementations / overriders. */
const NOT_INHERITABLE = /\b(private|static|final)\b/;
const NOT_OVERRIDING = /\b(private|static)\b/;
const ABSTRACT = /\babstract\b/;
/** An interface method with a body (Java `default`). */
const DEFAULT = /\bdefault\b/;
/** The declaration's text before its identifier (annotations and modifiers included). */
function declarationPrefix(item: OutlineItem, lineText: (line: number) => string): string {
const { start } = item.range;
const at = item.selection_range.start;
if (start.line > at.line) return lineText(at.line).slice(0, at.col);
const lines: string[] = [];
for (let l = start.line; l <= at.line && lines.length < 12; l++) {
const text = lineText(l);
lines.push(
l === at.line
? text.slice(l === start.line ? start.col : 0, at.col)
: l === start.line
? text.slice(start.col)
: text,
);
}
// Annotation arguments (`@Foo(final = …)`) are not modifiers.
return lines.join(" ").replace(/@[\w.]+(\([^)]*\))?/g, " ");
}
/** Declarations that may get a marker, in document order. */
export function markerCandidates(
outline: readonly OutlineItem[],
lineText: (line: number) => string,
): MarkerCandidate[] {
const out: MarkerCandidate[] = [];
const walk = (items: readonly OutlineItem[], parent: OutlineItem | null) => {
for (const it of items) {
const prefix = () => declarationPrefix(it, lineText);
const at = it.selection_range.start;
if (INHERITABLE_TYPES.has(it.kind)) {
const p = prefix();
out.push({
symbol: it.name,
name: baseName(it.name),
line: at.line,
col: at.col,
method: false,
abstract: ABSTRACT_TYPES.has(it.kind) || ABSTRACT.test(p),
inheritable: !NOT_INHERITABLE.test(p),
overriding: false,
owner: null,
});
} else if (it.kind === "method" && parent && MEMBER_OWNERS.has(parent.kind)) {
const p = prefix();
const inParent = ABSTRACT_TYPES.has(parent.kind);
out.push({
symbol: it.name,
name: baseName(it.name),
line: at.line,
col: at.col,
method: true,
abstract: (inParent && !DEFAULT.test(p)) || ABSTRACT.test(p),
inheritable: INHERITABLE_TYPES.has(parent.kind) && !NOT_INHERITABLE.test(p),
overriding: !NOT_OVERRIDING.test(p),
owner: parent.selection_range.start,
});
}
if (it.children.length) walk(it.children, it);
}
};
walk(outline, null);
return out.sort((a, b) => a.line - b.line || a.col - b.col);
}
export const candidateKey = (c: MarkerCandidate) => `${c.line}:${c.col}:${c.symbol}`;
/** Servers answer an implementation request on a declaration with the declaration itself too. */
export function withoutSelf(path: string, c: MarkerCandidate, targets: readonly Location[]): Location[] {
return targets.filter((t) => !(t.path === path && t.range.start.line === c.line));
}
const TYPE_DECLARATION = /\b(?:class|interface|trait|object|struct|enum|record|impl)\s+([\p{L}_$][\p{L}\p{N}_$]*)/u;
/** Name of the type a target sits in: the nearest type declaration above it, else the file name. */
export function enclosingTypeName(lines: readonly string[] | null | undefined, target: Location): string {
for (let l = target.range.start.line - 1; lines && l >= 0 && l >= target.range.start.line - 2000; l--) {
const m = TYPE_DECLARATION.exec(lines[l] ?? "");
if (m && !/^\s*(\/\/|\*|\/\*)/.test(lines[l] ?? "")) return m[1]!;
}
const file = target.path.split(/[\\/]/).pop() ?? target.path;
return file.replace(/\.[^.]+$/, "");
}
/** Display name of a target: the type itself for type markers, the type around it for methods. */
export function targetName(lines: readonly string[] | null | undefined, target: Location, method: boolean): string {
if (!method) {
const name = identifierAt(lines?.[target.range.start.line] ?? "", target.range.start.col);
if (name) return name;
}
return enclosingTypeName(lines, target);
}
const MAX_NAMES = 8;
function nameList(names: readonly string[]): string {
const unique = [...new Set(names)].sort((a, b) => a.localeCompare(b));
const shown = unique.slice(0, MAX_NAMES).join(", ");
return unique.length > MAX_NAMES ? tx`${shown} and ${unique.length - MAX_NAMES} more` : shown;
}
/** ↓ marker of `c` from its implementations (self excluded already); null without any. */
export function inheritorsMarker(
c: MarkerCandidate,
targets: readonly Location[],
files: ReadonlyMap<string, string[] | null>,
): LineMarker | null {
if (!targets.length) return null;
const kind: MarkerKind = c.abstract ? "implemented" : "overridden";
const names = nameList(targets.map((t) => targetName(files.get(t.path), t, c.method)));
const n = targets.length;
let tooltip: string;
let chooserTitle: string;
if (!c.method) {
tooltip = c.abstract ? tx`Is implemented by ${names}` : tx`Is subclassed by ${names}`;
chooserTitle = c.abstract
? tx`Choose Implementation of ${c.name} (${n} found)`
: tx`Choose Subclass of ${c.name} (${n} found)`;
} else {
tooltip = c.abstract ? tx`Is implemented in ${names}` : tx`Is overridden in ${names}`;
chooserTitle = c.abstract
? tx`Choose Implementation of ${c.name} (${n} found)`
: tx`Choose Overriding Method of ${c.name} (${n} found)`;
}
return { kind, line: c.line, name: c.name, targets: [...targets], tooltip, chooserTitle };
}
/** Whether a super method is abstract: declared in an interface / trait, or marked `abstract`. */
export function isAbstractSuper(s: SuperMethod, files: ReadonlyMap<string, string[] | null>): boolean {
const lines = files.get(s.location.path);
const prefix = lines ? declarationPrefix(s.method, (l) => lines[l] ?? "") : "";
return (ABSTRACT_TYPES.has(s.owner.kind) && !DEFAULT.test(prefix)) || ABSTRACT.test(prefix);
}
/** ↑ marker of method `c` from its super methods; null without any. */
export function supersMarker(
c: MarkerCandidate,
supers: readonly SuperMethod[],
files: ReadonlyMap<string, string[] | null>,
): LineMarker | null {
if (!supers.length) return null;
// Implementing wins when one of several super methods is abstract, as an interface method would.
const implementing = supers.some((s) => isAbstractSuper(s, files));
const owners = nameList(supers.map((s) => baseName(s.owner.name)));
return {
kind: implementing ? "implementing" : "overriding",
line: c.line,
name: c.name,
targets: supers.map((s) => s.location),
tooltip: implementing ? tx`Implements method in ${owners}` : tx`Overrides method in ${owners}`,
chooserTitle: tx`Choose Super Method of ${c.name} (${supers.length} found)`,
};
}
@@ -63,4 +63,25 @@ describe("Super Method", () => {
// jdtls names methods with their parameters.
expect(await superMethods("/Foo.java", { line: 0, col: 4 }, "a() : void", deps)).toHaveLength(1);
});
it("prefers the overload with the same signature", async () => {
const base: HierarchyItem = {
name: "Base",
kind: "class",
detail: null,
path: "/Base.java",
range: r(0, 10),
selectionRange: { start: { line: 0, col: 4 }, end: { line: 0, col: 8 } },
fromRanges: [],
};
const deps = {
supertypes: async (path: string) => (path === "/Foo.java" ? [base] : []),
outline: async () => [item("Base", "class", 0, 10, [item("eat(String)", "method", 2, 2), item("eat(int)", "method", 4, 4)])],
};
const lines = async (name: string) => (await superMethods("/Foo.java", { line: 0, col: 4 }, name, deps)).map((l) => l.range.start.line);
expect(await lines("eat(int)")).toEqual([4]);
// Without a matching signature (or without signatures at all), every overload.
expect(await lines("eat(T)")).toEqual([2, 4]);
expect(await lines("eat")).toEqual([2, 4]);
});
});
+35 -8
View File
@@ -60,8 +60,11 @@ export function memberAt(outline: readonly OutlineItem[], caret: Pos): CaretMemb
/** `beta(int)` (jdtls) and `beta` name the same method. */
export const baseName = (name: string) => name.replace(/\s*[(<].*$/, "");
/** Methods named `name` directly inside the type declared at `item` (its outline node found by position). */
export function methodsInType(outline: readonly OutlineItem[], item: HierarchyItem, name: string): OutlineItem[] {
/** `beta(int) : void` (jdtls) is the signature `beta(int)`; plain names stay as they are. */
const signature = (name: string) => name.replace(/\s*:.*$/, "");
/** The outline node of the type declared at `item` (found by position). */
export function typeOf(outline: readonly OutlineItem[], item: HierarchyItem): OutlineItem | null {
const at = item.selectionRange.start;
const find = (items: readonly OutlineItem[]): OutlineItem | null => {
for (const i of items) {
@@ -71,9 +74,20 @@ export function methodsInType(outline: readonly OutlineItem[], item: HierarchyIt
}
return null;
};
const type = find(outline);
return find(outline);
}
/**
* Methods named `name` directly inside the type declared at `item`: the
* overload with the same signature when the server names them with one
* (`eat(String)`), else every method of that name.
*/
export function methodsInType(outline: readonly OutlineItem[], item: HierarchyItem, name: string): OutlineItem[] {
const type = typeOf(outline, item);
const bare = baseName(name);
return type ? type.children.filter((c) => METHODS.has(c.kind) && baseName(c.name) === bare) : [];
const named = type ? type.children.filter((c) => METHODS.has(c.kind) && baseName(c.name) === bare) : [];
const exact = named.filter((c) => signature(c.name) === signature(name));
return exact.length ? exact : named;
}
export interface SuperDeps {
@@ -81,13 +95,20 @@ export interface SuperDeps {
outline: (path: string) => Promise<OutlineItem[]>;
}
/** Super methods of `method` (a member of the type at `typePos`), searching up to `depth` levels of supertypes. */
export async function superMethods(path: string, typePos: Pos, method: string, deps: SuperDeps, depth = 3): Promise<Location[]> {
export interface SuperMethod {
location: Location;
/** The supertype declaring it. */
owner: HierarchyItem;
method: OutlineItem;
}
/** Super methods of `method` (a member of the type at `typePos`), with their declaring types, nearest level first. */
export async function superMethodTargets(path: string, typePos: Pos, method: string, deps: SuperDeps, depth = 3): Promise<SuperMethod[]> {
let level: Array<{ path: string; pos: Pos }> = [{ path, pos: typePos }];
const seen = new Set<string>();
for (let d = 0; d < depth && level.length; d++) {
const next: Array<{ path: string; pos: Pos }> = [];
const found: Location[] = [];
const found: SuperMethod[] = [];
for (const t of level) {
const supers = await deps.supertypes(t.path, t.pos).catch(() => [] as HierarchyItem[]);
for (const s of supers) {
@@ -95,7 +116,8 @@ export async function superMethods(path: string, typePos: Pos, method: string, d
if (seen.has(key)) continue;
seen.add(key);
const methods = methodsInType(await deps.outline(s.path).catch(() => [] as OutlineItem[]), s, method);
if (methods.length) for (const m of methods) found.push({ path: s.path, range: m.selection_range });
if (methods.length)
for (const m of methods) found.push({ location: { path: s.path, range: m.selection_range }, owner: s, method: m });
else next.push({ path: s.path, pos: s.selectionRange.start });
}
}
@@ -104,3 +126,8 @@ export async function superMethods(path: string, typePos: Pos, method: string, d
}
return [];
}
/** Super methods of `method` (a member of the type at `typePos`), searching up to `depth` levels of supertypes. */
export async function superMethods(path: string, typePos: Pos, method: string, deps: SuperDeps, depth = 3): Promise<Location[]> {
return (await superMethodTargets(path, typePos, method, deps, depth)).map((s) => s.location);
}
@@ -536,6 +536,18 @@ const GUTTER = editorPage({
component: () => <GutterGroup id="run" label="Run configurations and tests" />,
},
{ t: "custom", label: "Bookmarks", component: () => <GutterGroup id="bookmarks" label="Bookmarks" /> },
{
t: "custom",
label: "Implemented and overridden",
keywords: ["implementations", "subclasses", "inheritors"],
component: () => <GutterGroup id="inheritors" label="Implemented and overridden declarations" />,
},
{
t: "custom",
label: "Implementing and overriding",
keywords: ["super method", "overrides", "implements"],
component: () => <GutterGroup id="supers" label="Implementing and overriding methods" />,
},
],
},
],
+14
View File
@@ -3708,6 +3708,20 @@
"Can't rename '{name}' without the language server: only local variables can be renamed based on syntax": "没有语言服务器时无法重命名 '{name}':基于语法只能重命名局部变量",
"Choose Super Class or Interface ({count} found)": "选择超类或接口(找到 {count} 个)",
"Choose Super Method of {name} ({count} found)": "选择 {name} 的超方法(找到 {count} 个)",
"Choose Implementation of {name} ({count} found)": "选择 {name} 的实现(找到 {count} 个)",
"Choose Subclass of {name} ({count} found)": "选择 {name} 的子类(找到 {count} 个)",
"Choose Overriding Method of {name} ({count} found)": "选择 {name} 的重写方法(找到 {count} 个)",
"Is implemented by {names}": "由 {names} 实现",
"Is subclassed by {names}": "子类:{names}",
"Is implemented in {names}": "在 {names} 中实现",
"Is overridden in {names}": "在 {names} 中重写",
"Implements method in {names}": "实现 {names} 中的方法",
"Overrides method in {names}": "重写 {names} 中的方法",
"{names} and {count} more": "{names} 等另外 {count} 个",
"Implemented and overridden": "已实现和已重写",
"Implementing and overriding": "实现和重写",
"Implemented and overridden declarations": "已被实现和重写的声明",
"Implementing and overriding methods": "实现和重写超类型方法的方法",
"Replace all {count} occurrences": "替换全部 {count} 处",
"↵ Apply · Esc Keep '{name}'": "↵ 应用 · Esc 保留 '{name}'",
" (in {files} files)": "(位于 {files} 个文件中)",
@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 11.5C7.98528 11.5 10 9.48528 10 7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5Z" fill="#F2FCF3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H4V4H7V5H6V9H7V10H4V9H5V5Z" fill="#208A3C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 6C11.7761 6 12 6.22386 12 6.5V12.2929L13.1464 11.1464C13.3417 10.9512 13.6583 10.9512 13.8536 11.1464C14.0488 11.3417 14.0488 11.6583 13.8536 11.8536L11.8536 13.8536C11.6583 14.0488 11.3417 14.0488 11.1464 13.8536L9.14645 11.8536C8.95118 11.6583 8.95118 11.3417 9.14645 11.1464C9.34171 10.9512 9.65829 10.9512 9.85355 11.1464L11 12.2929V6.5C11 6.22386 11.2239 6 11.5 6Z" fill="#6C707E"/>
<path d="M5.5 1.5C7.91689 1.5 9.96967 3.05893 10.7077 5.22611C10.2828 5.49095 10 5.96245 10 6.5V7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C6.61537 11.5 7.63597 11.0942 8.4222 10.4222L8.43934 10.4393C8.05008 10.8286 7.91949 11.3786 8.04756 11.8757C7.28593 12.2745 6.41931 12.5 5.5 12.5C2.46243 12.5 0 10.0376 0 7C0 3.96243 2.46243 1.5 5.5 1.5Z" fill="#208A3C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 11.5C7.98528 11.5 10 9.48528 10 7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5Z" fill="#253627"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H4V4H7V5H6V9H7V10H4V9H5V5Z" fill="#57965C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 6C11.7761 6 12 6.22386 12 6.5V12.2929L13.1464 11.1464C13.3417 10.9512 13.6583 10.9512 13.8536 11.1464C14.0488 11.3417 14.0488 11.6583 13.8536 11.8536L11.8536 13.8536C11.6583 14.0488 11.3417 14.0488 11.1464 13.8536L9.14645 11.8536C8.95118 11.6583 8.95118 11.3417 9.14645 11.1464C9.34171 10.9512 9.65829 10.9512 9.85355 11.1464L11 12.2929V6.5C11 6.22386 11.2239 6 11.5 6Z" fill="#CED0D6"/>
<path d="M5.5 1.5C7.91689 1.5 9.96967 3.05893 10.7077 5.22611C10.2828 5.49095 10 5.96245 10 6.5V7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C6.61537 11.5 7.63597 11.0942 8.4222 10.4222L8.43934 10.4393C8.05008 10.8286 7.91949 11.3786 8.04756 11.8757C7.28593 12.2745 6.41931 12.5 5.5 12.5C2.46243 12.5 0 10.0376 0 7C0 3.96243 2.46243 1.5 5.5 1.5Z" fill="#57965C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C7.98528 11.5 10 9.48528 10 7Z" fill="#F2FCF3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H4V4H7V5H6V9H7V10H4V9H5V5Z" fill="#208A3C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 8C11.2239 8 11 7.77614 11 7.5L11 1.70711L9.85355 2.85355C9.65829 3.04882 9.34171 3.04882 9.14645 2.85355C8.95118 2.65829 8.95118 2.34171 9.14645 2.14645L11.1464 0.146447C11.3417 -0.0488155 11.6583 -0.0488155 11.8536 0.146447L13.8536 2.14645C14.0488 2.34171 14.0488 2.65829 13.8536 2.85355C13.6583 3.04882 13.3417 3.04882 13.1464 2.85355L12 1.70711L12 7.5C12 7.77614 11.7761 8 11.5 8Z" fill="#DB3B4B"/>
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C6.61537 2.5 7.63597 2.90579 8.4222 3.5778L8.43934 3.56066C8.05008 3.1714 7.91948 2.62142 8.04756 2.12432C7.28593 1.72554 6.41931 1.5 5.5 1.5C2.46243 1.5 0 3.96243 0 7C0 10.0376 2.46243 12.5 5.5 12.5C7.91689 12.5 9.96967 10.9411 10.7077 8.7739C10.2828 8.50906 10 8.03755 10 7.5V7Z" fill="#208A3C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C7.98528 11.5 10 9.48528 10 7Z" fill="#253627"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H4V4H7V5H6V9H7V10H4V9H5V5Z" fill="#57965C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 8C11.2239 8 11 7.77614 11 7.5L11 1.70711L9.85355 2.85355C9.65829 3.04882 9.34171 3.04882 9.14645 2.85355C8.95118 2.65829 8.95118 2.34171 9.14645 2.14645L11.1464 0.146447C11.3417 -0.0488155 11.6583 -0.0488155 11.8536 0.146447L13.8536 2.14645C14.0488 2.34171 14.0488 2.65829 13.8536 2.85355C13.6583 3.04882 13.3417 3.04882 13.1464 2.85355L12 1.70711L12 7.5C12 7.77614 11.7761 8 11.5 8Z" fill="#DB5C5C"/>
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C6.61537 2.5 7.63597 2.90579 8.4222 3.5778L8.43934 3.56066C8.05008 3.1714 7.91948 2.62142 8.04756 2.12432C7.28593 1.72554 6.41931 1.5 5.5 1.5C2.46243 1.5 0 3.96243 0 7C0 10.0376 2.46243 12.5 5.5 12.5C7.91689 12.5 9.96967 10.9411 10.7077 8.7739C10.2828 8.50906 10 8.03755 10 7.5V7Z" fill="#57965C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+7
View File
@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C7.98528 2.5 10 4.51472 10 7Z" fill="#EDF3FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 6C11.7761 6 12 6.22386 12 6.5V12.2929L13.1464 11.1464C13.3417 10.9512 13.6583 10.9512 13.8536 11.1464C14.0488 11.3417 14.0488 11.6583 13.8536 11.8536L11.8536 13.8536C11.6583 14.0488 11.3417 14.0488 11.1464 13.8536L9.14645 11.8536C8.95118 11.6583 8.95118 11.3417 9.14645 11.1464C9.34171 10.9512 9.65829 10.9512 9.85355 11.1464L11 12.2929V6.5C11 6.22386 11.2239 6 11.5 6Z" fill="#6C707E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.4918 10C3.72951 10 2.5 8.64917 2.5 7.01657V7C2.5 5.3674 3.7459 4 5.5082 4C7.27049 4 8.5 5.35083 8.5 6.98343V7C8.5 8.6326 7.2541 10 5.4918 10ZM5.5082 9.05525C6.64754 9.05525 7.44262 8.14365 7.44262 7.01657V7C7.44262 5.87293 6.63115 4.94475 5.4918 4.94475C4.35246 4.94475 3.55738 5.85635 3.55738 6.98343V7C3.55738 8.12707 4.36885 9.05525 5.5082 9.05525Z" fill="#3574F0"/>
<path d="M5.5 1.5C7.91689 1.5 9.96967 3.05893 10.7077 5.22611C10.2828 5.49095 10 5.96245 10 6.5V7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C6.61537 11.5 7.63597 11.0942 8.4222 10.4222L8.43934 10.4393C8.05008 10.8286 7.91949 11.3786 8.04756 11.8757C7.28593 12.2745 6.41931 12.5 5.5 12.5C2.46243 12.5 0 10.0376 0 7C0 3.96243 2.46243 1.5 5.5 1.5Z" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C7.98528 2.5 10 4.51472 10 7Z" fill="#25324D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 6C11.7761 6 12 6.22386 12 6.5V12.2929L13.1464 11.1464C13.3417 10.9512 13.6583 10.9512 13.8536 11.1464C14.0488 11.3417 14.0488 11.6583 13.8536 11.8536L11.8536 13.8536C11.6583 14.0488 11.3417 14.0488 11.1464 13.8536L9.14645 11.8536C8.95118 11.6583 8.95118 11.3417 9.14645 11.1464C9.34171 10.9512 9.65829 10.9512 9.85355 11.1464L11 12.2929V6.5C11 6.22386 11.2239 6 11.5 6Z" fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.4918 10C3.72951 10 2.5 8.64917 2.5 7.01657V7C2.5 5.3674 3.7459 4 5.5082 4C7.27049 4 8.5 5.35083 8.5 6.98343V7C8.5 8.6326 7.2541 10 5.4918 10ZM5.5082 9.05525C6.64754 9.05525 7.44262 8.14365 7.44262 7.01657V7C7.44262 5.87293 6.63115 4.94475 5.4918 4.94475C4.35246 4.94475 3.55738 5.85635 3.55738 6.98343V7C3.55738 8.12707 4.36885 9.05525 5.5082 9.05525Z" fill="#548AF7"/>
<path d="M5.5 1.5C7.91689 1.5 9.96967 3.05893 10.7077 5.22611C10.2828 5.49095 10 5.96245 10 6.5V7C10 4.51472 7.98528 2.5 5.5 2.5C3.01472 2.5 1 4.51472 1 7C1 9.48528 3.01472 11.5 5.5 11.5C6.61537 11.5 7.63597 11.0942 8.4222 10.4222L8.43934 10.4393C8.05008 10.8286 7.91949 11.3786 8.04756 11.8757C7.28593 12.2745 6.41931 12.5 5.5 12.5C2.46243 12.5 0 10.0376 0 7C0 3.96243 2.46243 1.5 5.5 1.5Z" fill="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+7
View File
@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C7.98528 2.5 10 4.51472 10 7Z" fill="#EDF3FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.4918 10C3.72951 10 2.5 8.64917 2.5 7.01657V7C2.5 5.3674 3.7459 4 5.5082 4C7.27049 4 8.5 5.35083 8.5 6.98343V7C8.5 8.6326 7.2541 10 5.4918 10ZM5.5082 9.05525C6.64754 9.05525 7.44262 8.14365 7.44262 7.01657V7C7.44262 5.87293 6.63115 4.94475 5.4918 4.94475C4.35246 4.94475 3.55738 5.85635 3.55738 6.98343V7C3.55738 8.12707 4.36885 9.05525 5.5082 9.05525Z" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 8C11.2239 8 11 7.77614 11 7.5L11 1.70711L9.85355 2.85355C9.65829 3.04882 9.34171 3.04882 9.14645 2.85355C8.95118 2.65829 8.95118 2.34171 9.14645 2.14645L11.1464 0.146447C11.3417 -0.0488155 11.6583 -0.0488155 11.8536 0.146447L13.8536 2.14645C14.0488 2.34171 14.0488 2.65829 13.8536 2.85355C13.6583 3.04882 13.3417 3.04882 13.1464 2.85355L12 1.70711L12 7.5C12 7.77614 11.7761 8 11.5 8Z" fill="#DB3B4B"/>
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C6.61537 2.5 7.63597 2.90579 8.4222 3.5778L8.43934 3.56066C8.05008 3.1714 7.91948 2.62142 8.04756 2.12432C7.28593 1.72554 6.41931 1.5 5.5 1.5C2.46243 1.5 0 3.96243 0 7C0 10.0376 2.46243 12.5 5.5 12.5C7.91689 12.5 9.96967 10.9411 10.7077 8.7739C10.2828 8.50906 10 8.03755 10 7.5V7Z" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C7.98528 2.5 10 4.51472 10 7Z" fill="#25324D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.4918 10C3.72951 10 2.5 8.64917 2.5 7.01657V7C2.5 5.3674 3.7459 4 5.5082 4C7.27049 4 8.5 5.35083 8.5 6.98343V7C8.5 8.6326 7.2541 10 5.4918 10ZM5.5082 9.05525C6.64754 9.05525 7.44262 8.14365 7.44262 7.01657V7C7.44262 5.87293 6.63115 4.94475 5.4918 4.94475C4.35246 4.94475 3.55738 5.85635 3.55738 6.98343V7C3.55738 8.12707 4.36885 9.05525 5.5082 9.05525Z" fill="#548AF7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 8C11.2239 8 11 7.77614 11 7.5L11 1.70711L9.85355 2.85355C9.65829 3.04882 9.34171 3.04882 9.14645 2.85355C8.95118 2.65829 8.95118 2.34171 9.14645 2.14645L11.1464 0.146447C11.3417 -0.0488155 11.6583 -0.0488155 11.8536 0.146447L13.8536 2.14645C14.0488 2.34171 14.0488 2.65829 13.8536 2.85355C13.6583 3.04882 13.3417 3.04882 13.1464 2.85355L12 1.70711L12 7.5C12 7.77614 11.7761 8 11.5 8Z" fill="#DB5C5C"/>
<path d="M10 7C10 9.48528 7.98528 11.5 5.5 11.5C3.01472 11.5 1 9.48528 1 7C1 4.51472 3.01472 2.5 5.5 2.5C6.61537 2.5 7.63597 2.90579 8.4222 3.5778L8.43934 3.56066C8.05008 3.1714 7.91948 2.62142 8.04756 2.12432C7.28593 1.72554 6.41931 1.5 5.5 1.5C2.46243 1.5 0 3.96243 0 7C0 10.0376 2.46243 12.5 5.5 12.5C7.91689 12.5 9.96967 10.9411 10.7077 8.7739C10.2828 8.50906 10 8.03755 10 7.5V7Z" fill="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+4
View File
@@ -322,6 +322,10 @@ const ICONS = {
"gutter-run-all": "actions/runAll",
"gutter-run-passed": "gutter/runSuccess",
"gutter-run-failed": "gutter/runError",
"gutter-implemented": "gutter/implementedMethod",
"gutter-implementing": "gutter/implementingMethod",
"gutter-overridden": "gutter/overridenMethod",
"gutter-overriding": "gutter/overridingMethod",
stop: "run/stop",
rerun: "run/rerun",
"rerun-filled": "run/rerun_stroke",