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
217 changes: 197 additions & 20 deletions src/ccs/commands/contextHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const editor = vscode.window.activeTextEditor;
Expand All @@ -28,45 +31,69 @@ export async function resolveContextExpression(): Promise<void> {
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;

Comment on lines +62 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ensure --gif context expressions still replace text

The new guard if (!hasGifCommand) means the editor only performs a replacement when the context expression does not include --gif. With --gif present the command now skips editor.edit entirely and only shows the GIF, leaving the original context expression untouched. In the previous version the text was replaced for GIF responses as well, so this change regresses the command’s primary output. Users invoking --gif now see a webview but no text insertion, which likely wasn’t intended for a substitution-style response.

Useful? React with 👍 / 👎.

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);
} catch (error) {
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.");
}
Expand Down Expand Up @@ -132,6 +159,156 @@ function extractGifUri(text: string): {
return { textWithoutGifUri: processedLines.join("\n"), gifUri };
}

async function handleContextHelpDocumentationContent(rawContent: string): Promise<void> {
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<void> {
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 `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${cspSource} 'unsafe-inline';" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapedTitle}</title>
<style>
body {
margin: 0;
padding: 16px;
background-color: var(--vscode-editor-background, #1e1e1e);
color: var(--vscode-editor-foreground, #d4d4d4);
font-family: var(--vscode-editor-font-family, Consolas, 'Courier New', monospace);
font-size: var(--vscode-editor-font-size, 14px);
line-height: 1.5;
}

pre {
white-space: pre; /* em vez de pre-wrap */
word-break: normal; /* em vez de break-word */
overflow-x: auto; /* barra horizontal quando precisar */
overflow-y: auto; /* mantém a vertical também */
max-width: 100%;
}
</style>

</head>
<body>
<pre>${escapedContent}</pre>
</body>
</html>`;
}

function hasGlobalDocumentationContent(
value: unknown
): value is Pick<GlobalDocumentationResponse, "content" | "message"> {
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<Pick<ResolveContextExpressionResponse, "textExpression">> & 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<string, unknown> {
return typeof value === "object" && value !== null;
}

function getFileUriFromText(text: string): vscode.Uri | undefined {
const trimmed = text.trim();
if (!trimmed.toLowerCase().startsWith("file://")) {
Expand Down
12 changes: 7 additions & 5 deletions src/ccs/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ export interface LocationJSON {

export type ResolveDefinitionResponse = LocationJSON;

export interface GlobalDocumentationResponse {
content?: string | string[] | Record<string, unknown> | 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<string, unknown> | null;
message?: string;
}

export interface CreateItemResponse {
item?: Record<string, unknown>;
name?: string;
Expand Down
12 changes: 8 additions & 4 deletions src/ccs/sourcecontrol/clients/contextExpressionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

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

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

try {
const response = await sourceControlApi.post<ResolveContextExpressionResponse>(
const response = await sourceControlApi.post<ResolveContextExpressionResult>(
ROUTES.resolveContextExpression(),
payload,
{
Expand All @@ -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;
Expand Down