Skip to content

Commit b050ee8

Browse files
Context-help: add new response handling for context help API (#56)
1 parent cedc0e1 commit b050ee8

File tree

3 files changed

+212
-29
lines changed

3 files changed

+212
-29
lines changed

src/ccs/commands/contextHelp.ts

Lines changed: 197 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { URL } from "url";
33
import * as vscode from "vscode";
44

55
import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient";
6+
import { GlobalDocumentationResponse, ResolveContextExpressionResponse } from "../core/types";
67
import { handleError } from "../../utils";
78

89
const sharedClient = new ContextExpressionClient();
10+
const CONTEXT_HELP_PANEL_VIEW_TYPE = "contextHelpPreview";
11+
const CONTEXT_HELP_TITLE = "Ajuda de Contexto";
912

1013
export async function resolveContextExpression(): Promise<void> {
1114
const editor = vscode.window.activeTextEditor;
@@ -28,45 +31,69 @@ export async function resolveContextExpression(): Promise<void> {
2831
const response = await sharedClient.resolve(document, { routine, contextExpression });
2932
const data = response ?? {};
3033

31-
if (typeof data.status === "string" && data.status.toLowerCase() === "success" && data.textExpression) {
32-
const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n";
34+
if (typeof data === "string") {
35+
await handleContextHelpDocumentationContent(data);
36+
return;
37+
}
38+
39+
if (hasGlobalDocumentationContent(data)) {
40+
const normalizedContent = normalizeGlobalDocumentationContent(data.content);
41+
42+
if (normalizedContent.trim()) {
43+
await handleContextHelpDocumentationContent(normalizedContent);
44+
} else {
45+
const message = data.message || "A ajuda de contexto não retornou nenhum conteúdo.";
46+
void vscode.window.showInformationMessage(message);
47+
}
48+
return;
49+
}
50+
51+
if (isSuccessfulTextExpression(data)) {
52+
const hasGifCommand = /--gif\b/i.test(contextExpression);
3353
let normalizedTextExpression = data.textExpression.replace(/\r?\n/g, "\n");
3454
let gifUri: vscode.Uri | undefined;
3555

36-
if (/--gif\b/i.test(contextExpression)) {
56+
if (hasGifCommand) {
3757
const extracted = extractGifUri(normalizedTextExpression);
3858
normalizedTextExpression = extracted.textWithoutGifUri;
3959
gifUri = extracted.gifUri;
4060
}
4161

42-
const textExpression = normalizedTextExpression.replace(/\r?\n/g, eol);
43-
const formattedTextExpression = textExpression;
62+
if (!hasGifCommand) {
63+
const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n";
64+
const textExpression = normalizedTextExpression.replace(/\r?\n/g, eol);
65+
const formattedTextExpression = textExpression;
66+
67+
let rangeToReplace: vscode.Range;
68+
if (selection.isEmpty) {
69+
const fallbackLine = document.lineAt(selection.active.line);
70+
rangeToReplace = fallbackLine.range;
71+
} else {
72+
const start = document.lineAt(selection.start.line).range.start;
73+
const replacementEnd = contextInfo.replacementEnd ?? document.lineAt(selection.end.line).range.end;
74+
rangeToReplace = new vscode.Range(start, replacementEnd);
75+
}
4476

45-
let rangeToReplace: vscode.Range;
46-
if (selection.isEmpty) {
47-
const fallbackLine = document.lineAt(selection.active.line);
48-
rangeToReplace = fallbackLine.range;
49-
} else {
50-
const start = document.lineAt(selection.start.line).range.start;
51-
const replacementEnd = contextInfo.replacementEnd ?? document.lineAt(selection.end.line).range.end;
52-
rangeToReplace = new vscode.Range(start, replacementEnd);
77+
await editor.edit((editBuilder) => {
78+
editBuilder.replace(rangeToReplace, formattedTextExpression);
79+
});
5380
}
5481

55-
await editor.edit((editBuilder) => {
56-
editBuilder.replace(rangeToReplace, formattedTextExpression);
57-
});
58-
5982
if (gifUri) {
6083
try {
6184
await showGifInWebview(gifUri);
6285
} catch (error) {
6386
handleError(error, "Failed to open GIF from context expression.");
6487
}
6588
}
66-
} else {
67-
const errorMessage = data.message || "Failed to resolve context expression.";
68-
void vscode.window.showErrorMessage(errorMessage);
89+
return;
6990
}
91+
92+
const errorMessage =
93+
typeof data === "object" && data && "message" in data && typeof data.message === "string"
94+
? data.message
95+
: "Failed to resolve context expression.";
96+
void vscode.window.showErrorMessage(errorMessage);
7097
} catch (error) {
7198
handleError(error, "Failed to resolve context expression.");
7299
}
@@ -132,6 +159,156 @@ function extractGifUri(text: string): {
132159
return { textWithoutGifUri: processedLines.join("\n"), gifUri };
133160
}
134161

162+
async function handleContextHelpDocumentationContent(rawContent: string): Promise<void> {
163+
const sanitizedContent = sanitizeContextHelpContent(rawContent);
164+
165+
if (!sanitizedContent.trim()) {
166+
void vscode.window.showInformationMessage("A ajuda de contexto não retornou nenhum conteúdo.");
167+
return;
168+
}
169+
170+
const errorMessage = extractContextHelpError(sanitizedContent);
171+
if (errorMessage) {
172+
void vscode.window.showErrorMessage(errorMessage);
173+
return;
174+
}
175+
176+
await showContextHelpPreview(sanitizedContent);
177+
}
178+
179+
function sanitizeContextHelpContent(content: string): string {
180+
let sanitized = content.replace(/\{"status":"success","textExpression":""\}\s*$/i, "");
181+
182+
sanitized = sanitized.replace(/^\s*=+\s*Global Documentation\s*=+\s*(?:\r?\n)?/i, "");
183+
184+
return sanitized.replace(/\r?\n/g, "\n");
185+
}
186+
187+
function extractContextHelpError(content: string): string | undefined {
188+
const commandNotImplemented = content.match(/Comando\s+"([^"]+)"\s+n[ãa]o implementado!/i);
189+
if (commandNotImplemented) {
190+
return commandNotImplemented[0].replace(/\s+/g, " ");
191+
}
192+
193+
return undefined;
194+
}
195+
196+
async function showContextHelpPreview(content: string): Promise<void> {
197+
const panel = vscode.window.createWebviewPanel(
198+
CONTEXT_HELP_PANEL_VIEW_TYPE,
199+
CONTEXT_HELP_TITLE,
200+
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: false },
201+
{
202+
enableFindWidget: true,
203+
enableScripts: false,
204+
retainContextWhenHidden: false,
205+
}
206+
);
207+
208+
panel.webview.html = getContextHelpWebviewHtml(panel.webview, content);
209+
}
210+
211+
function getContextHelpWebviewHtml(webview: vscode.Webview, content: string): string {
212+
const escapedContent = escapeHtml(content);
213+
const cspSource = escapeHtml(webview.cspSource);
214+
const escapedTitle = escapeHtml(CONTEXT_HELP_TITLE);
215+
216+
return `<!DOCTYPE html>
217+
<html lang="pt-BR">
218+
<head>
219+
<meta charset="UTF-8" />
220+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${cspSource} 'unsafe-inline';" />
221+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
222+
<title>${escapedTitle}</title>
223+
<style>
224+
body {
225+
margin: 0;
226+
padding: 16px;
227+
background-color: var(--vscode-editor-background, #1e1e1e);
228+
color: var(--vscode-editor-foreground, #d4d4d4);
229+
font-family: var(--vscode-editor-font-family, Consolas, 'Courier New', monospace);
230+
font-size: var(--vscode-editor-font-size, 14px);
231+
line-height: 1.5;
232+
}
233+
234+
pre {
235+
white-space: pre; /* em vez de pre-wrap */
236+
word-break: normal; /* em vez de break-word */
237+
overflow-x: auto; /* barra horizontal quando precisar */
238+
overflow-y: auto; /* mantém a vertical também */
239+
max-width: 100%;
240+
}
241+
</style>
242+
243+
</head>
244+
<body>
245+
<pre>${escapedContent}</pre>
246+
</body>
247+
</html>`;
248+
}
249+
250+
function hasGlobalDocumentationContent(
251+
value: unknown
252+
): value is Pick<GlobalDocumentationResponse, "content" | "message"> {
253+
if (!isRecord(value)) {
254+
return false;
255+
}
256+
257+
if (!("content" in value)) {
258+
return false;
259+
}
260+
261+
const content = (value as GlobalDocumentationResponse).content;
262+
263+
return (
264+
typeof content === "string" ||
265+
Array.isArray(content) ||
266+
(content !== null && typeof content === "object") ||
267+
content === null
268+
);
269+
}
270+
271+
function normalizeGlobalDocumentationContent(content: GlobalDocumentationResponse["content"]): string {
272+
if (typeof content === "string") {
273+
return content;
274+
}
275+
276+
if (Array.isArray(content)) {
277+
return content.join("\n");
278+
}
279+
280+
if (content && typeof content === "object") {
281+
try {
282+
return JSON.stringify(content, null, 2);
283+
} catch (error) {
284+
handleError(error, "Failed to parse global documentation content.");
285+
}
286+
}
287+
288+
return "";
289+
}
290+
291+
function isSuccessfulTextExpression(
292+
value: unknown
293+
): value is Required<Pick<ResolveContextExpressionResponse, "textExpression">> & ResolveContextExpressionResponse {
294+
if (!isRecord(value)) {
295+
return false;
296+
}
297+
298+
const { status, textExpression } = value as ResolveContextExpressionResponse;
299+
300+
return (
301+
typeof status === "string" &&
302+
status.toLowerCase() === "success" &&
303+
typeof textExpression === "string" &&
304+
textExpression.length > 0
305+
);
306+
}
307+
308+
function isRecord(value: unknown): value is Record<string, unknown> {
309+
return typeof value === "object" && value !== null;
310+
}
311+
135312
function getFileUriFromText(text: string): vscode.Uri | undefined {
136313
const trimmed = text.trim();
137314
if (!trimmed.toLowerCase().startsWith("file://")) {

src/ccs/core/types.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ export interface LocationJSON {
55

66
export type ResolveDefinitionResponse = LocationJSON;
77

8+
export interface GlobalDocumentationResponse {
9+
content?: string | string[] | Record<string, unknown> | null;
10+
message?: string;
11+
}
12+
813
export interface ResolveContextExpressionResponse {
914
status?: string;
1015
textExpression?: string;
1116
message?: string;
1217
}
1318

19+
export type ResolveContextExpressionResult = ResolveContextExpressionResponse | GlobalDocumentationResponse | string;
20+
1421
export interface SourceControlError {
1522
message: string;
1623
cause?: unknown;
1724
}
18-
export interface GlobalDocumentationResponse {
19-
content?: string | string[] | Record<string, unknown> | null;
20-
message?: string;
21-
}
22-
2325
export interface CreateItemResponse {
2426
item?: Record<string, unknown>;
2527
name?: string;

src/ccs/sourcecontrol/clients/contextExpressionClient.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as vscode from "vscode";
33
import { AtelierAPI } from "../../../api";
44
import { getCcsSettings } from "../../config/settings";
55
import { logDebug } from "../../core/logging";
6-
import { ResolveContextExpressionResponse } from "../../core/types";
6+
import { ResolveContextExpressionResult } from "../../core/types";
77
import { SourceControlApi } from "../client";
88
import { ROUTES } from "../routes";
99

@@ -22,7 +22,7 @@ export class ContextExpressionClient {
2222
public async resolve(
2323
document: vscode.TextDocument,
2424
payload: ResolveContextExpressionPayload
25-
): Promise<ResolveContextExpressionResponse> {
25+
): Promise<ResolveContextExpressionResult> {
2626
const api = new AtelierAPI(document.uri);
2727

2828
let sourceControlApi: SourceControlApi;
@@ -36,7 +36,7 @@ export class ContextExpressionClient {
3636
const { requestTimeout } = getCcsSettings();
3737

3838
try {
39-
const response = await sourceControlApi.post<ResolveContextExpressionResponse>(
39+
const response = await sourceControlApi.post<ResolveContextExpressionResult>(
4040
ROUTES.resolveContextExpression(),
4141
payload,
4242
{
@@ -45,7 +45,11 @@ export class ContextExpressionClient {
4545
}
4646
);
4747

48-
return response.data ?? {};
48+
if (typeof response.data === "undefined" || response.data === null) {
49+
return {};
50+
}
51+
52+
return response.data;
4953
} catch (error) {
5054
logDebug("Context expression resolution failed", error);
5155
throw error;

0 commit comments

Comments
 (0)