Skip to content

Commit 9f8fb57

Browse files
authored
Implement Source Analysis Link Provider With Command Target (Takeover Output) (#37)
1 parent fe4af7f commit 9f8fb57

File tree

5 files changed

+210
-2
lines changed

5 files changed

+210
-2
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,11 @@
878878
"command": "vscode-objectscript.ccs.followDefinitionLink",
879879
"title": "Follow Definition Link"
880880
},
881+
{
882+
"category": "ObjectScript",
883+
"command": "vscode-objectscript.ccs.followSourceAnalysisLink",
884+
"title": "Follow Source Analysis Link"
885+
},
881886
{
882887
"category": "ObjectScript",
883888
"command": "vscode-objectscript.compile",

src/ccs/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ export {
2020
DefinitionDocumentLinkProvider,
2121
followDefinitionLinkCommand,
2222
} from "./providers/DefinitionDocumentLinkProvider";
23+
export {
24+
SourceAnalysisLinkProvider,
25+
type SourceAnalysisLinkArgs,
26+
followSourceAnalysisLink,
27+
followSourceAnalysisLinkCommand,
28+
} from "./providers/SourceAnalysisLinkProvider";
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import * as vscode from "vscode";
2+
3+
import { DocumentContentProvider } from "../../providers/DocumentContentProvider";
4+
import { logDebug } from "../core/logging";
5+
6+
export const followSourceAnalysisLinkCommand = "vscode-objectscript.ccs.followSourceAnalysisLink" as const;
7+
8+
const METHOD_WITH_OFFSET_REGEX = /([%\w.]+)\(([\w%]+)\+(\d+)\)/g;
9+
const ROUTINE_OFFSET_REGEX = /([%\w.]+)\((\d+)\)/g;
10+
11+
export interface SourceAnalysisLinkArgs {
12+
targetUri: string;
13+
offset: number;
14+
methodName?: string;
15+
}
16+
17+
export class SourceAnalysisLinkProvider implements vscode.DocumentLinkProvider {
18+
public provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
19+
const links: vscode.DocumentLink[] = [];
20+
21+
for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
22+
const text = document.lineAt(lineIndex).text;
23+
24+
METHOD_WITH_OFFSET_REGEX.lastIndex = 0;
25+
for (const match of text.matchAll(METHOD_WITH_OFFSET_REGEX)) {
26+
const [fullMatch, filename, methodName, offsetString] = match;
27+
const range = new vscode.Range(
28+
new vscode.Position(lineIndex, match.index ?? 0),
29+
new vscode.Position(lineIndex, (match.index ?? 0) + fullMatch.length)
30+
);
31+
const link = this.createLink(range, filename, Number.parseInt(offsetString, 10), methodName);
32+
if (link) {
33+
links.push(link);
34+
}
35+
}
36+
37+
ROUTINE_OFFSET_REGEX.lastIndex = 0;
38+
for (const match of text.matchAll(ROUTINE_OFFSET_REGEX)) {
39+
const [fullMatch, filename, offsetString] = match;
40+
41+
// Skip matches that also match the method+offset pattern, which has already been handled above.
42+
if (/\+/.test(fullMatch)) {
43+
continue;
44+
}
45+
46+
const range = new vscode.Range(
47+
new vscode.Position(lineIndex, match.index ?? 0),
48+
new vscode.Position(lineIndex, (match.index ?? 0) + fullMatch.length)
49+
);
50+
const link = this.createLink(range, filename, Number.parseInt(offsetString, 10));
51+
if (link) {
52+
links.push(link);
53+
}
54+
}
55+
}
56+
57+
return links;
58+
}
59+
60+
private createLink(
61+
range: vscode.Range,
62+
filename: string,
63+
offset: number,
64+
methodName?: string
65+
): vscode.DocumentLink | undefined {
66+
if (!Number.isFinite(offset)) {
67+
return undefined;
68+
}
69+
70+
const normalizedFilename = lowercaseExtension(filename);
71+
const targetUri = DocumentContentProvider.getUri(normalizedFilename);
72+
if (!targetUri) {
73+
return undefined;
74+
}
75+
76+
const args: SourceAnalysisLinkArgs = {
77+
targetUri: targetUri.toString(),
78+
offset,
79+
...(methodName ? { methodName } : {}),
80+
};
81+
82+
const commandUri = vscode.Uri.parse(
83+
`command:${followSourceAnalysisLinkCommand}?${encodeURIComponent(JSON.stringify(args))}`
84+
);
85+
86+
const link = new vscode.DocumentLink(range, commandUri);
87+
link.tooltip = vscode.l10n.t("Open Source Analysis location");
88+
return link;
89+
}
90+
}
91+
92+
export async function followSourceAnalysisLink(args: SourceAnalysisLinkArgs): Promise<void> {
93+
try {
94+
if (!args?.targetUri) {
95+
logDebug("Missing targetUri for source analysis link", args);
96+
return;
97+
}
98+
99+
const uri = vscode.Uri.parse(args.targetUri);
100+
const editor = await vscode.window.showTextDocument(uri, { preview: false });
101+
const document = editor.document;
102+
103+
const targetLine = await resolveTargetLine(uri, document, args.offset, args.methodName);
104+
const line = document.lineAt(targetLine);
105+
const position = line.range.start;
106+
editor.selection = new vscode.Selection(position, position);
107+
editor.revealRange(line.range, vscode.TextEditorRevealType.InCenter);
108+
} catch (error) {
109+
logDebug("Failed to follow source analysis link", error);
110+
}
111+
}
112+
113+
async function resolveTargetLine(
114+
uri: vscode.Uri,
115+
document: vscode.TextDocument,
116+
offset: number,
117+
methodName?: string
118+
): Promise<number> {
119+
const clampedOffset = Math.max(offset, 0);
120+
121+
if (!methodName) {
122+
return clampLine(document, Math.max(clampedOffset - 1, 0));
123+
}
124+
125+
const methodStartLine = await findMethodStartLine(uri, methodName);
126+
if (typeof methodStartLine === "number") {
127+
return clampLine(document, methodStartLine + clampedOffset);
128+
}
129+
130+
return clampLine(document, Math.max(clampedOffset - 1, 0));
131+
}
132+
133+
async function findMethodStartLine(uri: vscode.Uri, methodName: string): Promise<number | undefined> {
134+
try {
135+
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
136+
"vscode.executeDocumentSymbolProvider",
137+
uri
138+
);
139+
const methodSymbol = findMethodSymbol(symbols, methodName);
140+
return methodSymbol?.range.start.line;
141+
} catch (error) {
142+
logDebug("Failed to resolve document symbols for source analysis link", error);
143+
return undefined;
144+
}
145+
}
146+
147+
function findMethodSymbol(
148+
symbols: readonly vscode.DocumentSymbol[] | undefined,
149+
methodName: string
150+
): vscode.DocumentSymbol | undefined {
151+
if (!Array.isArray(symbols)) {
152+
return undefined;
153+
}
154+
155+
for (const symbol of symbols) {
156+
if (isMethodSymbol(symbol) && symbol.name === methodName) {
157+
return symbol;
158+
}
159+
160+
const child = findMethodSymbol(symbol.children, methodName);
161+
if (child) {
162+
return child;
163+
}
164+
}
165+
166+
return undefined;
167+
}
168+
169+
function isMethodSymbol(symbol: vscode.DocumentSymbol): boolean {
170+
const detail = symbol.detail ?? "";
171+
return detail === "Method" || detail === "ClassMethod";
172+
}
173+
174+
function clampLine(document: vscode.TextDocument, line: number): number {
175+
if (document.lineCount === 0) {
176+
return 0;
177+
}
178+
return Math.min(Math.max(line, 0), document.lineCount - 1);
179+
}
180+
181+
function lowercaseExtension(name: string): string {
182+
const lastDot = name.lastIndexOf(".");
183+
if (lastDot === -1 || lastDot === name.length - 1) {
184+
return name;
185+
}
186+
return name.slice(0, lastDot + 1) + name.slice(lastDot + 1).toLowerCase();
187+
}

src/extension.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ import {
115115
displayableUri,
116116
} from "./utils";
117117
import { ObjectScriptDiagnosticProvider } from "./providers/ObjectScriptDiagnosticProvider";
118-
import { DocumentLinkProvider } from "./providers/DocumentLinkProvider";
119118

120119
/* proposed */
121120
import { FileSearchProvider } from "./providers/FileSystemProvider/FileSearchProvider";
@@ -169,6 +168,10 @@ import {
169168
followDefinitionLinkCommand,
170169
followDefinitionLink,
171170
goToDefinitionLocalFirst,
171+
SourceAnalysisLinkProvider,
172+
followSourceAnalysisLink,
173+
followSourceAnalysisLinkCommand,
174+
type SourceAnalysisLinkArgs,
172175
resolveContextExpression,
173176
showGlobalDocumentation,
174177
} from "./ccs";
@@ -1302,6 +1305,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
13021305
await followDefinitionLink(documentUri, line, character);
13031306
}
13041307
),
1308+
vscode.commands.registerCommand(followSourceAnalysisLinkCommand, async (args: SourceAnalysisLinkArgs) => {
1309+
sendCommandTelemetryEvent("ccs.followSourceAnalysisLink");
1310+
await followSourceAnalysisLink(args);
1311+
}),
13051312
vscode.commands.registerCommand("vscode-objectscript.debug", (program: string, askArgs: boolean) => {
13061313
sendCommandTelemetryEvent("debug");
13071314
const startDebugging = (args) => {
@@ -1537,7 +1544,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
15371544
sendCommandTelemetryEvent("compileOnlyWithFlags");
15381545
compileOnly(true);
15391546
}),
1540-
vscode.languages.registerDocumentLinkProvider({ language: outputLangId }, new DocumentLinkProvider()),
1547+
vscode.languages.registerDocumentLinkProvider({ language: outputLangId }, new SourceAnalysisLinkProvider()),
15411548
vscode.commands.registerCommand("vscode-objectscript.editOthers", () => {
15421549
sendCommandTelemetryEvent("editOthers");
15431550
viewOthers(true);

src/providers/DocumentLinkProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// @deprecated Not registered. Kept only for upstream diffs.
2+
// Active implementation: src/ccs/providers/SourceAnalysisLinkProvider.ts
3+
14
import * as vscode from "vscode";
25
import { DocumentContentProvider } from "./DocumentContentProvider";
36
import { handleError } from "../utils";

0 commit comments

Comments
 (0)