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
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,13 @@
{
"category": "Consistem",
"command": "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity",
"title": "Go to Name + Offset ^ Item"
"title": "Go to Definition (+Offset ^Item)"
},
{
"category": "Consistem",
"command": "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity.insertSelection",
"title": "Insert Selection (Quick Pick)",
"enablement": "inQuickOpen && vscode-objectscript.ccs.jumpToTagQuickPickActive"
},
{
"category": "ObjectScript",
Expand Down Expand Up @@ -1300,6 +1306,11 @@
"mac": "Cmd+Q",
"when": "editorTextFocus && editorLangId =~ /^objectscript/"
},
{
"command": "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity.insertSelection",
"key": "Tab",
"when": "inQuickOpen && vscode-objectscript.ccs.jumpToTagQuickPickActive"
},
{
"command": "vscode-objectscript.viewOthers",
"key": "Ctrl+Shift+V",
Expand Down
2 changes: 1 addition & 1 deletion src/ccs/commands/createItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function promptForItemName(options: PromptForItemNameOptions = {}): Promis
const hasBadChars = (s: string) => /[\\/]/.test(s) || /\s/.test(s);

const ib = vscode.window.createInputBox();
ib.title = "Consistem — Criar Item";
ib.title = "Criar Item";
ib.prompt = "Informe o nome da classe ou rotina a ser criada (.cls ou .mac)";
ib.placeholder = "MeuPacote.MinhaClasse.cls ou MINHAROTINA.mac";
ib.ignoreFocusOut = true;
Expand Down
248 changes: 213 additions & 35 deletions src/ccs/commands/jumpToTagOffsetCrossEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ const ROUTINE_NAME_PATTERN = new RegExp(`^${IDENTIFIER_START}${IDENTIFIER_BODY}*
const CLASS_METHOD_NAME_PATTERN = new RegExp(`^${IDENTIFIER_START}${IDENTIFIER_BODY}*$`);
const ROUTINE_LABEL_NAME_PATTERN = new RegExp(`^[A-Za-z0-9_%][A-Za-z0-9_%]*$`);

const JUMP_QP_CONTEXT_KEY = "vscode-objectscript.ccs.jumpToTagQuickPickActive";
const INSERT_SELECTION_COMMAND_ID = "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity.insertSelection";
const QUICK_PICK_OVERLAY_LINE_PADDING = 6;
const EXTRA_LINES_BELOW_QP = 2;

type EntityKind = "class" | "routine" | "unknown";

interface LocalNameInfo {
readonly line: number;
readonly originalName: string;
readonly selectionRange?: vscode.Range;
readonly blockRange?: vscode.Range;
}

type LocalNamesMap = Map<string, LocalNameInfo>;
Expand Down Expand Up @@ -77,7 +84,14 @@ export async function jumpToTagAndOffsetCrossEntity(): Promise<void> {
let pendingValidationError: string | undefined;

while (true) {
const parsed = await promptWithQuickPick(previousValue, pendingValidationError, localNames, docCtx);
const parsed = await promptWithQuickPick(
previousValue,
pendingValidationError,
localNames,
docCtx,
document,
editor
);
if (!parsed) return;

previousValue = parsed.input;
Expand All @@ -95,27 +109,130 @@ async function promptWithQuickPick(
previousValue: string | undefined,
initialValidationError: string | undefined,
localNames: LocalNamesMap,
docCtx: DocContext
docCtx: DocContext,
document: vscode.TextDocument,
editor: vscode.TextEditor
): Promise<ParseSuccess | undefined> {
// Remember where the user was before opening the QuickPick,
// so we can restore on ESC (cancel).
const originalSelection = editor.selection;
const originalVisible = editor.visibleRanges?.[0];
let wasAccepted = false;

const qp = vscode.window.createQuickPick<vscode.QuickPickItem>();
qp.title = "Consistem — Ir para Nome + Offset ^ Item";
qp.title = "Navegar para Definição (+Offset ^Item)";
qp.placeholder = docCtx.placeholder;
qp.ignoreFocusOut = true;
qp.matchOnDescription = true;
qp.matchOnDetail = true;
qp.canSelectMany = false;

const disposables: vscode.Disposable[] = [];
let cleanedUp = false;

const blockHighlightDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: new vscode.ThemeColor("editor.rangeHighlightBackground"),
isWholeLine: true,
});
disposables.push(blockHighlightDecoration);

const highlightDecoration = vscode.window.createTextEditorDecorationType({
borderColor: new vscode.ThemeColor("editor.selectionHighlightBorder"),
borderStyle: "solid",
borderWidth: "1px",
});
disposables.push(highlightDecoration);

let lastHighlightedRange: vscode.Range | undefined;
let lastHighlightedBlockRange: vscode.Range | undefined;

const clearHighlight = () => {
if (!lastHighlightedRange && !lastHighlightedBlockRange) return;
lastHighlightedRange = undefined;
lastHighlightedBlockRange = undefined;
editor.setDecorations(highlightDecoration, []);
editor.setDecorations(blockHighlightDecoration, []);
};

const highlightInfo = (info?: LocalNameInfo) => {
if (!info) {
clearHighlight();
return;
}

const range = info.selectionRange ?? document.lineAt(info.line).range;
const blockRange = info.blockRange ?? range;
lastHighlightedRange = range;
lastHighlightedBlockRange = blockRange;
editor.setDecorations(blockHighlightDecoration, [blockRange]);
editor.setDecorations(highlightDecoration, [range]);

// Keep highlighted block below the QuickPick overlay.
// We derive a dynamic padding from the current visible height,
// falling back to the fixed constant when needed.
const visible = editor.visibleRanges?.[0];
const visibleHeight = visible
? Math.max(0, visible.end.line - visible.start.line)
: QUICK_PICK_OVERLAY_LINE_PADDING * 3;
const dynamicGap = Math.floor(visibleHeight * 0.35);
const gap = Math.max(QUICK_PICK_OVERLAY_LINE_PADDING, dynamicGap) + EXTRA_LINES_BELOW_QP;

const revealStartLine = Math.max(blockRange.start.line - gap, 0);
const revealRangeStart = new vscode.Position(revealStartLine, 0);
const revealRange = new vscode.Range(revealRangeStart, blockRange.end);
editor.revealRange(revealRange, vscode.TextEditorRevealType.AtTop);
};

const updateHighlightFromItem = (item: vscode.QuickPickItem | undefined) => {
if (!item) {
clearHighlight();
return;
}

// Ignore tip item (first blank row)
if ((item as any).__isTipItem) {
clearHighlight();
return;
}
const info = localNames.get(item.label.toLowerCase());
highlightInfo(info);
};

const cleanup = () => {
if (cleanedUp) return;
cleanedUp = true;
while (disposables.length) {
const d = disposables.pop();
try {
d?.dispose();
} catch {
// Ignore dispose errors.
}
}
clearHighlight();
void vscode.commands.executeCommand("setContext", JUMP_QP_CONTEXT_KEY, false);
};

void vscode.commands.executeCommand("setContext", JUMP_QP_CONTEXT_KEY, true);

let lastParse: ParseSuccess | undefined;
let lastValidatedValue: string | undefined;
let currentValidationId = 0;
let lastValidationPromise: Promise<void> | undefined;

qp.value = previousValue ?? "";

const localItems: vscode.QuickPickItem[] = buildLocalItems(localNames);
const { items: localItems, tipItem } = buildLocalItems(localNames);
const setItems = () => (qp.items = localItems);
setItems();

try {
(qp as any).activeItems = [tipItem];
(qp as any).selectedItems = [];
} catch {
/* ignore */
}

if (initialValidationError) {
vscode.window.showErrorMessage(initialValidationError);
} else if (qp.value.trim() !== "") {
Expand Down Expand Up @@ -152,10 +269,51 @@ async function promptWithQuickPick(
return p;
}

const applySelectedItemToValue = ({ revalidate }: { revalidate?: boolean } = {}): boolean => {
const picked = qp.selectedItems[0] ?? qp.activeItems[0];
if (!picked) return false;

if ((picked as any).__isTipItem) return false;

const trimmed = qp.value.trim();
const normalized = replaceNameInExpression(trimmed, picked.label);
if (normalized === qp.value) return false;

qp.value = normalized;

try {
(qp as any).selectedItems = [];
} catch {
// Ignore errors from manipulating QuickPick internals.
}

if (revalidate && qp.value.trim() !== "") {
void runValidation(qp.value, localNames, docCtx, false);
}

return true;
};

const insertSelectionDisposable = vscode.commands.registerCommand(INSERT_SELECTION_COMMAND_ID, () => {
applySelectedItemToValue({ revalidate: true });
});
disposables.push(insertSelectionDisposable);

const changeActiveDisposable = qp.onDidChangeActive((items) => {
updateHighlightFromItem(items[0]);
});
disposables.push(changeActiveDisposable);

const changeSelectionDisposable = qp.onDidChangeSelection((items) => {
updateHighlightFromItem(items[0]);
});
disposables.push(changeSelectionDisposable);

qp.onDidChangeValue((value) => {
if (value.trim() === "") {
lastParse = undefined;
lastValidatedValue = undefined;
clearHighlight();
return;
}

Expand All @@ -164,24 +322,9 @@ async function promptWithQuickPick(

const accepted = new Promise<ParseSuccess | undefined>((resolve) => {
qp.onDidAccept(async () => {
const trimmed = qp.value.trim();

if (qp.selectedItems.length) {
const picked = qp.selectedItems[0];
const normalized = replaceNameInExpression(trimmed, picked.label);
if (normalized !== trimmed) {
qp.value = normalized;

try {
(qp as any).selectedItems = [];
} catch {
// Ignore errors from manipulating QuickPick internals.
}
applySelectedItemToValue();

if (qp.value.trim() !== "") void runValidation(qp.value, localNames, docCtx, false);
return;
}
}
const trimmed = qp.value.trim();

if (trimmed === "") {
vscode.window.showErrorMessage(ERR_NAME_REQUIRED);
Expand All @@ -200,39 +343,69 @@ async function promptWithQuickPick(
if (!lastParse) return;

resolve(lastParse);
qp.hide();
wasAccepted = true;
cleanup();
qp.dispose();
});

qp.onDidHide(() => {
// If user cancelled (ESC), restore cursor and viewport.
if (!wasAccepted) {
try {
editor.selection = originalSelection;
if (originalVisible) {
// Use Default so VS Code restores without forcing center/top.
editor.revealRange(originalVisible, vscode.TextEditorRevealType.Default);
}
} catch {
/* ignore */
}
}
resolve(undefined);
qp.dispose();
cleanup();
});
});

qp.show();
return accepted;
}

function buildLocalItems(localNames: LocalNamesMap): vscode.QuickPickItem[] {
function buildLocalItems(localNames: LocalNamesMap): {
items: vscode.QuickPickItem[];
tipItem: vscode.QuickPickItem;
} {
const tipItem: vscode.QuickPickItem = {
label: "",
description: "Tab ↹ Inserir • Enter ↩ Navegar",
detail: "",
alwaysShow: true,
} as vscode.QuickPickItem;

(tipItem as any).__isTipItem = true;

if (!localNames.size) {
return [
{
label: "Nenhum nome local encontrado",
description: "—",
detail: "Defina métodos/labels no arquivo atual para listá-los aqui.",
alwaysShow: true,
},
];
return {
tipItem,
items: [
tipItem,
{
label: "Nenhum nome local encontrado",
description: "—",
detail: "Defina métodos/labels no arquivo atual para listá-los aqui.",
alwaysShow: true,
},
],
};
}

return [...localNames.values()]
const items = [...localNames.values()]
.sort((a, b) => a.line - b.line || a.originalName.localeCompare(b.originalName))
.map((info) => ({
label: info.originalName,
description: "definição local",
detail: `linha ${info.line + 1}`,
}));

return { tipItem, items: [tipItem, ...items] };
}

/** Replaces only the "name" portion in the expression, preserving +offset and ^item. */
Expand Down Expand Up @@ -340,7 +513,12 @@ async function collectLocalNames(document: vscode.TextDocument): Promise<LocalNa
const line = symbol.selectionRange?.start.line ?? symbol.range.start.line;
const key = symbol.name.toLowerCase();
if (!map.has(key)) {
map.set(key, { line, originalName: symbol.name });
map.set(key, {
line,
originalName: symbol.name,
selectionRange: symbol.selectionRange ?? symbol.range,
blockRange: symbol.range,
});
}
}
if (symbol.children?.length) pending.push(...symbol.children);
Expand Down