Skip to content

Commit c4a0be3

Browse files
authored
Add multi-select push and pull to tracked explorer tree (#1809)
* add multi select push and pull actions * add unit test for new functionality * extend unit tests * add test for siblings being returned * standardise tree view name * have path item properties present in the source control view * set state of repository dependants after experiments data is processed * mock correct file
1 parent c0da142 commit c4a0be3

File tree

11 files changed

+408
-25
lines changed

11 files changed

+408
-25
lines changed

extension/src/fileSystem/tree.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import {
44
TreeDataProvider,
55
TreeItem,
66
TreeItemCollapsibleState,
7-
Uri,
8-
window
7+
TreeView,
8+
Uri
99
} from 'vscode'
1010
import { exists, relativeWithUri } from '.'
1111
import { fireWatcher } from './watcher'
1212
import { deleteTarget, moveTargets } from './workspace'
13-
import { definedAndNonEmpty } from '../util/array'
13+
import { definedAndNonEmpty, uniqueValues } from '../util/array'
1414
import {
1515
AvailableCommands,
1616
CommandId,
@@ -26,16 +26,22 @@ import { warnOfConsequences } from '../vscode/modal'
2626
import { Response } from '../vscode/response'
2727
import { Resource } from '../repository/commands'
2828
import { WorkspaceRepositories } from '../repository/workspace'
29-
import { collectTrackedPaths, PathItem } from '../repository/model/collect'
29+
import {
30+
collectSelected,
31+
collectTrackedPaths,
32+
PathItem
33+
} from '../repository/model/collect'
3034
import { Title } from '../vscode/title'
3135
import { Disposable } from '../class/dispose'
36+
import { createTreeView } from '../vscode/tree'
3237

3338
export class TrackedExplorerTree
3439
extends Disposable
3540
implements TreeDataProvider<PathItem>
3641
{
3742
public readonly onDidChangeTreeData: Event<void>
3843

44+
private readonly view: TreeView<string | PathItem>
3945
private readonly internalCommands: InternalCommands
4046
private readonly repositories: WorkspaceRepositories
4147

@@ -57,8 +63,8 @@ export class TrackedExplorerTree
5763

5864
this.onDidChangeTreeData = repositories.treeDataChanged.event
5965

60-
this.dispose.track(
61-
window.registerTreeDataProvider('dvc.views.trackedExplorerTree', this)
66+
this.view = this.dispose.track(
67+
createTreeView<PathItem>('dvc.views.trackedExplorerTree', this, true)
6268
)
6369
}
6470

@@ -242,13 +248,29 @@ export class TrackedExplorerTree
242248

243249
private tryThenForce(commandId: CommandId) {
244250
return async (pathItem: PathItem) => {
245-
const { dvcRoot } = pathItem
246-
const tracked = await collectTrackedPaths(pathItem, (path: string) =>
247-
this.getRepoChildren(dvcRoot, path)
248-
)
249-
const args = [dvcRoot, ...tracked.sort()]
251+
const selected = collectSelected([
252+
...this.getSelectedPathItems(),
253+
pathItem
254+
])
255+
256+
for (const [dvcRoot, pathItems] of Object.entries(selected)) {
257+
const tracked = []
258+
for (const pathItem of pathItems) {
259+
tracked.push(
260+
...(await collectTrackedPaths(pathItem, (path: string) =>
261+
this.getRepoChildren(dvcRoot, path)
262+
))
263+
)
264+
}
250265

251-
return tryThenMaybeForce(this.internalCommands, commandId, ...args)
266+
const args = [dvcRoot, ...uniqueValues(tracked).sort()]
267+
268+
await tryThenMaybeForce(this.internalCommands, commandId, ...args)
269+
}
252270
}
253271
}
272+
273+
private getSelectedPathItems() {
274+
return [...this.view.selection]
275+
}
254276
}

extension/src/repository/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class Repository extends DeferredDisposable {
6969
experiments.onDidChangeExperiments(data => {
7070
if (data) {
7171
this.model.transformAndSetExperiments(data)
72+
this.setState()
7273
}
7374
})
7475
)

extension/src/repository/model/collect.test.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { join } from 'path'
22
import { Uri } from 'vscode'
3-
import { collectTree } from './collect'
3+
import { collectSelected, collectTree } from './collect'
44
import { dvcDemoPath } from '../../test/util'
55

6+
const makeUri = (...paths: string[]): Uri =>
7+
Uri.file(join(dvcDemoPath, ...paths))
8+
69
describe('collectTree', () => {
7-
const makeUri = (...paths: string[]): Uri =>
8-
Uri.file(join(dvcDemoPath, ...paths))
910
const makeAbsPath = (...paths: string[]): string => makeUri(...paths).fsPath
1011

1112
it('should transform recursive list output into a tree', () => {
@@ -190,3 +191,87 @@ describe('collectTree', () => {
190191
)
191192
})
192193
})
194+
195+
describe('collectSelected', () => {
196+
const logsPathItem = {
197+
dvcRoot: dvcDemoPath,
198+
isDirectory: true,
199+
isTracked: true,
200+
resourceUri: makeUri('logs')
201+
}
202+
203+
const accPathItem = {
204+
dvcRoot: dvcDemoPath,
205+
isDirectory: false,
206+
isTracked: true,
207+
resourceUri: makeUri('logs', 'acc.tsv')
208+
}
209+
210+
const lossPathItem = {
211+
dvcRoot: dvcDemoPath,
212+
isDirectory: false,
213+
isTracked: true,
214+
resourceUri: makeUri('logs', 'loss.tsv')
215+
}
216+
217+
it('should return an empty object if no path items are provided', () => {
218+
expect(collectSelected([])).toStrictEqual({})
219+
})
220+
221+
it('should return the original item if only one is provided', () => {
222+
const selected = collectSelected([logsPathItem])
223+
224+
expect(selected).toStrictEqual({
225+
[dvcDemoPath]: [logsPathItem]
226+
})
227+
})
228+
229+
it('should return a root given it is select', () => {
230+
const selected = collectSelected([dvcDemoPath, logsPathItem, accPathItem])
231+
232+
expect(selected).toStrictEqual({
233+
[dvcDemoPath]: [dvcDemoPath]
234+
})
235+
})
236+
237+
it('should return siblings if a parent is not provided', () => {
238+
const selected = collectSelected([accPathItem, lossPathItem])
239+
240+
expect(selected).toStrictEqual({
241+
[dvcDemoPath]: [accPathItem, lossPathItem]
242+
})
243+
})
244+
245+
it('should exclude all children from the final list', () => {
246+
const selected = collectSelected([lossPathItem, accPathItem, logsPathItem])
247+
248+
expect(selected).toStrictEqual({
249+
[dvcDemoPath]: [logsPathItem]
250+
})
251+
})
252+
253+
it('should return multiple entries when multiple roots are provided', () => {
254+
const mockOtherRepoItem = {
255+
dvcRoot: __dirname,
256+
isDirectory: true,
257+
isTracked: true,
258+
resourceUri: Uri.file(join(__dirname, 'mock', 'path'))
259+
}
260+
261+
const selected = collectSelected([
262+
mockOtherRepoItem,
263+
{
264+
dvcRoot: dvcDemoPath,
265+
isDirectory: false,
266+
isTracked: true,
267+
resourceUri: makeUri('logs', 'acc.tsv')
268+
},
269+
logsPathItem
270+
])
271+
272+
expect(selected).toStrictEqual({
273+
[__dirname]: [mockOtherRepoItem],
274+
[dvcDemoPath]: [logsPathItem]
275+
})
276+
})
277+
})

extension/src/repository/model/collect.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,16 @@ export const collectTrackedOuts = (data: ExperimentsOutput): Set<string> => {
135135
}
136136

137137
export const collectTrackedPaths = async (
138-
{ dvcRoot, resourceUri, isTracked }: PathItem,
138+
pathItem: string | PathItem,
139139
getChildren: (path: string) => Promise<PathItem[]>
140140
): Promise<string[]> => {
141-
const acc = []
141+
const acc: string[] = []
142+
if (typeof pathItem === 'string') {
143+
return acc
144+
}
142145

143-
if (isTracked) {
146+
const { dvcRoot, resourceUri, isTracked } = pathItem
147+
if (isTracked !== false) {
144148
acc.push(relativeWithUri(dvcRoot, resourceUri))
145149
return acc
146150
}
@@ -151,6 +155,89 @@ export const collectTrackedPaths = async (
151155
return acc
152156
}
153157

158+
type SelectedPathAccumulator = { [dvcRoot: string]: (string | PathItem)[] }
159+
160+
const collectSelectedPaths = (pathItems: (string | PathItem)[]): string[] => {
161+
const acc = new Set<string>()
162+
for (const pathItem of pathItems) {
163+
if (typeof pathItem === 'string') {
164+
acc.add(pathItem)
165+
continue
166+
}
167+
acc.add(pathItem.resourceUri.fsPath)
168+
}
169+
return [...acc]
170+
}
171+
172+
const parentIsSelected = (fsPath: string, paths: string[]) => {
173+
for (const path of paths.filter(path => path !== fsPath)) {
174+
if (fsPath.includes(path)) {
175+
return true
176+
}
177+
}
178+
return false
179+
}
180+
181+
const initializeAccumulatorRoot = (
182+
acc: SelectedPathAccumulator,
183+
dvcRoot: string
184+
) => {
185+
if (!acc[dvcRoot]) {
186+
acc[dvcRoot] = []
187+
}
188+
}
189+
190+
const collectPathItem = (
191+
acc: SelectedPathAccumulator,
192+
addedPaths: Set<string>,
193+
pathItem: PathItem | string,
194+
dvcRoot: string,
195+
path: string
196+
) => {
197+
initializeAccumulatorRoot(acc, dvcRoot)
198+
acc[dvcRoot].push(pathItem)
199+
addedPaths.add(path)
200+
}
201+
202+
const collectRoot = (
203+
acc: SelectedPathAccumulator,
204+
addedPaths: Set<string>,
205+
path: string
206+
) => collectPathItem(acc, addedPaths, path, path, path)
207+
208+
const collectRootOrPathItem = (
209+
acc: SelectedPathAccumulator,
210+
addedPaths: Set<string>,
211+
paths: string[],
212+
pathItem: string | PathItem
213+
) => {
214+
const path =
215+
typeof pathItem === 'string' ? pathItem : pathItem.resourceUri.fsPath
216+
if (addedPaths.has(path) || parentIsSelected(path, paths)) {
217+
return
218+
}
219+
220+
if (typeof pathItem === 'string') {
221+
collectRoot(acc, addedPaths, path)
222+
return
223+
}
224+
225+
const { dvcRoot } = pathItem
226+
collectPathItem(acc, addedPaths, pathItem, dvcRoot, path)
227+
}
228+
229+
export const collectSelected = (
230+
pathItems: (string | PathItem)[]
231+
): SelectedPathAccumulator => {
232+
const selectedPaths = collectSelectedPaths(pathItems)
233+
const acc: SelectedPathAccumulator = {}
234+
const addedPaths = new Set<string>()
235+
for (const pathItem of pathItems) {
236+
collectRootOrPathItem(acc, addedPaths, selectedPaths, pathItem)
237+
}
238+
return acc
239+
}
240+
154241
const isUncollectedChild = (
155242
deleted: Set<string>,
156243
deletedPath: string,

0 commit comments

Comments
 (0)