Skip to content

Commit c2f78e9

Browse files
authored
Merge pull request #32 from ketupia/23-navigation-from-code-interface-to-action
feat: enhance navigation with cross-reference code lenses
2 parents 67a834f + e136aac commit c2f78e9

14 files changed

Lines changed: 375 additions & 76 deletions

.github/copilot-instructions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@
7979
- Function signatures should make the purpose and usage clear without requiring knowledge of
8080
internal algorithms or data structures.
8181

82+
- **Favor pipeline-style processing with helper functions.**
83+
- When implementing logic that involves filtering, mapping, or transforming collections, prefer a
84+
pipeline of array methods (`filter`, `map`, `flatMap`, etc.) with small, well-named helper
85+
functions for each transformation step.
86+
- This style should resemble Elixir's `Enum` pipelines: break up complex logic into a series of
87+
focused, composable steps.
88+
- Avoid deeply nested loops or imperative code when a pipeline with helpers would be clearer.
89+
- Use early returns and guard clauses in helpers to keep each step focused and readable.
90+
- Example: Instead of a large nested loop, use chained array methods and extract the innermost
91+
logic into a named function.
92+
8293
## Modularization & Interface-Driven Architecture
8394

8495
- All logic should be organized into small, well-defined modules with clear responsibilities.

src/ashParserService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class AshParserService {
5252
`Parser ${result.parserName} succeeded`,
5353
{
5454
sectionsFound: result.sections.length,
55-
codeLensesFound: result.codeLenses.length,
55+
diagramCodeLensesFound: result.diagramCodeLenses.length,
5656
}
5757
);
5858
} catch (error) {
@@ -62,8 +62,9 @@ export class AshParserService {
6262
// Fallback to an empty result on error
6363
result = {
6464
sections: [],
65-
parserName: "ErrorFallback",
66-
codeLenses: [],
65+
parserName: "",
66+
diagramCodeLenses: [],
67+
crossReferenceCodeLenses: [],
6768
};
6869
}
6970

@@ -113,7 +114,8 @@ export class AshParserService {
113114
return {
114115
sections: [],
115116
parserName: "LanguageFilter",
116-
codeLenses: [],
117+
diagramCodeLenses: [],
118+
crossReferenceCodeLenses: [],
117119
};
118120
}
119121

src/configurations/Ash.Resource.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ const Ash_Resource_Config: ModuleInterface = {
101101
{
102102
keyword: "define",
103103
namePattern: "(:\\w+|\\w+)",
104+
crossReference: {
105+
blockName: "actions",
106+
},
104107
},
105108
],
106109
},

src/extension.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
generateDiagramWebviewContent,
1010
getOrCreateAshStudioWebview,
1111
} from "./features/ashStudioWebview";
12-
import { CodeLensEntry } from "./types/parser";
1312
import { getTheoreticalDiagramFilePath } from "./utils/diagramUtils";
1413
import { generateDiagramWithMix } from "./utils/diagramMixUtils";
14+
import { DiagramCodeLensEntry } from "./types/parser";
15+
import { GotoFileLocationEntry } from "./types/commands";
1516

1617
// Debounce map for text change events to prevent excessive parsing
1718
const debounceTimers = new Map<string, NodeJS.Timeout>();
@@ -105,22 +106,6 @@ export function activate(context: vscode.ExtensionContext) {
105106
registerAshSectionNavigation(context, parserService);
106107
registerAshCodeLensProvider(context, parserService);
107108

108-
// Register the reveal command that the sidebar uses
109-
context.subscriptions.push(
110-
vscode.commands.registerCommand(
111-
"ash-studio.revealSectionOrSubBlock",
112-
(line: number) => {
113-
const editor = vscode.window.activeTextEditor;
114-
if (editor && typeof line === "number") {
115-
const position = new vscode.Position(line, 0);
116-
editor.selection = new vscode.Selection(position, position);
117-
editor.revealRange(new vscode.Range(position, position));
118-
vscode.window.showTextDocument(editor.document);
119-
}
120-
}
121-
)
122-
);
123-
124109
// Register diagram commands
125110
context.subscriptions.push(
126111
vscode.commands.registerCommand(
@@ -133,7 +118,7 @@ export function activate(context: vscode.ExtensionContext) {
133118
* @param entry - The CodeLensEntry containing diagram metadata and resource info
134119
*/
135120
"ash-studio.showDiagram",
136-
async (filePath: string, entry: CodeLensEntry) => {
121+
async (filePath: string, entry: DiagramCodeLensEntry) => {
137122
const diagramFilePath = getTheoreticalDiagramFilePath(
138123
entry.target,
139124
entry.diagramSpec
@@ -168,6 +153,33 @@ export function activate(context: vscode.ExtensionContext) {
168153
)
169154
);
170155

156+
// Register generic file location navigation command
157+
context.subscriptions.push(
158+
vscode.commands.registerCommand(
159+
/**
160+
* Registers the ash-studio.gotoFileLocation command to reveal a file and line in the editor.
161+
* Used by all navigation features (code lenses, QuickPick, sidebar, etc.) for unified navigation.
162+
*
163+
* @param filePath - The file path to open
164+
* @param entry - An object containing targetLine or line (1-based)
165+
*/
166+
"ash-studio.gotoFileLocation",
167+
async (filePath: string, entry: GotoFileLocationEntry) => {
168+
const doc = await vscode.workspace.openTextDocument(filePath);
169+
const editor = await vscode.window.showTextDocument(doc, {
170+
preview: false,
171+
});
172+
const line = Math.max(0, (entry.targetLine ?? entry.line ?? 1) - 1); // 0-based
173+
const pos = new vscode.Position(line, 0);
174+
editor.selection = new vscode.Selection(pos, pos);
175+
editor.revealRange(
176+
new vscode.Range(pos, pos),
177+
vscode.TextEditorRevealType.InCenter
178+
);
179+
}
180+
)
181+
);
182+
171183
logger.info("Extension", "...complete");
172184
} catch (error) {
173185
logger.error("Extension", "Extension activation failed", error);

src/features/ashCodeLensProvider.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as vscode from "vscode";
22
import { AshParserService } from "../ashParserService";
3-
import { Logger } from "../utils/logger";
43
import { ConfigurationManager } from "../utils/config";
54

65
/**
@@ -38,30 +37,20 @@ export class AshCodeLensProvider implements vscode.CodeLensProvider {
3837
parseResult = this.parserService.parseElixirDocument(document);
3938
}
4039

41-
if (!parseResult || parseResult.codeLenses.length === 0) {
40+
if (!parseResult) {
4241
return [];
4342
}
4443

4544
// Convert parser results to VS Code CodeLens objects
4645
const codeLenses: vscode.CodeLens[] = [];
47-
for (const entry of parseResult.codeLenses) {
48-
// Create a range for the CodeLens
46+
// Handle diagram code lenses
47+
for (const entry of parseResult.diagramCodeLenses || []) {
4948
const line = Math.max(0, entry.line - 1); // Convert to 0-based line number
5049
const range = new vscode.Range(
5150
new vscode.Position(line, entry.character),
5251
new vscode.Position(line, entry.character + 1)
5352
);
54-
55-
// Create the CodeLens with a command that opens the diagram
5653
const lens = new vscode.CodeLens(range);
57-
58-
// Debug logging
59-
Logger.getInstance().debug(
60-
"AshCodeLensProvider",
61-
`Creating code lens: ${entry.title} -> ${entry.target}`
62-
);
63-
64-
// Assign command directly from entry
6554
if (entry.command === "ash-studio.showDiagram") {
6655
lens.command = {
6756
title: entry.title,
@@ -70,14 +59,26 @@ export class AshCodeLensProvider implements vscode.CodeLensProvider {
7059
tooltip: `View diagram for ${entry.source}`,
7160
};
7261
} else {
73-
const logger = Logger.getInstance();
74-
logger.error("Code Lens Provider", `Unknown Command ${entry.command}`);
7562
vscode.window.showErrorMessage(`Unknown Command ${entry.command}`);
7663
}
77-
7864
codeLenses.push(lens);
7965
}
80-
66+
// Handle cross-reference code lenses
67+
for (const entry of parseResult.crossReferenceCodeLenses || []) {
68+
const line = Math.max(0, entry.line - 1); // Convert to 0-based line number
69+
const range = new vscode.Range(
70+
new vscode.Position(line, entry.character),
71+
new vscode.Position(line, entry.character + 1)
72+
);
73+
const lens = new vscode.CodeLens(range);
74+
lens.command = {
75+
title: "➡️ " + entry.title,
76+
command: "ash-studio.gotoFileLocation",
77+
arguments: [document.uri.fsPath, entry],
78+
tooltip: `Go to referenced section/detail (line ${entry.targetLine})`,
79+
};
80+
codeLenses.push(lens);
81+
}
8182
return codeLenses;
8283
}
8384

src/features/ashQuickPick.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,11 @@ export function registerAshQuickPick(
4646
});
4747

4848
if (pick && pick.section) {
49-
const position = new vscode.Position(pick.section.startLine - 1, 0);
50-
activeEditor.revealRange(
51-
new vscode.Range(position, position),
52-
vscode.TextEditorRevealType.InCenter
49+
await vscode.commands.executeCommand(
50+
"ash-studio.gotoFileLocation",
51+
activeEditor.document.uri.fsPath,
52+
{ line: pick.section.startLine }
5353
);
54-
activeEditor.selection = new vscode.Selection(position, position);
5554
}
5655
}
5756
);

src/features/ashSidebarProvider.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class AshSidebarProvider
3636
);
3737
return [];
3838
}
39+
const filePath = activeEditor.document.uri.fsPath;
3940

4041
// Get parse result for the active document
4142
let parseResult = this.parserService.getCachedResult(activeEditor.document);
@@ -65,9 +66,9 @@ export class AshSidebarProvider
6566
section.startLine, // Use startLine instead of line
6667
undefined,
6768
{
68-
command: "ash-studio.revealSectionOrSubBlock",
69+
command: "ash-studio.gotoFileLocation",
6970
title: "Go to Section",
70-
arguments: [section.startLine],
71+
arguments: [filePath, { targetLine: section.startLine }],
7172
},
7273
undefined,
7374
true // Mark as section
@@ -82,7 +83,7 @@ export class AshSidebarProvider
8283
return [];
8384

8485
return section.details.map((detail: ParsedDetail) =>
85-
this.createDetailTreeItem(detail)
86+
this.createDetailTreeItem(detail, filePath)
8687
);
8788
} else if (
8889
element.detail &&
@@ -91,7 +92,7 @@ export class AshSidebarProvider
9192
) {
9293
// Level 2+: Show nested details (recursive handling)
9394
return element.detail.childDetails.map((childDetail: ParsedDetail) =>
94-
this.createDetailTreeItem(childDetail)
95+
this.createDetailTreeItem(childDetail, filePath)
9596
);
9697
}
9798

@@ -101,7 +102,10 @@ export class AshSidebarProvider
101102
/**
102103
* Helper method to create a tree item for a detail, handling nested details recursively
103104
*/
104-
private createDetailTreeItem(detail: ParsedDetail): AshSidebarItem {
105+
private createDetailTreeItem(
106+
detail: ParsedDetail,
107+
filePath?: string
108+
): AshSidebarItem {
105109
const hasChildren = detail.childDetails && detail.childDetails.length > 0;
106110

107111
// Create a label that shows both block type and name (if available)
@@ -117,11 +121,13 @@ export class AshSidebarProvider
117121
: vscode.TreeItemCollapsibleState.None,
118122
detail.line,
119123
undefined, // Remove parent section display
120-
{
121-
command: "ash-studio.revealSectionOrSubBlock",
122-
title: "Go to Detail",
123-
arguments: [detail.line],
124-
},
124+
filePath
125+
? {
126+
command: "ash-studio.gotoFileLocation",
127+
title: "Go to Detail",
128+
arguments: [filePath, { targetLine: detail.line }],
129+
}
130+
: undefined,
125131
detail, // Pass the detail for recursive nesting
126132
false // Not a section
127133
);
@@ -148,13 +154,7 @@ class AshSidebarItem extends vscode.TreeItem {
148154
isSection: boolean = false
149155
) {
150156
super(label, collapsibleState);
151-
if (sectionLine !== undefined) {
152-
this.command = {
153-
command: "ash-studio.revealSectionOrSubBlock",
154-
title: "Go to block",
155-
arguments: [sectionLine],
156-
};
157-
}
157+
// Remove legacy command assignment here; now passed in explicitly
158158
if (parentSection) {
159159
this.description = parentSection;
160160
}

0 commit comments

Comments
 (0)