Skip to content

Commit d6044fc

Browse files
Implements trigger locator tool (#71)
1 parent 60ea6bf commit d6044fc

File tree

7 files changed

+435
-1
lines changed

7 files changed

+435
-1
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
"command": "vscode-objectscript.ccs.getGlobalDocumentation",
132132
"when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive"
133133
},
134+
{
135+
"command": "vscode-objectscript.ccs.locateTriggers",
136+
"when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive"
137+
},
134138
{
135139
"command": "vscode-objectscript.subclass",
136140
"when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive"
@@ -881,6 +885,16 @@
881885
"command": "vscode-objectscript.ccs.followSourceAnalysisLink",
882886
"title": "Follow Source Analysis Link"
883887
},
888+
{
889+
"category": "Consistem",
890+
"command": "vscode-objectscript.ccs.locateTriggers",
891+
"title": "Locate Triggers"
892+
},
893+
{
894+
"category": "Consistem",
895+
"command": "vscode-objectscript.ccs.locateTriggers.openLocation",
896+
"title": "Open Located Trigger"
897+
},
884898
{
885899
"category": "Consistem",
886900
"command": "vscode-objectscript.ccs.createItem",

src/ccs/commands/locateTriggers.ts

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import * as path from "path";
2+
import * as vscode from "vscode";
3+
4+
import { DocumentContentProvider } from "../../providers/DocumentContentProvider";
5+
import { AtelierAPI } from "../../api";
6+
import { FILESYSTEM_SCHEMA } from "../../extension";
7+
import { handleError, outputChannel } from "../../utils";
8+
import { LocateTriggersClient, LocateTriggersPayload } from "../sourcecontrol/clients/locateTriggersClient";
9+
import { getUrisForDocument } from "../../utils/documentIndex";
10+
import { notIsfs } from "../../utils";
11+
import { getCcsSettings } from "../config/settings";
12+
import { createAbortSignal } from "../core/http";
13+
import { logDebug } from "../core/logging";
14+
import { ResolveDefinitionResponse } from "../core/types";
15+
import { SourceControlApi } from "../sourcecontrol/client";
16+
import { ROUTES } from "../sourcecontrol/routes";
17+
import { toVscodeLocation } from "../sourcecontrol/paths";
18+
19+
const TRIGGER_PATTERNS = [/GatilhoRegra\^%CSW1GATCUST/i, /GatilhoInterface\^%CSW1GATCUST/i];
20+
21+
const sharedClient = new LocateTriggersClient();
22+
23+
interface RoutineLocation {
24+
routineName: string;
25+
line: number;
26+
}
27+
28+
export async function locateTriggers(): Promise<void> {
29+
const editor = vscode.window.activeTextEditor;
30+
31+
if (!editor) {
32+
return;
33+
}
34+
35+
const routineName = path.basename(editor.document.fileName);
36+
if (!routineName) {
37+
void vscode.window.showErrorMessage("Routine name not available for localizar gatilhos.");
38+
return;
39+
}
40+
41+
const selectedText = getSelectedOrCurrentLineText(editor);
42+
const payload: LocateTriggersPayload = { routineName };
43+
44+
if (shouldSendSelectedText(selectedText)) {
45+
payload.selectedText = escapeTriggerText(selectedText);
46+
}
47+
48+
try {
49+
const { content, api } = await sharedClient.locate(editor.document, payload);
50+
51+
if (!content || !content.trim()) {
52+
void vscode.window.showInformationMessage("Localizar Gatilhos não retornou nenhum conteúdo.");
53+
return;
54+
}
55+
56+
await renderContentToOutput(content, api.ns);
57+
} catch (error) {
58+
handleError(error, "Falha ao localizar gatilhos.");
59+
}
60+
}
61+
62+
export async function openLocatedTriggerLocation(location?: RoutineLocation & { namespace?: string }): Promise<void> {
63+
if (!location?.routineName || !location.line) {
64+
return;
65+
}
66+
67+
const namespace = location.namespace ?? new AtelierAPI().ns;
68+
69+
if (!namespace) {
70+
void vscode.window.showErrorMessage("Não foi possível determinar o namespace para abrir o gatilho.");
71+
return;
72+
}
73+
74+
await openRoutineLocation(location.routineName, location.line, namespace);
75+
}
76+
77+
function getSelectedOrCurrentLineText(editor: vscode.TextEditor): string {
78+
const { selection, document } = editor;
79+
80+
if (!selection || selection.isEmpty) {
81+
return document.lineAt(selection.active.line).text.trim();
82+
}
83+
84+
return document.getText(selection).trim();
85+
}
86+
87+
function shouldSendSelectedText(text: string): boolean {
88+
return TRIGGER_PATTERNS.some((pattern) => pattern.test(text));
89+
}
90+
91+
function escapeTriggerText(text: string): string {
92+
return text.replace(/"/g, '""');
93+
}
94+
95+
async function renderContentToOutput(content: string, namespace?: string): Promise<void> {
96+
const annotatedLines = await annotateRoutineLocations(content, namespace);
97+
98+
annotatedLines.forEach((line) => outputChannel.appendLine(line));
99+
outputChannel.show(true);
100+
}
101+
102+
async function annotateRoutineLocations(content: string, namespace?: string): Promise<string[]> {
103+
const routineLineRegex = /^\s*([\w%][\w%.-]*\.[\w]+)\((\d+)\)/i;
104+
const resolutionCache = new Map<string, Promise<vscode.Uri | undefined>>();
105+
106+
const getResolvedUri = (routineName: string): Promise<vscode.Uri | undefined> => {
107+
const normalizedName = routineName.toLowerCase();
108+
109+
if (!resolutionCache.has(normalizedName)) {
110+
resolutionCache.set(normalizedName, resolveWorkspaceRoutineUri(routineName));
111+
}
112+
113+
return resolutionCache.get(normalizedName) ?? Promise.resolve(undefined);
114+
};
115+
116+
return Promise.all(
117+
content.split(/\r?\n/).map(async (line) => {
118+
const match = routineLineRegex.exec(line);
119+
120+
if (!match) {
121+
return line;
122+
}
123+
124+
const [, routineName, lineStr] = match;
125+
const lineNumber = Number.parseInt(lineStr, 10);
126+
127+
if (!Number.isFinite(lineNumber)) {
128+
return line;
129+
}
130+
131+
const resolvedUri = await getResolvedUri(routineName);
132+
const baseLine = line.replace(/\s+$/, "");
133+
134+
if (resolvedUri) {
135+
return `${baseLine} (${resolvedUri.toString()})`;
136+
}
137+
138+
return baseLine;
139+
})
140+
);
141+
}
142+
143+
async function openRoutineLocation(routineName: string, line: number, namespace: string): Promise<void> {
144+
const targetUri = await resolveRoutineUri(routineName, namespace);
145+
146+
if (!targetUri) {
147+
void vscode.window.showErrorMessage(`Não foi possível abrir a rotina ${routineName}.`);
148+
return;
149+
}
150+
151+
const document = await vscode.workspace.openTextDocument(targetUri);
152+
const editor = await vscode.window.showTextDocument(document, { preview: false });
153+
const targetLine = Math.max(line - 1, 0);
154+
const position = new vscode.Position(targetLine, 0);
155+
editor.selection = new vscode.Selection(position, position);
156+
editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter);
157+
}
158+
159+
async function getRoutineUriFromDefinition(routineName: string, namespace: string): Promise<vscode.Uri | undefined> {
160+
const api = new AtelierAPI();
161+
api.setNamespace(namespace);
162+
163+
if (!api.active || !api.ns) {
164+
return undefined;
165+
}
166+
167+
let sourceControlApi: SourceControlApi;
168+
169+
try {
170+
sourceControlApi = SourceControlApi.fromAtelierApi(api);
171+
} catch (error) {
172+
logDebug("Failed to create SourceControl API client for resolveDefinition", error);
173+
return undefined;
174+
}
175+
176+
const { requestTimeout } = getCcsSettings();
177+
const tokenSource = new vscode.CancellationTokenSource();
178+
const { signal, dispose } = createAbortSignal(tokenSource.token);
179+
const query = `^${routineName}`;
180+
181+
try {
182+
const response = await sourceControlApi.post<ResolveDefinitionResponse>(
183+
ROUTES.resolveDefinition(api.ns),
184+
{ query },
185+
{
186+
timeout: requestTimeout,
187+
signal,
188+
validateStatus: (status) => status >= 200 && status < 300,
189+
}
190+
);
191+
192+
return toVscodeLocation(response.data ?? {})?.uri;
193+
} catch (error) {
194+
logDebug("ResolveDefinition lookup for localizar gatilhos failed", error);
195+
return undefined;
196+
} finally {
197+
dispose();
198+
tokenSource.dispose();
199+
}
200+
}
201+
202+
async function getRoutineUri(routineName: string, namespace: string): Promise<vscode.Uri | null> {
203+
const workspaceUri = await findWorkspaceRoutineUri(routineName);
204+
205+
if (workspaceUri) {
206+
return workspaceUri;
207+
}
208+
209+
const primaryUri = DocumentContentProvider.getUri(routineName, undefined, namespace);
210+
211+
if (primaryUri) {
212+
if (primaryUri.scheme === "file") {
213+
try {
214+
await vscode.workspace.fs.stat(primaryUri);
215+
return primaryUri;
216+
} catch (error) {
217+
// Fall back to isfs when the routine isn't available locally.
218+
}
219+
} else {
220+
return primaryUri;
221+
}
222+
}
223+
224+
const fallbackWorkspaceUri = vscode.Uri.parse(`${FILESYSTEM_SCHEMA}://consistem:${namespace}/`);
225+
return (
226+
DocumentContentProvider.getUri(routineName, undefined, namespace, undefined, fallbackWorkspaceUri, true) ??
227+
primaryUri
228+
);
229+
}
230+
231+
async function resolveRoutineUri(routineName: string, namespace?: string): Promise<vscode.Uri | undefined> {
232+
const workspaceUri = await findWorkspaceRoutineUri(routineName);
233+
234+
if (workspaceUri) {
235+
return workspaceUri;
236+
}
237+
238+
if (!namespace) {
239+
return undefined;
240+
}
241+
242+
const definitionUri = await getRoutineUriFromDefinition(routineName, namespace);
243+
244+
if (definitionUri) {
245+
return definitionUri;
246+
}
247+
248+
return (await getRoutineUri(routineName, namespace)) ?? undefined;
249+
}
250+
251+
async function resolveWorkspaceRoutineUri(routineName: string): Promise<vscode.Uri | undefined> {
252+
const workspaceUri = await findWorkspaceRoutineUri(routineName);
253+
254+
if (!workspaceUri) {
255+
return undefined;
256+
}
257+
258+
try {
259+
await vscode.workspace.fs.stat(workspaceUri);
260+
return workspaceUri;
261+
} catch (error) {
262+
return undefined;
263+
}
264+
}
265+
266+
async function findWorkspaceRoutineUri(routineName: string): Promise<vscode.Uri | undefined> {
267+
const workspaces = vscode.workspace.workspaceFolders ?? [];
268+
const candidates: vscode.Uri[] = [];
269+
const dedupe = new Set<string>();
270+
const preferredRoot = normalizeFsPath(path.normalize("C:/workspacecsw/projetos/COMP-7.0/xcustom/"));
271+
272+
const addCandidate = (uri: vscode.Uri): void => {
273+
if (!notIsfs(uri) || dedupe.has(uri.toString())) {
274+
return;
275+
}
276+
277+
candidates.push(uri);
278+
dedupe.add(uri.toString());
279+
};
280+
281+
for (const workspace of workspaces) {
282+
if (!notIsfs(workspace.uri)) {
283+
continue;
284+
}
285+
286+
for (const uri of getUrisForDocument(routineName, workspace)) {
287+
addCandidate(uri);
288+
}
289+
}
290+
291+
const allMatches = await vscode.workspace.findFiles(`**/${routineName}`);
292+
const preferredMatches: vscode.Uri[] = [];
293+
294+
for (const uri of allMatches) {
295+
if (!notIsfs(uri)) {
296+
continue;
297+
}
298+
299+
const normalizedPath = normalizeFsPath(uri.fsPath);
300+
301+
if (normalizedPath.includes(preferredRoot)) {
302+
preferredMatches.push(uri);
303+
}
304+
305+
addCandidate(uri);
306+
}
307+
308+
if (preferredMatches.length) {
309+
return preferredMatches[0];
310+
}
311+
312+
if (!candidates.length) {
313+
return undefined;
314+
}
315+
316+
const preferredSegment = `${path.sep}xcustom${path.sep}`;
317+
318+
return (
319+
candidates.find((uri) => {
320+
const lowerPath = normalizeFsPath(uri.fsPath);
321+
return (
322+
lowerPath.includes(preferredSegment) || lowerPath.includes("/xcustom/") || lowerPath.includes("\\xcustom\\")
323+
);
324+
}) ?? candidates[0]
325+
);
326+
}
327+
328+
function normalizeFsPath(p: string): string {
329+
return p.replace(/\\/g, "/").toLowerCase();
330+
}

src/ccs/core/http.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,13 @@ function resolveFullUrl(client: AxiosInstance, config: AxiosRequestConfig | Inte
7070
return `${base}${url}`;
7171
}
7272

73-
export function createAbortSignal(token: vscode.CancellationToken): { signal: AbortSignal; dispose: () => void } {
73+
export function createAbortSignal(token?: vscode.CancellationToken): { signal: AbortSignal; dispose: () => void } {
7474
const controller = new AbortController();
75+
76+
if (!token) {
77+
return { signal: controller.signal, dispose: () => undefined };
78+
}
79+
7580
const subscription = token.onCancellationRequested(() => controller.abort());
7681

7782
return {

src/ccs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
export { goToDefinitionLocalFirst } from "./commands/goToDefinitionLocalFirst";
1717
export { followDefinitionLink } from "./commands/followDefinitionLink";
1818
export { jumpToTagAndOffsetCrossEntity } from "./commands/jumpToTagOffsetCrossEntity";
19+
export { locateTriggers, openLocatedTriggerLocation } from "./commands/locateTriggers";
1920
export { PrioritizedDefinitionProvider } from "./providers/PrioritizedDefinitionProvider";
2021
export {
2122
DefinitionDocumentLinkProvider,

0 commit comments

Comments
 (0)