From f4643f4dc449e54d499eefb800fad804725edb1d Mon Sep 17 00:00:00 2001 From: Ricardo Alves <112292689+RicardoASJunior@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:02:02 -0300 Subject: [PATCH] feat(context-help): add new response handling for context help API --- src/ccs/commands/contextHelp.ts | 217 ++++++++++++++++-- src/ccs/core/types.ts | 12 +- .../clients/contextExpressionClient.ts | 12 +- 3 files changed, 212 insertions(+), 29 deletions(-) diff --git a/src/ccs/commands/contextHelp.ts b/src/ccs/commands/contextHelp.ts index 083e3028..0901f7ac 100644 --- a/src/ccs/commands/contextHelp.ts +++ b/src/ccs/commands/contextHelp.ts @@ -3,9 +3,12 @@ import { URL } from "url"; import * as vscode from "vscode"; import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient"; +import { GlobalDocumentationResponse, ResolveContextExpressionResponse } from "../core/types"; import { handleError } from "../../utils"; const sharedClient = new ContextExpressionClient(); +const CONTEXT_HELP_PANEL_VIEW_TYPE = "contextHelpPreview"; +const CONTEXT_HELP_TITLE = "Ajuda de Contexto"; export async function resolveContextExpression(): Promise { const editor = vscode.window.activeTextEditor; @@ -28,34 +31,54 @@ export async function resolveContextExpression(): Promise { const response = await sharedClient.resolve(document, { routine, contextExpression }); const data = response ?? {}; - if (typeof data.status === "string" && data.status.toLowerCase() === "success" && data.textExpression) { - const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; + if (typeof data === "string") { + await handleContextHelpDocumentationContent(data); + return; + } + + if (hasGlobalDocumentationContent(data)) { + const normalizedContent = normalizeGlobalDocumentationContent(data.content); + + if (normalizedContent.trim()) { + await handleContextHelpDocumentationContent(normalizedContent); + } else { + const message = data.message || "A ajuda de contexto não retornou nenhum conteúdo."; + void vscode.window.showInformationMessage(message); + } + return; + } + + if (isSuccessfulTextExpression(data)) { + const hasGifCommand = /--gif\b/i.test(contextExpression); let normalizedTextExpression = data.textExpression.replace(/\r?\n/g, "\n"); let gifUri: vscode.Uri | undefined; - if (/--gif\b/i.test(contextExpression)) { + if (hasGifCommand) { const extracted = extractGifUri(normalizedTextExpression); normalizedTextExpression = extracted.textWithoutGifUri; gifUri = extracted.gifUri; } - const textExpression = normalizedTextExpression.replace(/\r?\n/g, eol); - const formattedTextExpression = textExpression; + if (!hasGifCommand) { + const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; + const textExpression = normalizedTextExpression.replace(/\r?\n/g, eol); + const formattedTextExpression = textExpression; + + let rangeToReplace: vscode.Range; + if (selection.isEmpty) { + const fallbackLine = document.lineAt(selection.active.line); + rangeToReplace = fallbackLine.range; + } else { + const start = document.lineAt(selection.start.line).range.start; + const replacementEnd = contextInfo.replacementEnd ?? document.lineAt(selection.end.line).range.end; + rangeToReplace = new vscode.Range(start, replacementEnd); + } - let rangeToReplace: vscode.Range; - if (selection.isEmpty) { - const fallbackLine = document.lineAt(selection.active.line); - rangeToReplace = fallbackLine.range; - } else { - const start = document.lineAt(selection.start.line).range.start; - const replacementEnd = contextInfo.replacementEnd ?? document.lineAt(selection.end.line).range.end; - rangeToReplace = new vscode.Range(start, replacementEnd); + await editor.edit((editBuilder) => { + editBuilder.replace(rangeToReplace, formattedTextExpression); + }); } - await editor.edit((editBuilder) => { - editBuilder.replace(rangeToReplace, formattedTextExpression); - }); - if (gifUri) { try { await showGifInWebview(gifUri); @@ -63,10 +86,14 @@ export async function resolveContextExpression(): Promise { handleError(error, "Failed to open GIF from context expression."); } } - } else { - const errorMessage = data.message || "Failed to resolve context expression."; - void vscode.window.showErrorMessage(errorMessage); + return; } + + const errorMessage = + typeof data === "object" && data && "message" in data && typeof data.message === "string" + ? data.message + : "Failed to resolve context expression."; + void vscode.window.showErrorMessage(errorMessage); } catch (error) { handleError(error, "Failed to resolve context expression."); } @@ -132,6 +159,156 @@ function extractGifUri(text: string): { return { textWithoutGifUri: processedLines.join("\n"), gifUri }; } +async function handleContextHelpDocumentationContent(rawContent: string): Promise { + const sanitizedContent = sanitizeContextHelpContent(rawContent); + + if (!sanitizedContent.trim()) { + void vscode.window.showInformationMessage("A ajuda de contexto não retornou nenhum conteúdo."); + return; + } + + const errorMessage = extractContextHelpError(sanitizedContent); + if (errorMessage) { + void vscode.window.showErrorMessage(errorMessage); + return; + } + + await showContextHelpPreview(sanitizedContent); +} + +function sanitizeContextHelpContent(content: string): string { + let sanitized = content.replace(/\{"status":"success","textExpression":""\}\s*$/i, ""); + + sanitized = sanitized.replace(/^\s*=+\s*Global Documentation\s*=+\s*(?:\r?\n)?/i, ""); + + return sanitized.replace(/\r?\n/g, "\n"); +} + +function extractContextHelpError(content: string): string | undefined { + const commandNotImplemented = content.match(/Comando\s+"([^"]+)"\s+n[ãa]o implementado!/i); + if (commandNotImplemented) { + return commandNotImplemented[0].replace(/\s+/g, " "); + } + + return undefined; +} + +async function showContextHelpPreview(content: string): Promise { + const panel = vscode.window.createWebviewPanel( + CONTEXT_HELP_PANEL_VIEW_TYPE, + CONTEXT_HELP_TITLE, + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }, + { + enableFindWidget: true, + enableScripts: false, + retainContextWhenHidden: false, + } + ); + + panel.webview.html = getContextHelpWebviewHtml(panel.webview, content); +} + +function getContextHelpWebviewHtml(webview: vscode.Webview, content: string): string { + const escapedContent = escapeHtml(content); + const cspSource = escapeHtml(webview.cspSource); + const escapedTitle = escapeHtml(CONTEXT_HELP_TITLE); + + return ` + + + + + + ${escapedTitle} + + + + +
${escapedContent}
+ +`; +} + +function hasGlobalDocumentationContent( + value: unknown +): value is Pick { + if (!isRecord(value)) { + return false; + } + + if (!("content" in value)) { + return false; + } + + const content = (value as GlobalDocumentationResponse).content; + + return ( + typeof content === "string" || + Array.isArray(content) || + (content !== null && typeof content === "object") || + content === null + ); +} + +function normalizeGlobalDocumentationContent(content: GlobalDocumentationResponse["content"]): string { + if (typeof content === "string") { + return content; + } + + if (Array.isArray(content)) { + return content.join("\n"); + } + + if (content && typeof content === "object") { + try { + return JSON.stringify(content, null, 2); + } catch (error) { + handleError(error, "Failed to parse global documentation content."); + } + } + + return ""; +} + +function isSuccessfulTextExpression( + value: unknown +): value is Required> & ResolveContextExpressionResponse { + if (!isRecord(value)) { + return false; + } + + const { status, textExpression } = value as ResolveContextExpressionResponse; + + return ( + typeof status === "string" && + status.toLowerCase() === "success" && + typeof textExpression === "string" && + textExpression.length > 0 + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + function getFileUriFromText(text: string): vscode.Uri | undefined { const trimmed = text.trim(); if (!trimmed.toLowerCase().startsWith("file://")) { diff --git a/src/ccs/core/types.ts b/src/ccs/core/types.ts index 20e19b92..c6b60f62 100644 --- a/src/ccs/core/types.ts +++ b/src/ccs/core/types.ts @@ -5,21 +5,23 @@ export interface LocationJSON { export type ResolveDefinitionResponse = LocationJSON; +export interface GlobalDocumentationResponse { + content?: string | string[] | Record | null; + message?: string; +} + export interface ResolveContextExpressionResponse { status?: string; textExpression?: string; message?: string; } +export type ResolveContextExpressionResult = ResolveContextExpressionResponse | GlobalDocumentationResponse | string; + export interface SourceControlError { message: string; cause?: unknown; } -export interface GlobalDocumentationResponse { - content?: string | string[] | Record | null; - message?: string; -} - export interface CreateItemResponse { item?: Record; name?: string; diff --git a/src/ccs/sourcecontrol/clients/contextExpressionClient.ts b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts index 50461039..a0e566d0 100644 --- a/src/ccs/sourcecontrol/clients/contextExpressionClient.ts +++ b/src/ccs/sourcecontrol/clients/contextExpressionClient.ts @@ -3,7 +3,7 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../../../api"; import { getCcsSettings } from "../../config/settings"; import { logDebug } from "../../core/logging"; -import { ResolveContextExpressionResponse } from "../../core/types"; +import { ResolveContextExpressionResult } from "../../core/types"; import { SourceControlApi } from "../client"; import { ROUTES } from "../routes"; @@ -22,7 +22,7 @@ export class ContextExpressionClient { public async resolve( document: vscode.TextDocument, payload: ResolveContextExpressionPayload - ): Promise { + ): Promise { const api = new AtelierAPI(document.uri); let sourceControlApi: SourceControlApi; @@ -36,7 +36,7 @@ export class ContextExpressionClient { const { requestTimeout } = getCcsSettings(); try { - const response = await sourceControlApi.post( + const response = await sourceControlApi.post( ROUTES.resolveContextExpression(), payload, { @@ -45,7 +45,11 @@ export class ContextExpressionClient { } ); - return response.data ?? {}; + if (typeof response.data === "undefined" || response.data === null) { + return {}; + } + + return response.data; } catch (error) { logDebug("Context expression resolution failed", error); throw error;