Skip to content

Commit 320e9c5

Browse files
authored
Focus pipeline when dvc.yaml file is open in the active editor (#4273)
1 parent 99a30f4 commit 320e9c5

File tree

8 files changed

+194
-18
lines changed

8 files changed

+194
-18
lines changed

extension/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,8 @@
420420
{
421421
"title": "Show Pipeline DAG",
422422
"command": "dvc.showPipelineDAG",
423-
"category": "DVC"
423+
"category": "DVC",
424+
"icon": "$(symbol-class)"
424425
},
425426
{
426427
"title": "Show Plots",
@@ -1094,6 +1095,11 @@
10941095
"group": "navigation@0",
10951096
"when": "dvc.experiments.webview.active && dvc.experiment.running && dvc.commands.available"
10961097
},
1098+
{
1099+
"command": "dvc.showPipelineDAG",
1100+
"group": "navigation@0",
1101+
"when": "dvc.pipeline.file.active && dvc.commands.available && dvc.project.available"
1102+
},
10971103
{
10981104
"command": "dvc.runExperiment",
10991105
"group": "navigation@1",

extension/src/pipeline/context.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { dirname } from 'path'
2+
import { EventEmitter, window } from 'vscode'
3+
import { Disposable, Disposer } from '@hediet/std/disposable'
4+
import { ContextKey, setContextValue } from '../vscode/context'
5+
import { standardizePossiblePath } from '../fileSystem/path'
6+
7+
const setContextOnDidChangeActiveEditor = (
8+
setActiveEditorContext: (path: string) => void,
9+
dvcRoot: string
10+
): Disposable =>
11+
window.onDidChangeActiveTextEditor(event => {
12+
const path = standardizePossiblePath(event?.document.fileName)
13+
if (!path) {
14+
setActiveEditorContext('')
15+
return
16+
}
17+
18+
if (!path.includes(dvcRoot)) {
19+
return
20+
}
21+
22+
setActiveEditorContext(path)
23+
})
24+
25+
export const setContextForEditorTitleIcons = (
26+
dvcRoot: string,
27+
disposer: (() => void) & Disposer,
28+
pipelineFileFocused: EventEmitter<string | undefined>
29+
): void => {
30+
const setActiveEditorContext = (path: string) => {
31+
const pipeline = path.endsWith('dvc.yaml') ? dirname(path) : undefined
32+
void setContextValue(ContextKey.PIPELINE_FILE_ACTIVE, !!pipeline)
33+
pipelineFileFocused.fire(pipeline)
34+
}
35+
36+
const activePath = window.activeTextEditor?.document?.fileName
37+
if (activePath?.startsWith(dvcRoot)) {
38+
setActiveEditorContext(activePath)
39+
}
40+
41+
disposer.track(
42+
setContextOnDidChangeActiveEditor(setActiveEditorContext, dvcRoot)
43+
)
44+
}

extension/src/pipeline/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'path'
22
import { Event, EventEmitter } from 'vscode'
33
import { appendFileSync, writeFileSync } from 'fs-extra'
4+
import { setContextForEditorTitleIcons } from './context'
45
import { PipelineData } from './data'
56
import { PipelineModel } from './model'
67
import { DeferredDisposable } from '../class/deferred'
@@ -30,9 +31,21 @@ const getScriptCommand = (script: string) => {
3031

3132
export class Pipeline extends DeferredDisposable {
3233
public onDidUpdate: Event<void>
34+
public readonly onDidFocusProject: Event<string | undefined>
3335

3436
private updated: EventEmitter<void>
3537

38+
private focusedPipeline: string | undefined
39+
private readonly pipelineFileFocused: EventEmitter<string | undefined> =
40+
this.dispose.track(new EventEmitter())
41+
42+
private readonly onDidFocusPipelineFile: Event<string | undefined> =
43+
this.pipelineFileFocused.event
44+
45+
private projectFocused: EventEmitter<string | undefined> = this.dispose.track(
46+
new EventEmitter()
47+
)
48+
3649
private readonly dvcRoot: string
3750
private readonly data: PipelineData
3851
private readonly model: PipelineModel
@@ -51,14 +64,22 @@ export class Pipeline extends DeferredDisposable {
5164
this.updated = this.dispose.track(new EventEmitter<void>())
5265
this.onDidUpdate = this.updated.event
5366

67+
this.onDidFocusProject = this.projectFocused.event
68+
5469
void this.initialize()
70+
this.watchActiveEditor()
5571
}
5672

5773
public hasPipeline() {
5874
return this.model.hasPipeline()
5975
}
6076

6177
public async getCwd() {
78+
const focusedPipeline = this.getFocusedPipeline()
79+
if (focusedPipeline) {
80+
return focusedPipeline
81+
}
82+
6283
await this.checkOrAddPipeline()
6384

6485
const pipelines = this.model.getPipelines()
@@ -192,4 +213,23 @@ export class Pipeline extends DeferredDisposable {
192213
private writeDag(dag: string) {
193214
writeFileSync(join(this.dvcRoot, TEMP_DAG_FILE), dag)
194215
}
216+
217+
private getFocusedPipeline() {
218+
return this.focusedPipeline
219+
}
220+
221+
private watchActiveEditor() {
222+
setContextForEditorTitleIcons(
223+
this.dvcRoot,
224+
this.dispose,
225+
this.pipelineFileFocused
226+
)
227+
228+
this.dispose.track(
229+
this.onDidFocusPipelineFile(cwd => {
230+
this.focusedPipeline = cwd
231+
this.projectFocused.fire(cwd && this.dvcRoot)
232+
})
233+
)
234+
}
195235
}

extension/src/pipeline/workspace.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { getOnDidChangeExtensions, isInstalled } from '../vscode/extensions'
1313
export class WorkspacePipeline extends BaseWorkspace<Pipeline> {
1414
private isMermaidSupportInstalled = isInstalled(MARKDOWN_MERMAID_EXTENSION_ID)
1515

16+
private focusedProject: string | undefined
17+
1618
constructor(internalCommands: InternalCommands) {
1719
super(internalCommands)
1820

@@ -37,11 +39,17 @@ export class WorkspacePipeline extends BaseWorkspace<Pipeline> {
3739

3840
this.setRepository(dvcRoot, pipeline)
3941

42+
this.dispose.track(
43+
pipeline.onDidFocusProject(project => {
44+
this.focusedProject = project
45+
})
46+
)
47+
4048
return pipeline
4149
}
4250

4351
public async showDag() {
44-
const cwd = await this.getOnlyOrPickProject()
52+
const cwd = this.focusedProject || (await this.getOnlyOrPickProject())
4553

4654
if (!cwd) {
4755
return

extension/src/test/suite/experiments/index.test.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
configurationChangeEvent,
4848
experimentsUpdatedEvent,
4949
extensionUri,
50+
getActiveEditorUpdatedEvent,
5051
getInputBoxEvent,
5152
getMessageReceivedEmitter
5253
} from '../util'
@@ -1893,17 +1894,6 @@ suite('Experiments Test Suite', () => {
18931894
})
18941895

18951896
describe('editor/title icons', () => {
1896-
const getActiveEditorUpdatedEvent = () =>
1897-
new Promise(resolve => {
1898-
const listener = disposable.track(
1899-
window.onDidChangeActiveTextEditor(() => {
1900-
resolve(undefined)
1901-
disposable.untrack(listener)
1902-
listener.dispose()
1903-
})
1904-
)
1905-
})
1906-
19071897
it('should set the appropriate context value when a params file is open in the active editor/closed', async () => {
19081898
const paramsFile = Uri.file(join(dvcDemoPath, 'params.yaml'))
19091899
await window.showTextDocument(paramsFile)
@@ -1928,7 +1918,7 @@ suite('Experiments Test Suite', () => {
19281918

19291919
mockSetContextValue.resetHistory()
19301920

1931-
const startupEditorClosed = getActiveEditorUpdatedEvent()
1921+
const startupEditorClosed = getActiveEditorUpdatedEvent(disposable)
19321922

19331923
await closeAllEditors()
19341924
await startupEditorClosed
@@ -1940,12 +1930,12 @@ suite('Experiments Test Suite', () => {
19401930

19411931
mockSetContextValue.resetHistory()
19421932

1943-
const activeEditorUpdated = getActiveEditorUpdatedEvent()
1933+
const activeEditorUpdated = getActiveEditorUpdatedEvent(disposable)
19441934

19451935
await window.showTextDocument(paramsFile)
19461936
await activeEditorUpdated
19471937

1948-
const activeEditorClosed = getActiveEditorUpdatedEvent()
1938+
const activeEditorClosed = getActiveEditorUpdatedEvent(disposable)
19491939

19501940
expect(
19511941
mockContext['dvc.experiments.file.active'],
@@ -1970,7 +1960,9 @@ suite('Experiments Test Suite', () => {
19701960
const { experiments } = buildExperiments({ disposer: disposable })
19711961
await experiments.isReady()
19721962

1973-
expect(setContextValueSpy).not.to.be.called
1963+
expect(setContextValueSpy).not.to.be.calledWith(
1964+
'dvc.experiments.file.active'
1965+
)
19741966
})
19751967
})
19761968

extension/src/test/suite/pipeline/index.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { join } from 'path'
22
import { afterEach, beforeEach, describe, it, suite } from 'mocha'
3-
import { SinonStub, restore, stub } from 'sinon'
3+
import { SinonStub, restore, spy, stub } from 'sinon'
44
import { expect } from 'chai'
55
import { QuickPickItem, Uri, window } from 'vscode'
66
import { buildPipeline } from './util'
77
import {
88
bypassProcessManagerDebounce,
99
closeAllEditors,
10+
getActiveEditorUpdatedEvent,
1011
getMockNow
1112
} from '../util'
1213
import { Disposable } from '../../../extension'
@@ -15,6 +16,7 @@ import * as QuickPick from '../../../vscode/quickPick'
1516
import { QuickPickOptionsWithTitle } from '../../../vscode/quickPick'
1617
import * as FileSystem from '../../../fileSystem'
1718
import { ScriptCommand } from '../../../pipeline'
19+
import * as VscodeContext from '../../../vscode/context'
1820

1921
suite('Pipeline Test Suite', () => {
2022
const disposable = Disposable.fn()
@@ -285,4 +287,76 @@ suite('Pipeline Test Suite', () => {
285287
expect(mockFindOrCreateDvcYamlFile).not.to.be.called
286288
})
287289
})
290+
291+
it('should set the appropriate context value when a dvc.yaml is open in the active editor', async () => {
292+
const dvcYaml = Uri.file(join(dvcDemoPath, 'dvc.yaml'))
293+
await window.showTextDocument(dvcYaml)
294+
295+
const mockContext: { [key: string]: unknown } = {
296+
'dvc.pipeline.file.active': false
297+
}
298+
299+
const mockSetContextValue = stub(VscodeContext, 'setContextValue')
300+
mockSetContextValue.callsFake((key: string, value: unknown) => {
301+
mockContext[key] = value
302+
return Promise.resolve(undefined)
303+
})
304+
305+
const { pipeline } = buildPipeline({ disposer: disposable })
306+
await pipeline.isReady()
307+
308+
expect(
309+
mockContext['dvc.pipeline.file.active'],
310+
'should set dvc.pipeline.file.active to true when a dvc.yaml is open and the extension starts'
311+
).to.be.true
312+
313+
mockSetContextValue.resetHistory()
314+
315+
const startupEditorClosed = getActiveEditorUpdatedEvent(disposable)
316+
317+
await closeAllEditors()
318+
await startupEditorClosed
319+
320+
expect(
321+
mockContext['dvc.pipeline.file.active'],
322+
'should set dvc.pipeline.file.active to false when the dvc.yaml in the active editor is closed'
323+
).to.be.false
324+
325+
mockSetContextValue.resetHistory()
326+
327+
const activeEditorUpdated = getActiveEditorUpdatedEvent(disposable)
328+
329+
await window.showTextDocument(dvcYaml)
330+
await activeEditorUpdated
331+
332+
const activeEditorClosed = getActiveEditorUpdatedEvent(disposable)
333+
334+
expect(
335+
mockContext['dvc.pipeline.file.active'],
336+
'should set dvc.pipeline.file.active to true when a dvc.yaml file is in the active editor'
337+
).to.be.true
338+
339+
await closeAllEditors()
340+
await activeEditorClosed
341+
342+
expect(
343+
mockContext['dvc.pipeline.file.active'],
344+
'should set dvc.pipeline.file.active to false when the dvc.yaml in the active editor is closed again'
345+
).to.be.false
346+
})
347+
348+
it('should set dvc.pipeline.file.active to false when a dvc.yaml is not open and the extension starts', async () => {
349+
const nonDvcYaml = Uri.file(join(dvcDemoPath, '.gitignore'))
350+
await window.showTextDocument(nonDvcYaml)
351+
352+
const setContextValueSpy = spy(VscodeContext, 'setContextValue')
353+
354+
const { pipeline } = buildPipeline({ disposer: disposable })
355+
await pipeline.isReady()
356+
357+
expect(setContextValueSpy).to.be.calledWith(
358+
'dvc.pipeline.file.active',
359+
false
360+
)
361+
})
288362
})

extension/src/test/suite/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,14 @@ export const waitForEditorText = async (): Promise<unknown> => {
327327
}
328328
return waitForEditorText()
329329
}
330+
331+
export const getActiveEditorUpdatedEvent = (disposer: Disposer) =>
332+
new Promise(resolve => {
333+
const listener = disposer.track(
334+
window.onDidChangeActiveTextEditor(() => {
335+
resolve(undefined)
336+
disposer.untrack(listener)
337+
listener.dispose()
338+
})
339+
)
340+
})

extension/src/vscode/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export enum ContextKey {
1111
EXPERIMENTS_SORTED = 'dvc.experiments.sorted',
1212
EXPERIMENTS_WEBVIEW_ACTIVE = 'dvc.experiments.webview.active',
1313
MULTIPLE_PROJECTS = 'dvc.multiple.projects',
14+
PIPELINE_FILE_ACTIVE = 'dvc.pipeline.file.active',
1415
PLOTS_WEBVIEW_ACTIVE = 'dvc.plots.webview.active',
1516
PROJECT_AVAILABLE = 'dvc.project.available',
1617
PROJECT_HAS_DATA = 'dvc.project.hasData',

0 commit comments

Comments
 (0)