дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-14 13:45:49 +02:00
Порівняти коміти
1
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
93cccb9bb5 |
No files matched your search
@@ -309,9 +309,13 @@ pub async fn lsp_signature_help(host: &Host, window: &str, doc: DocId, pos: Pos)
|
||||
|
||||
pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: GotoKind) -> IdeResult<Vec<Location>> {
|
||||
let target = position_document(host, window, doc, pos).await?;
|
||||
// ⌘B on a declaration shows its usages, also when a plugin's index links the name elsewhere
|
||||
// (a MyBatis mapper method to its XML statement): the server's answer comes along for the
|
||||
// editor to tell.
|
||||
if target == doc
|
||||
&& kind == GotoKind::Definition
|
||||
&& let Some(plugins) = crate::plugins::editor_plugin_host()
|
||||
&& !on_declaration_name(host, window, doc, pos).await
|
||||
{
|
||||
let project = host.project(window)?;
|
||||
// A notebook cell is a language-server document only: no plugin navigates in it.
|
||||
@@ -352,6 +356,30 @@ pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: Got
|
||||
definition_targets(result, found, in_string)
|
||||
}
|
||||
|
||||
/// Whether the syntax tree puts the identifier at `pos` as a declaration's name (`int f()`'s `f`).
|
||||
async fn on_declaration_name(host: &Host, window: &str, doc: DocId, pos: Pos) -> bool {
|
||||
let Ok(project) = host.project(window) else { return false };
|
||||
blocking(move || {
|
||||
let snap = project.documents.text_snapshot(doc)?;
|
||||
if snap.large {
|
||||
return Ok(false);
|
||||
}
|
||||
let s = ide_syntax::Snapshot::doc(doc, snap.version, &snap.language, &snap.text);
|
||||
let cancel = ide_core::CancellationToken::new();
|
||||
let Ok(Some(at)) = project.structure.node_at(&s, Range::new(pos, pos), &cancel) else { return Ok(false) };
|
||||
Ok(is_declaration_name(&at))
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_declaration_name(at: &ide_syntax::NodeAt) -> bool {
|
||||
at.node.field.as_deref() == Some("name")
|
||||
&& at.ancestors.first().is_some_and(|parent| {
|
||||
["declaration", "definition", "declarator"].iter().any(|kind| parent.kind.ends_with(kind))
|
||||
})
|
||||
}
|
||||
|
||||
async fn in_string_literal(host: &Host, window: &str, doc: DocId, pos: Pos) -> bool {
|
||||
let Ok(project) = host.project(window) else { return false };
|
||||
blocking(move || {
|
||||
@@ -2136,6 +2164,24 @@ mod tests {
|
||||
assert_eq!(paths(definition_targets(Ok(vec![helper, guess]), Vec::new(), false)), ["/p/app/helpers.php"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declaration_names_are_told_apart_from_uses() {
|
||||
let syntax = std::sync::Arc::new(ide_language::Syntax::new());
|
||||
let structure = ide_syntax::StructureService::new(std::sync::Arc::new(ide_language::SyntaxCache::new(syntax)));
|
||||
let java = ide_core::LanguageId::new("java");
|
||||
let text = "interface ShareMapper {\n int batchCancel(List<Long> ids);\n}\nclass S { int f(ShareMapper m) { return m.batchCancel(null); } }\n";
|
||||
let snap = ide_syntax::Snapshot::text(&java, text);
|
||||
let cancel = ide_core::CancellationToken::new();
|
||||
let at = |line, col| {
|
||||
let pos = Pos::new(line, col);
|
||||
is_declaration_name(&structure.node_at(&snap, Range::new(pos, pos), &cancel).unwrap().unwrap())
|
||||
};
|
||||
assert!(at(1, 10), "the mapper method's name");
|
||||
assert!(at(0, 12), "the interface's name");
|
||||
assert!(!at(3, 45), "a call of the method");
|
||||
assert!(!at(1, 4), "its return type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions_inside_string_literals_are_told_apart() {
|
||||
let syntax = std::sync::Arc::new(ide_language::Syntax::new());
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
SYNTAX_NOTE,
|
||||
declarationFallback,
|
||||
indexDefinitions,
|
||||
isDeclarationName,
|
||||
isSelfTarget,
|
||||
localHighlights,
|
||||
type DeclarationTarget,
|
||||
@@ -360,6 +361,15 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the caret is on the name of a declaration, by the syntax tree (no language server wait). */
|
||||
async function onDeclarationName(view: EditorView, offset: number): Promise<boolean> {
|
||||
const doc = docOf(view);
|
||||
if (!doc) return false;
|
||||
const pos = offsetToPos(view.state.doc, offset);
|
||||
const at = await structure.nodeAt(doc.doc, { start: pos, end: pos }).catch(() => null);
|
||||
return isDeclarationName(at);
|
||||
}
|
||||
|
||||
async function gotoResolved(
|
||||
view: EditorView,
|
||||
kind: GotoKind,
|
||||
@@ -374,7 +384,10 @@ async function gotoResolved(
|
||||
const before = view.state.doc;
|
||||
const plugin = await contributedNavigation(view, offset).catch(() => null);
|
||||
if (view.state.doc !== before || superseded()) return;
|
||||
if (plugin) {
|
||||
// ⌘B on a declaration shows its usages, also when a plugin links the name elsewhere (a
|
||||
// MyBatis mapper method to its XML statement).
|
||||
if (plugin && !(kind === "definition" && (await onDeclarationName(view, offset)))) {
|
||||
if (superseded()) return;
|
||||
if (plugin.targets.length === 1) await navigateTo(plugin.targets[0]!);
|
||||
else {
|
||||
const items = await locationItems(plugin.targets);
|
||||
|
||||
@@ -45,6 +45,7 @@ provideEditorService({ openFile } as never);
|
||||
|
||||
const fb = await import("./syntaxFallback");
|
||||
const { gotoTargets, navigationExtension, highlightUsages } = await import("./navigation");
|
||||
const { registerNavigationProvider } = await import("@/editor/navigationProviders");
|
||||
const { startRename, renameNamePart } = await import("./rename");
|
||||
const { lspDoc } = await import("./lsp");
|
||||
const { invalidateConfigurationNavigation } = await import("./configurationNavigation");
|
||||
@@ -416,6 +417,48 @@ describe("navigation wiring", () => {
|
||||
expect(openFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows usages on a declaration a plugin's index links elsewhere, and follows the link from a call", async () => {
|
||||
// MyBatis Duo indexes the mapper method's name as a link to its XML statement.
|
||||
const xml = { path: "/p/UserMapper.xml", range: r(3, 16, 24) };
|
||||
const off = registerNavigationProvider(async (view) => {
|
||||
const at = view.state.doc.toString().indexOf("findName");
|
||||
return { from: at, to: at + 8, targets: [xml] };
|
||||
});
|
||||
try {
|
||||
// The syntax tree puts the caret on the method's name; the host then answers both.
|
||||
cmd.syntaxNodeAt.mockReturnValueOnce(
|
||||
ok({
|
||||
node: node("identifier", r(3, 11, 19), "name"),
|
||||
ancestors: [node("method_declaration", { start: { line: 3, col: 4 }, end: { line: 3, col: 29 } })],
|
||||
}),
|
||||
);
|
||||
cmd.lspGoto.mockReturnValueOnce(ok([xml, { path: "/p/UserMapper.java", range: r(3, 11, 19) }]));
|
||||
const view = editor(
|
||||
"package demo;\n\npublic interface UserMapper {\n String findName(Long id);\n}\n",
|
||||
"java",
|
||||
58,
|
||||
"/p/UserMapper.java",
|
||||
);
|
||||
await gotoTargets(view, "definition");
|
||||
expect(executeAction).toHaveBeenCalledWith("find.showUsages");
|
||||
expect(openFile).not.toHaveBeenCalled();
|
||||
|
||||
// A call: the plugin's link is followed without asking the language server.
|
||||
executeAction.mockClear();
|
||||
cmd.lspGoto.mockClear();
|
||||
cmd.syntaxNodeAt.mockReturnValueOnce(
|
||||
ok({ node: node("identifier", r(1, 26, 34), "name"), ancestors: [node("method_invocation", r(1, 24, 38))] }),
|
||||
);
|
||||
const call = editor("class S {\n void f(UserMapper m) { m.findName(1L); }\n}\n", "java", 37, "/p/S.java");
|
||||
await gotoTargets(call, "definition");
|
||||
expect(openFile).toHaveBeenCalledWith("/p/UserMapper.xml", expect.objectContaining({ focus: true }));
|
||||
expect(executeAction).not.toHaveBeenCalledWith("find.showUsages");
|
||||
expect(cmd.lspGoto).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
off();
|
||||
}
|
||||
});
|
||||
|
||||
it("looks past a server answer that points at the import itself", async () => {
|
||||
cmd.lspGoto.mockReturnValueOnce(ok([{ path: "/p/a.py", range: r(0, 37, 43) }]));
|
||||
cmd.navIndexDefinitions.mockReturnValueOnce(ok([sym("logger", "variable", "/deps/torchtitan/tools/logging.py")]));
|
||||
|
||||
@@ -63,6 +63,14 @@ export function usageRole(at: NodeAt | null): UsageRole {
|
||||
return "other";
|
||||
}
|
||||
|
||||
const DECLARATION = /(declaration|definition|declarator)$/;
|
||||
|
||||
/** Whether the syntax tree puts the identifier at `at` as the name of a declaration (`int f()`'s `f`). */
|
||||
export function isDeclarationName(at: NodeAt | null): boolean {
|
||||
const parent = at?.ancestors[0];
|
||||
return !!parent && at.node.field === "name" && DECLARATION.test(parent.kind);
|
||||
}
|
||||
|
||||
/** Index symbols that may be the declaration of `name` used in `role`. */
|
||||
export function filterSymbols(matches: readonly SymbolMatch[], name: string, role: UsageRole): SymbolMatch[] {
|
||||
return matches.filter((m) => {
|
||||
|
||||
Reference in new issue
Заблокувати користувача