Skip to content

Commit 9b5b981

Browse files
Merge branch 'main' into fix-wrong-root
2 parents 8a8b420 + bf28619 commit 9b5b981

9 files changed

Lines changed: 466 additions & 0 deletions

File tree

.github/labeler.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
docs:
2+
- all:
3+
- changed-files:
4+
- any-glob-to-any-file:
5+
- README.md
6+
- SUPPORT.md
7+
- all-globs-to-all-files:
8+
- '!src/**'
9+
10+
internal:
11+
- all:
12+
- changed-files:
13+
- any-glob-to-any-file:
14+
- .github/**
15+
- .vscode/**
16+
- .husky/**
17+
- .gitignore
18+
- .vscode-test.mjs
19+
- .vscodeignore
20+
- biome.json
21+
- bun.lock
22+
- esbuild.js
23+
- tsconfig.json
24+
25+
- all-globs-to-all-files:
26+
- '!src/**'

.github/workflows/labeler.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Labels
2+
on:
3+
pull_request_target:
4+
types:
5+
- opened
6+
- synchronize
7+
- reopened
8+
# For label-checker
9+
- labeled
10+
- unlabeled
11+
12+
jobs:
13+
labeler:
14+
permissions:
15+
contents: read
16+
pull-requests: write
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/labeler@v6
20+
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
21+
- run: echo "Done adding labels"
22+
# Run this after labeler applied labels
23+
check-labels:
24+
needs:
25+
- labeler
26+
permissions:
27+
pull-requests: read
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: docker://agilepathway/pull-request-label-checker:latest
31+
with:
32+
one_of: breaking,security,feature,bug,refactor,upgrade,docs,internal
33+
repo_token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Latest Changes
2+
3+
on:
4+
pull_request_target:
5+
branches:
6+
- main
7+
types:
8+
- closed
9+
workflow_dispatch:
10+
inputs:
11+
number:
12+
description: PR number
13+
required: true
14+
15+
jobs:
16+
latest-changes:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Dump GitHub context
20+
env:
21+
GITHUB_CONTEXT: ${{ toJson(github) }}
22+
run: echo "$GITHUB_CONTEXT"
23+
- uses: actions/checkout@v6
24+
with:
25+
# To allow latest-changes to commit to the main branch
26+
token: ${{ secrets.FASTAPI_VSCODE_LATEST_CHANGES }}
27+
- uses: tiangolo/latest-changes@0.4.1
28+
with:
29+
token: ${{ secrets.GITHUB_TOKEN }}
30+
latest_changes_file: release-notes.md
31+
latest_changes_header: '# Latest Changes'
32+
end_regex: '^## '
33+
debug_logs: true
34+
label_header_prefix: '## '

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@
113113
"type": "string",
114114
"default": "",
115115
"description": "Path to the main FastAPI application file (e.g., 'src/main.py'). If not set, the extension will search common locations."
116+
},
117+
"fastapi.showTestCodeLenses": {
118+
"type": "boolean",
119+
"default": true,
120+
"description": "Show CodeLens links above test client calls (e.g., client.get('/items')) to navigate to the corresponding route definition."
116121
}
117122
}
118123
}

release-notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Release Notes
2+
3+
## Latest Changes

src/core/pathUtils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,35 @@ export function getPathSegments(path: string, count: number): string {
7777
export function countSegments(path: string): number {
7878
return path.split("/").filter(Boolean).length
7979
}
80+
81+
/**
82+
* Checks if a test path matches an endpoint path pattern.
83+
* Endpoint paths may contain path parameters like {item_id} which match any segment.
84+
*
85+
* Examples:
86+
* pathMatchesEndpoint("/items/123", "/items/{item_id}") -> true
87+
* pathMatchesEndpoint("/items/123/details", "/items/{item_id}") -> false
88+
* pathMatchesEndpoint("/users/abc/posts/456", "/users/{user_id}/posts/{post_id}") -> true
89+
* pathMatchesEndpoint("/items/", "/items/{item_id}") -> false
90+
*/
91+
export function pathMatchesEndpoint(
92+
testPath: string,
93+
endpointPath: string,
94+
): boolean {
95+
const testSegments = testPath.split("/").filter(Boolean)
96+
const endpointSegments = endpointPath.split("/").filter(Boolean)
97+
98+
// Segment counts must match
99+
if (testSegments.length !== endpointSegments.length) {
100+
return false
101+
}
102+
103+
return endpointSegments.every((seg, index) => {
104+
// Path parameter (e.g., {item_id}) matches any segment
105+
if (seg.startsWith("{") && seg.endsWith("}")) {
106+
return true
107+
}
108+
// Literal segments must match exactly
109+
return seg === testSegments[index]
110+
})
111+
}

src/extension.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type EndpointTreeItem,
1616
EndpointTreeProvider,
1717
} from "./providers/EndpointTreeProvider"
18+
import { TestCodeLensProvider } from "./providers/TestCodeLensProvider"
1819

1920
async function discoverFastAPIApps(parser: Parser): Promise<AppDefinition[]> {
2021
const apps: AppDefinition[] = []
@@ -102,11 +103,48 @@ export async function activate(context: vscode.ExtensionContext) {
102103
// Discover FastAPI endpoints from workspace
103104
const apps = await discoverFastAPIApps(parserService)
104105
const endpointProvider = new EndpointTreeProvider(apps)
106+
const codeLensProvider = new TestCodeLensProvider(parserService, apps)
107+
108+
let refreshTimeout: NodeJS.Timeout | null = null
109+
110+
const triggerRefresh = () => {
111+
if (refreshTimeout) {
112+
clearTimeout(refreshTimeout)
113+
}
114+
refreshTimeout = setTimeout(async () => {
115+
if (!parserService) {
116+
return
117+
}
118+
const newApps = await discoverFastAPIApps(parserService)
119+
endpointProvider.setApps(newApps)
120+
codeLensProvider.setApps(newApps)
121+
}, 500)
122+
}
123+
124+
// Watch for changes in Python files to refresh endpoints
125+
const watcher = vscode.workspace.createFileSystemWatcher("**/*.py")
126+
watcher.onDidChange(triggerRefresh)
127+
watcher.onDidCreate(triggerRefresh)
128+
watcher.onDidDelete(triggerRefresh)
129+
context.subscriptions.push(watcher)
105130

106131
const treeView = vscode.window.createTreeView("endpoint-explorer", {
107132
treeDataProvider: endpointProvider,
108133
})
109134

135+
// Register CodeLens provider for test files
136+
const config = vscode.workspace.getConfiguration("fastapi")
137+
if (config.get<boolean>("showTestCodeLenses", true)) {
138+
context.subscriptions.push(
139+
vscode.languages.registerCodeLensProvider(
140+
// Covers common test file patterns
141+
// e.g., test_*.py, *_test.py, tests/*.py
142+
{ language: "python", pattern: "**/*test*.py" },
143+
codeLensProvider,
144+
),
145+
)
146+
}
147+
110148
context.subscriptions.push(
111149
treeView,
112150

@@ -119,6 +157,7 @@ export async function activate(context: vscode.ExtensionContext) {
119157
clearImportCache()
120158
const newApps = await discoverFastAPIApps(parserService)
121159
endpointProvider.setApps(newApps)
160+
codeLensProvider.setApps(newApps)
122161
},
123162
),
124163

@@ -162,6 +201,24 @@ export async function activate(context: vscode.ExtensionContext) {
162201
vscode.commands.registerCommand("fastapi-vscode.toggleRouters", () => {
163202
endpointProvider.toggleRouters()
164203
}),
204+
205+
vscode.commands.registerCommand(
206+
"fastapi-vscode.goToDefinition",
207+
(
208+
locations: vscode.Location[],
209+
fromUri: vscode.Uri,
210+
fromPosition: vscode.Position,
211+
) => {
212+
vscode.commands.executeCommand(
213+
"editor.action.goToLocations",
214+
fromUri,
215+
fromPosition,
216+
locations,
217+
locations.length === 1 ? "goto" : "peek",
218+
"No matching route found",
219+
)
220+
},
221+
),
165222
)
166223
}
167224

0 commit comments

Comments
 (0)