Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,11 @@
"command": "vscode-objectscript.ccs.followDefinitionLink",
"title": "Follow Definition Link"
},
{
"category": "ObjectScript",
"command": "vscode-objectscript.ccs.followSourceAnalysisLink",
"title": "Follow Source Analysis Link"
},
{
"category": "ObjectScript",
"command": "vscode-objectscript.compile",
Expand Down
6 changes: 6 additions & 0 deletions src/ccs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ export {
DefinitionDocumentLinkProvider,
followDefinitionLinkCommand,
} from "./providers/DefinitionDocumentLinkProvider";
export {
SourceAnalysisLinkProvider,
type SourceAnalysisLinkArgs,
followSourceAnalysisLink,
followSourceAnalysisLinkCommand,
} from "./providers/SourceAnalysisLinkProvider";
187 changes: 187 additions & 0 deletions src/ccs/providers/SourceAnalysisLinkProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import * as vscode from "vscode";

import { DocumentContentProvider } from "../../providers/DocumentContentProvider";
import { logDebug } from "../core/logging";

export const followSourceAnalysisLinkCommand = "vscode-objectscript.ccs.followSourceAnalysisLink" as const;

const METHOD_WITH_OFFSET_REGEX = /([%\w.]+)\(([\w%]+)\+(\d+)\)/g;
const ROUTINE_OFFSET_REGEX = /([%\w.]+)\((\d+)\)/g;

export interface SourceAnalysisLinkArgs {
targetUri: string;
offset: number;
methodName?: string;
}

export class SourceAnalysisLinkProvider implements vscode.DocumentLinkProvider {
public provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
const links: vscode.DocumentLink[] = [];

for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
const text = document.lineAt(lineIndex).text;

METHOD_WITH_OFFSET_REGEX.lastIndex = 0;
for (const match of text.matchAll(METHOD_WITH_OFFSET_REGEX)) {
const [fullMatch, filename, methodName, offsetString] = match;
const range = new vscode.Range(
new vscode.Position(lineIndex, match.index ?? 0),
new vscode.Position(lineIndex, (match.index ?? 0) + fullMatch.length)
);
const link = this.createLink(range, filename, Number.parseInt(offsetString, 10), methodName);
if (link) {
links.push(link);
}
}

ROUTINE_OFFSET_REGEX.lastIndex = 0;
for (const match of text.matchAll(ROUTINE_OFFSET_REGEX)) {
const [fullMatch, filename, offsetString] = match;

// Skip matches that also match the method+offset pattern, which has already been handled above.
if (/\+/.test(fullMatch)) {
continue;
}

const range = new vscode.Range(
new vscode.Position(lineIndex, match.index ?? 0),
new vscode.Position(lineIndex, (match.index ?? 0) + fullMatch.length)
);
const link = this.createLink(range, filename, Number.parseInt(offsetString, 10));
if (link) {
links.push(link);
}
}
}

return links;
}

private createLink(
range: vscode.Range,
filename: string,
offset: number,
methodName?: string
): vscode.DocumentLink | undefined {
if (!Number.isFinite(offset)) {
return undefined;
}

const normalizedFilename = lowercaseExtension(filename);
const targetUri = DocumentContentProvider.getUri(normalizedFilename);
if (!targetUri) {
return undefined;
}

const args: SourceAnalysisLinkArgs = {
targetUri: targetUri.toString(),
offset,
...(methodName ? { methodName } : {}),
};

const commandUri = vscode.Uri.parse(
`command:${followSourceAnalysisLinkCommand}?${encodeURIComponent(JSON.stringify(args))}`
);

const link = new vscode.DocumentLink(range, commandUri);
link.tooltip = vscode.l10n.t("Open Source Analysis location");
return link;
}
}

export async function followSourceAnalysisLink(args: SourceAnalysisLinkArgs): Promise<void> {
try {
if (!args?.targetUri) {
logDebug("Missing targetUri for source analysis link", args);
return;
}

const uri = vscode.Uri.parse(args.targetUri);
const editor = await vscode.window.showTextDocument(uri, { preview: false });
const document = editor.document;

const targetLine = await resolveTargetLine(uri, document, args.offset, args.methodName);
const line = document.lineAt(targetLine);
const position = line.range.start;
editor.selection = new vscode.Selection(position, position);
editor.revealRange(line.range, vscode.TextEditorRevealType.InCenter);
} catch (error) {
logDebug("Failed to follow source analysis link", error);
}
}

async function resolveTargetLine(
uri: vscode.Uri,
document: vscode.TextDocument,
offset: number,
methodName?: string
): Promise<number> {
const clampedOffset = Math.max(offset, 0);

if (!methodName) {
return clampLine(document, Math.max(clampedOffset - 1, 0));
}

const methodStartLine = await findMethodStartLine(uri, methodName);
if (typeof methodStartLine === "number") {
return clampLine(document, methodStartLine + clampedOffset);
}

return clampLine(document, Math.max(clampedOffset - 1, 0));
}

async function findMethodStartLine(uri: vscode.Uri, methodName: string): Promise<number | undefined> {
try {
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
"vscode.executeDocumentSymbolProvider",
uri
);
const methodSymbol = findMethodSymbol(symbols, methodName);
return methodSymbol?.range.start.line;
} catch (error) {
logDebug("Failed to resolve document symbols for source analysis link", error);
return undefined;
}
}

function findMethodSymbol(
symbols: readonly vscode.DocumentSymbol[] | undefined,
methodName: string
): vscode.DocumentSymbol | undefined {
if (!Array.isArray(symbols)) {
return undefined;
}

for (const symbol of symbols) {
if (isMethodSymbol(symbol) && symbol.name === methodName) {
return symbol;
}

const child = findMethodSymbol(symbol.children, methodName);
if (child) {
return child;
}
}

return undefined;
}

function isMethodSymbol(symbol: vscode.DocumentSymbol): boolean {
const detail = symbol.detail ?? "";
return detail === "Method" || detail === "ClassMethod";
}

function clampLine(document: vscode.TextDocument, line: number): number {
if (document.lineCount === 0) {
return 0;
}
return Math.min(Math.max(line, 0), document.lineCount - 1);
}

function lowercaseExtension(name: string): string {
const lastDot = name.lastIndexOf(".");
if (lastDot === -1 || lastDot === name.length - 1) {
return name;
}
return name.slice(0, lastDot + 1) + name.slice(lastDot + 1).toLowerCase();
}
11 changes: 9 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ import {
displayableUri,
} from "./utils";
import { ObjectScriptDiagnosticProvider } from "./providers/ObjectScriptDiagnosticProvider";
import { DocumentLinkProvider } from "./providers/DocumentLinkProvider";

/* proposed */
import { FileSearchProvider } from "./providers/FileSystemProvider/FileSearchProvider";
Expand Down Expand Up @@ -169,6 +168,10 @@ import {
followDefinitionLinkCommand,
followDefinitionLink,
goToDefinitionLocalFirst,
SourceAnalysisLinkProvider,
followSourceAnalysisLink,
followSourceAnalysisLinkCommand,
type SourceAnalysisLinkArgs,
resolveContextExpression,
showGlobalDocumentation,
} from "./ccs";
Expand Down Expand Up @@ -1302,6 +1305,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
await followDefinitionLink(documentUri, line, character);
}
),
vscode.commands.registerCommand(followSourceAnalysisLinkCommand, async (args: SourceAnalysisLinkArgs) => {
sendCommandTelemetryEvent("ccs.followSourceAnalysisLink");
await followSourceAnalysisLink(args);
}),
vscode.commands.registerCommand("vscode-objectscript.debug", (program: string, askArgs: boolean) => {
sendCommandTelemetryEvent("debug");
const startDebugging = (args) => {
Expand Down Expand Up @@ -1537,7 +1544,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
sendCommandTelemetryEvent("compileOnlyWithFlags");
compileOnly(true);
}),
vscode.languages.registerDocumentLinkProvider({ language: outputLangId }, new DocumentLinkProvider()),
vscode.languages.registerDocumentLinkProvider({ language: outputLangId }, new SourceAnalysisLinkProvider()),
vscode.commands.registerCommand("vscode-objectscript.editOthers", () => {
sendCommandTelemetryEvent("editOthers");
viewOthers(true);
Expand Down
3 changes: 3 additions & 0 deletions src/providers/DocumentLinkProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// @deprecated Not registered. Kept only for upstream diffs.
// Active implementation: src/ccs/providers/SourceAnalysisLinkProvider.ts

import * as vscode from "vscode";
import { DocumentContentProvider } from "./DocumentContentProvider";
import { handleError } from "../utils";
Expand Down