Skip to content

Commit

Permalink
Add hints to update sync scope when a test file is out of scope (#26)
Browse files Browse the repository at this point in the history
This change adds additional guidance when users have synced their test
cases, but have then navigated to a file that is outside of their Bazel
sync scope. Instead of a lack of test run arrows with no clear reason
why, they will now see a greyed out run arrow with further information.

- Open a file that's outside of the current scope.
- Logic from #24 attempts to expand this file's target and add its test
cases to the test explorer.
- With this PR, if the step above fails, decorators will now be added in
the gutter to help provide further guidance, at the locations where the
run arrows would normally appear.
-
![image](https://github.com/user-attachments/assets/ad914118-4ea4-486f-93d3-595729f236b4)
- Adjust Project Scope will open the .bazelproject file for editing, and
sync now triggers the test explorer's existing sync functionality.
- This functionality is behind the `bazelbsp.autoExpandTarget` setting
as it is part of that logic.
  • Loading branch information
mnoah1 authored Oct 30, 2024
1 parent fa3032e commit ab3d648
Show file tree
Hide file tree
Showing 10 changed files with 653 additions and 47 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"command": "bazelbsp.showServerOutput",
"title": "Bazel BSP: Show Server Output Channel",
"icon": "$(output)"
},
{
"command": "bazelbsp.openProjectView",
"title": "Bazel BSP: Open Project View File"
}
],
"configuration": {
Expand Down
4 changes: 4 additions & 0 deletions resources/gutter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {BazelBSPInstaller} from './server/install'
import {TestItemFactory} from './test-info/test-item-factory'
import {CoverageTracker} from './coverage-utils/coverage-tracker'
import {LanguageToolManager} from './language-tools/manager'
import {SyncHintDecorationsManager} from './test-explorer/decorator'

export async function bootstrap(context: vscode.ExtensionContext) {
// Define the application's dependencies. This is done at runtime to allow for dynamically created providers such as extension context.
Expand All @@ -38,6 +39,7 @@ export async function bootstrap(context: vscode.ExtensionContext) {
TestItemFactory,
CoverageTracker,
LanguageToolManager,
SyncHintDecorationsManager,
testControllerProvider,
],
})
Expand Down
138 changes: 138 additions & 0 deletions src/test-explorer/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as vscode from 'vscode'
import {Inject, Injectable, OnModuleInit} from '@nestjs/common'
import {EXTENSION_CONTEXT_TOKEN} from '../custom-providers'
import {TestCaseStore} from './store'
import {LanguageToolManager, TestFileContents} from '../language-tools/manager'

/**
* This will allow dummy run arrows to be applied to a document, to provide indication of out of scope files.
* The resolver can use this class to enable or disable these decorators in a given file.
*/
@Injectable()
export class SyncHintDecorationsManager implements OnModuleInit {
@Inject(EXTENSION_CONTEXT_TOKEN) private readonly ctx: vscode.ExtensionContext
@Inject(TestCaseStore) private readonly store: TestCaseStore
@Inject(LanguageToolManager)
private readonly languageToolManager: LanguageToolManager

private activeFiles = new Map<string, vscode.Disposable>()
private hoverMessage: vscode.MarkdownString
private decorationType: vscode.TextEditorDecorationType

onModuleInit() {
this.ctx.subscriptions.push(
// Access to the Project View file for use in the markdown command on hover.
vscode.commands.registerCommand('bazelbsp.openProjectView', async () => {
const uri = this.store.testController.items.get('root')?.uri
if (uri) {
const document = await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(document)
}
})
)

// Set up the decorator type and hover message.
this.decorationType = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.file(
this.ctx.asAbsolutePath('resources/gutter.svg')
),
isWholeLine: true,
gutterIconSize: 'contain',
})
this.hoverMessage = new vscode.MarkdownString(
'**Test Explorer**\n\nTests in this file are not yet synced.\n\n- [Adjust Project Scope](command:bazelbsp.openProjectView)\n\n- [Sync Now](command:testing.refreshTests)\n\n'
)
this.hoverMessage.isTrusted = true
}

/**
* Enable decorators for a given file.
* @param uri file on which to enable sync hint decorators.
* @param repoRoot portion of the path representing this file's repo root.
* @param docInfo existing processed test file contents for initial decorator positions.
*/
async enable(uri: vscode.Uri, repoRoot: string, docInfo: TestFileContents) {
const editor = vscode.window.visibleTextEditors.find(
editor => editor.document.uri.toString() === uri.toString()
)
if (editor) this.setDecorationRanges(editor, docInfo)

this.ensureWatcher(uri, repoRoot)
}

/**
* Stop applying decorators for a given file, and clear if visible.
* @param uri file on which decorators will be removed.
*/
async disable(uri: vscode.Uri) {
const watcher = this.activeFiles.get(uri.fsPath)
if (watcher) {
watcher.dispose()
this.activeFiles.delete(uri.fsPath)

const editor = vscode.window.visibleTextEditors.find(
editor => editor.document.uri.toString() === uri.toString()
)
if (editor) this.setDecorationRanges(editor, null)
}
}

/**
* Determine current test case positions then apply the decorator.
* @param editor text document to be updated.
* @param repoRoot repo root to be used when getting
*/
private async refreshDecoratorPositions(
editor: vscode.TextEditor,
repoRoot: string
) {
const testFileContents = await this.languageToolManager
.getLanguageToolsForFile(editor.document)
.getDocumentTestCases(editor.document.uri, repoRoot)
this.setDecorationRanges(editor, testFileContents)
}

/**
* Adds a watcher for this file, reapplying the decorators each file the file is shown.
* @param uri text document to be updated.
* @param repoRoot portion of the path representing this file's repo root.
*/
private ensureWatcher(uri: vscode.Uri, repoRoot: string) {
const existing = this.activeFiles.get(uri.fsPath)
if (existing) {
existing.dispose()
}

const watcher = vscode.window.onDidChangeActiveTextEditor(async editor => {
if (editor?.document.uri === uri) {
await this.refreshDecoratorPositions(editor, repoRoot)
}
})

this.activeFiles.set(uri.fsPath, watcher)
}

/**
* Apply decorators to the given editor based on the provided TestFileContents.
* @param editor editor to be updated.
* @param docInfo processed test file information indication expected positions for documents. null to clear current contents.
*/
private setDecorationRanges(
editor: vscode.TextEditor,
docInfo: TestFileContents | null
) {
let ranges: vscode.Range[] = []
if (docInfo) {
ranges = docInfo.testCases.map(test => test.range)
}

const decorations: vscode.DecorationOptions[] = []
for (const range of ranges) {
decorations.push({
range: new vscode.Range(range.start, range.start), // first line of the test only
hoverMessage: this.hoverMessage,
})
}
editor.setDecorations(this.decorationType, decorations)
}
}
34 changes: 23 additions & 11 deletions src/test-explorer/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {getExtensionSetting, SettingName} from '../utils/settings'
import {Utils} from '../utils/utils'
import {TestItemFactory} from '../test-info/test-item-factory'
import {DocumentTestItem, LanguageToolManager} from '../language-tools/manager'
import {SyncHintDecorationsManager} from './decorator'

@Injectable()
export class TestResolver implements OnModuleInit, vscode.Disposable {
Expand All @@ -31,7 +32,10 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
private readonly languageToolManager: LanguageToolManager
@Inject(PRIMARY_OUTPUT_CHANNEL_TOKEN)
private readonly outputChannel: vscode.OutputChannel
@Inject(SyncHintDecorationsManager)
private readonly syncHint: SyncHintDecorationsManager
private repoRoot: string | null
private openDocumentWatcherEnabled = false

onModuleInit() {
this.ctx.subscriptions.push(this)
Expand Down Expand Up @@ -188,6 +192,7 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
const buildFileName = getExtensionSetting(SettingName.BUILD_FILE_NAME)
parentTest.children.replace([])
this.store.clearTargetIdentifiers()
this.store.knownFiles.clear()

result.targets.forEach(target => {
if (!target.capabilities.canTest) return
Expand Down Expand Up @@ -256,6 +261,8 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
await this.expandTargetsForDocument(doc)
}

if (this.openDocumentWatcherEnabled) return
this.openDocumentWatcherEnabled = true
this.ctx.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(async doc => {
// Discovery within newly opened documents.
Expand Down Expand Up @@ -296,30 +303,34 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
cancellable: true,
},
async (progress, token) => {
result = await conn.sendRequest(
bsp.BuildTargetInverseSources.type,
params,
token
)
try {
result = await conn.sendRequest(
bsp.BuildTargetInverseSources.type,
params,
token
)
} catch (e) {
result = undefined
}
}
)

if (!result) {
// TODO(IDE-1203): Add more guidance to update their sync scope.
this.outputChannel.appendLine(`Target not in scope for ${doc.fileName}.`)
this.outputChannel.appendLine(
`Unable to determine target for ${doc.fileName}.`
)
return
}

for (const target of result.targets) {
// Put this file under the first matching target, in the rare event that a test is part of multiple targets.
const targetItem = this.store.getTargetIdentifier(target)
if (targetItem) {
await this.resolveHandler(targetItem)
} else {
this.outputChannel.appendLine(
`Couldn't find a matching test item for ${target.uri}.`
)
return
}
}
this.syncHint.enable(doc.uri, this.repoRoot ?? '', docInfo)
}

/**
Expand Down Expand Up @@ -377,6 +388,7 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
source
)
this.store.knownFiles.add(source.uri)
this.syncHint.disable(newTest.uri!)

relevantParent.children.add(newTest)
allDocumentTestItems.push(newTest)
Expand Down
Loading

0 comments on commit ab3d648

Please sign in to comment.