Skip to content

Commit c89e2b7

Browse files
authored
Enable sub-project selection in monorepos (#3030)
* auto-locate sub-projects inside of monorepos * add config option to set focused projects inside of the workspace * rewrite config description * ensure focused projects are set on startup * standardize test paths * rename private method inside of setup * update user facing text
1 parent 84c30a9 commit c89e2b7

File tree

9 files changed

+250
-16
lines changed

9 files changed

+250
-16
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ These are the VS Code [settings] available for the Extension:
151151
| `dvc.dvcPath` | Path or shell command to the DVC binary. Required unless Microsoft's [Python extension] is installed and the `dvc` package found in its environment. |
152152
| `dvc.pythonPath` | Path to the desired Python interpreter to use with DVC. Should only be utilized when using a virtual environment without Microsoft's [Python extension]. |
153153
| `dvc.experimentsTableHeadMaxHeight` | Maximum height of experiment table head rows. |
154+
| `dvc.focusedProjects` | A subset of paths to the workspace's available DVC projects. Using this option will override project auto-discovery. |
154155
| `dvc.doNotShowWalkthroughAfterInstall` | Do not prompt to show the Get Started page after installing. Useful for pre-configured development environments |
155156
| `dvc.doNotRecommendRedHatExtension` | Do not prompt to install the Red Hat YAML extension, which helps with DVC YAML schema validation (`dvc.yaml` and `.dvc` files). |
156157
| `dvc.doNotShowCliUnavailable` | Do not warn when the workspace contains a DVC project but the DVC binary is unavailable. |

extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,16 @@
529529
"configuration": {
530530
"title": "DVC",
531531
"properties": {
532+
"dvc.focusedProjects": {
533+
"title": "%config.focusedProjects.title%",
534+
"description": "%config.focusedProjects.description%",
535+
"type": [
536+
"string",
537+
"array",
538+
"null"
539+
],
540+
"default": null
541+
},
532542
"dvc.doNotRecommendRedHatExtension": {
533543
"title": "%config.doNotRecommendRedHatExtension.title%",
534544
"description": "%config.doNotRecommendRedHatExtension.description%",

extension/package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
"command.views.experimentsTree.selectExperiments": "Select Experiments to Display in Plots",
7676
"command.views.plotsPathsTree.selectPlots": "Select Plots to Display",
7777
"command.views.plotsPathsTree.refreshPlots": "Refresh Plots for Selected Experiments",
78+
"config.focusedProjects.description": "A subset of paths to the workspace's available DVC projects. Using this option will override project auto-discovery. Only absolute paths inside of the workspace will be included. All other DVC projects in the workspace will be ignored.",
79+
"config.focusedProjects.title": "DVC project path(s) to include.",
7880
"config.doNotRecommendRedHatExtension.description": "Do not prompt to install the Red Hat YAML extension, which helps with DVC YAML schema validation.",
7981
"config.doNotRecommendRedHatExtension.title": "Do not recommend the YAML extension.",
8082
"config.doNotShowCliUnavailable.description": "Do not warn when the workspace contains a DVC project but the DVC binary is unavailable.",

extension/src/config.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
1-
import { Disposable } from '@hediet/std/disposable'
21
import { EventEmitter, Event, workspace } from 'vscode'
2+
import { Disposable } from '@hediet/std/disposable'
3+
import isEqual from 'lodash.isequal'
4+
import isEmpty from 'lodash.isempty'
35
import {
46
getOnDidChangePythonExecutionDetails,
57
getPythonBinPath
68
} from './extensions/python'
79
import { ConfigKey, getConfigValue } from './vscode/config'
810
import { DeferredDisposable } from './class/deferred'
911
import { getOnDidChangeExtensions } from './vscode/extensions'
12+
import { standardizePath } from './fileSystem/path'
13+
import { getWorkspaceFolders } from './vscode/workspaceFolders'
14+
import { isSameOrChild } from './fileSystem'
1015

1116
export class Config extends DeferredDisposable {
12-
public readonly onDidChangeExecutionDetails: Event<void>
17+
public readonly onDidChangeConfigurationDetails: Event<void>
1318

1419
private pythonBinPath: string | undefined
1520

1621
private dvcPath = this.getCliPath()
1722

18-
private readonly executionDetailsChanged: EventEmitter<void>
23+
private focusedProjects: string[] | undefined
24+
25+
private readonly configurationDetailsChanged: EventEmitter<void>
1926

2027
private pythonExecutionDetailsListener?: Disposable
2128
private readonly onDidChangeExtensionsEvent: Event<void>
2229

2330
constructor(onDidChangeExtensionsEvent = getOnDidChangeExtensions()) {
2431
super()
2532

26-
this.executionDetailsChanged = this.dispose.track(new EventEmitter())
27-
this.onDidChangeExecutionDetails = this.executionDetailsChanged.event
33+
this.configurationDetailsChanged = this.dispose.track(new EventEmitter())
34+
this.onDidChangeConfigurationDetails =
35+
this.configurationDetailsChanged.event
2836
this.onDidChangeExtensionsEvent = onDidChangeExtensionsEvent
2937

3038
this.setPythonBinPath()
39+
this.setFocusedProjects()
3140

3241
this.onDidChangePythonExecutionDetails()
3342
this.onDidChangeExtensions()
@@ -43,6 +52,10 @@ export class Config extends DeferredDisposable {
4352
return this.pythonBinPath
4453
}
4554

55+
public getFocusedProjects() {
56+
return this.focusedProjects
57+
}
58+
4659
public async setPythonBinPath() {
4760
this.pythonBinPath = await this.getConfigOrExtensionPythonBinPath()
4861
return this.deferred.resolve()
@@ -97,16 +110,71 @@ export class Config extends DeferredDisposable {
97110
if (e.affectsConfiguration(ConfigKey.PYTHON_PATH)) {
98111
this.setPythonAndNotifyIfChanged()
99112
}
113+
if (e.affectsConfiguration(ConfigKey.FOCUSED_PROJECTS)) {
114+
this.setFocusedProjectsAndNotifyIfChanged()
115+
}
100116
})
101117
)
102118
}
103119

120+
private setFocusedProjectsAndNotifyIfChanged() {
121+
const oldFocusedProjects = this.focusedProjects
122+
this.setFocusedProjects()
123+
if (!isEqual(oldFocusedProjects, this.focusedProjects)) {
124+
this.configurationDetailsChanged.fire()
125+
}
126+
}
127+
128+
private setFocusedProjects() {
129+
this.focusedProjects = this.validateFocusedProjects()
130+
}
131+
132+
private validateFocusedProjects() {
133+
const focusedProjects = getConfigValue<string | string[] | undefined>(
134+
ConfigKey.FOCUSED_PROJECTS
135+
)
136+
137+
if (!focusedProjects) {
138+
return undefined
139+
}
140+
const workspaceFolders = getWorkspaceFolders()
141+
142+
const validatedFocusedProjects = new Set<string>()
143+
144+
const paths = Array.isArray(focusedProjects)
145+
? focusedProjects
146+
: ([focusedProjects].filter(Boolean) as string[])
147+
for (const path of paths) {
148+
this.collectValidFocusedProject(
149+
validatedFocusedProjects,
150+
path,
151+
workspaceFolders
152+
)
153+
}
154+
return validatedFocusedProjects.size > 0
155+
? [...validatedFocusedProjects].sort()
156+
: undefined
157+
}
158+
159+
private collectValidFocusedProject(
160+
validatedFocusedProjects: Set<string>,
161+
path: string,
162+
workspaceFolders: string[]
163+
) {
164+
const standardizedPath = standardizePath(path)
165+
for (const workspaceFolder of workspaceFolders) {
166+
if (isSameOrChild(workspaceFolder, standardizedPath)) {
167+
validatedFocusedProjects.add(standardizedPath)
168+
}
169+
}
170+
}
171+
104172
private notifyIfChanged(
105173
oldPath: string | undefined,
106174
newPath: string | undefined
107175
) {
108176
if (oldPath !== newPath) {
109-
this.executionDetailsChanged.fire()
177+
this.configurationDetailsChanged.fire()
110178
}
111179
}
112180
}

extension/src/fileSystem/index.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ describe('findDvcRootPaths', () => {
3535

3636
expect([...dvcRoots]).toStrictEqual([dvcDemoPath, mockDvcRoot])
3737
})
38+
39+
it('should find a mono-repo root as well as sub-roots if available one directory below the given folder', async () => {
40+
const parentDir = dvcDemoPath
41+
const mockFirstDvcRoot = join(parentDir, 'mockFirstDvc')
42+
const mockSecondDvcRoot = join(parentDir, 'mockSecondDvc')
43+
ensureDirSync(join(mockFirstDvcRoot, DOT_DVC))
44+
ensureDirSync(join(mockSecondDvcRoot, DOT_DVC))
45+
46+
const dvcRoots = await findDvcRootPaths(parentDir)
47+
48+
remove(mockFirstDvcRoot)
49+
remove(mockSecondDvcRoot)
50+
51+
expect([...dvcRoots]).toStrictEqual([
52+
dvcDemoPath,
53+
mockFirstDvcRoot,
54+
mockSecondDvcRoot
55+
])
56+
})
3857
})
3958

4059
describe('findAbsoluteDvcRootPath', () => {

extension/src/fileSystem/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,18 @@ export const findSubRootPaths = async (
4444
}
4545

4646
export const findDvcRootPaths = async (cwd: string): Promise<string[]> => {
47+
const dvcRoots = []
48+
4749
if (isDirectory(join(cwd, DOT_DVC))) {
48-
return [cwd]
50+
dvcRoots.push(cwd)
4951
}
5052

5153
const subRoots = await findSubRootPaths(cwd, DOT_DVC)
5254

5355
if (definedAndNonEmpty(subRoots)) {
54-
return subRoots
56+
dvcRoots.push(...subRoots)
5557
}
56-
return []
58+
return dvcRoots.sort()
5759
}
5860

5961
export const findAbsoluteDvcRootPath = async (

extension/src/setup/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class Setup
150150
})
151151
)
152152
this.watchForVenvChanges()
153-
this.watchExecutionDetailsForChanges()
153+
this.watchConfigurationDetailsForChanges()
154154
this.watchDotFolderForChanges()
155155
this.watchPathForChanges(stopWatch)
156156
}
@@ -273,7 +273,8 @@ export class Setup
273273
}
274274

275275
private async findDvcRoots(cwd: string): Promise<string[]> {
276-
const dvcRoots = await findDvcRootPaths(cwd)
276+
const dvcRoots =
277+
this.config.getFocusedProjects() || (await findDvcRootPaths(cwd))
277278
if (definedAndNonEmpty(dvcRoots)) {
278279
return dvcRoots
279280
}
@@ -400,9 +401,9 @@ export class Setup
400401
}
401402
}
402403

403-
private watchExecutionDetailsForChanges() {
404+
private watchConfigurationDetailsForChanges() {
404405
this.dispose.track(
405-
this.config.onDidChangeExecutionDetails(async () => {
406+
this.config.onDidChangeConfigurationDetails(async () => {
406407
const stopWatch = new StopWatch()
407408
try {
408409
this.sendDataToWebview()

0 commit comments

Comments
 (0)