Skip to content
This repository was archived by the owner on Mar 10, 2022. It is now read-only.

Commit 3275d92

Browse files
committed
fix: optimize listing file tree children
1 parent f631b92 commit 3275d92

7 files changed

Lines changed: 196 additions & 64 deletions

File tree

src/code-intel/SourcegraphReferenceProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class SourcegraphReferenceProvider implements vscode.ReferenceProvider {
4040
const repos = [...this.fs.allRepositoryUris()]
4141
.map(repo => {
4242
const uri = SourcegraphUri.parse(repo)
43-
return `repo:^${uri.repositoryName}$${uri.revisionSuffix()}`
43+
return `repo:^${uri.repositoryName}$${uri.revisionPart()}`
4444
})
4545
.join(' OR ')
4646
const query = `(${repos}) AND ${document.getText()}`

src/file-system/FileTree.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const tree = new FileTree(SourcegraphUri.parse('https://sourcegraph.com/sourcegr
2828
'src/git/remoteUrl.test.ts',
2929
'src/git/remoteUrl.ts',
3030
'src/log.ts',
31+
'tests/config.json',
3132
'tsconfig.json',
3233
])
3334

@@ -46,12 +47,21 @@ function checkChildren(directory: string, expected: string[]) {
4647
})
4748
}
4849
describe('FileTree', () => {
50+
it('binarySearchStart', () => {
51+
const end = tree.binarySearchDirectoryStart('src/browse/')
52+
console.log(`end=${end} file=${tree.files[end]}`)
53+
})
54+
it('binarySearchEnd', () => {
55+
const end = tree.binarySearchDirectoryEnd('src/browse/', 0)
56+
console.log(`end=${end} file=${tree.files[end]}`)
57+
})
4958
checkChildren('src', ['src/browse/', 'src/git/', 'src/config.ts', 'src/extension.ts', 'src/log.ts'])
5059
checkChildren('', [
51-
'.github/',
60+
'.github/workflows/',
5261
'.vscode/',
5362
'images/',
5463
'src/',
64+
'tests/',
5565
'.eslintrc.json',
5666
'.gitignore',
5767
'.vscodeignore',

src/file-system/FileTree.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,39 @@ export class FileTree {
1616
// usable in its current state but could be much faster if we use binary
1717
// search to skip unrelated paths.
1818
public directChildren(directory: string): string[] {
19+
return this.directChildrenInternal(directory, true)
20+
}
21+
22+
private directChildrenInternal(directory: string, allowRecursion: boolean): string[] {
1923
const depth = this.depth(directory)
2024
const directFiles = new Set<string>()
2125
const directDirectories = new Set<string>()
2226
const isRoot = directory === ''
2327
if (!isRoot && !directory.endsWith('/')) {
2428
directory = directory + '/'
2529
}
26-
for (const file of this.files) {
30+
let index = this.binarySearchDirectoryStart(directory)
31+
while (index < this.files.length) {
32+
const startIndex = index
33+
const file = this.files[index]
2734
if (file === '') {
35+
index++
2836
continue
2937
}
3038
if (file.startsWith(directory)) {
3139
const fileDepth = this.depth(file)
3240
const isFile = isRoot ? fileDepth === 0 : fileDepth === depth + 1
33-
const path = isFile ? file : file.slice(0, file.indexOf('/', directory.length))
41+
let path = isFile ? file : file.slice(0, file.indexOf('/', directory.length))
42+
let nestedChildren = allowRecursion && !isFile ? this.directChildrenInternal(path, false) : []
43+
while (allowRecursion && nestedChildren.length === 1) {
44+
const child = SourcegraphUri.parse(nestedChildren[0])
45+
if (child.isDirectory()) {
46+
path = child.path || ''
47+
nestedChildren = this.directChildrenInternal(path, false)
48+
} else {
49+
break
50+
}
51+
}
3452
const uri = SourcegraphUri.fromParts(this.uri.host, this.uri.repositoryName, {
3553
revision: this.uri.revision,
3654
path,
@@ -39,13 +57,50 @@ export class FileTree {
3957
if (isFile) {
4058
directFiles.add(uri)
4159
} else {
60+
index = this.binarySearchDirectoryEnd(path + '/', index + 1)
4261
directDirectories.add(uri)
62+
// log.appendLine(`directory=${path} end=${this.files[index]}`)
4363
}
4464
}
65+
if (index === startIndex) {
66+
index++
67+
}
4568
}
4669
return [...directDirectories, ...directFiles]
4770
}
4871

72+
public binarySearchDirectoryStart(directory: string): number {
73+
if (directory === '') {
74+
return 0
75+
}
76+
return this.binarySearch(
77+
{ low: 0, high: this.files.length },
78+
midpoint => this.files[midpoint].localeCompare(directory) > 0
79+
)
80+
}
81+
82+
public binarySearchDirectoryEnd(directory: string, low: number): number {
83+
while (low < this.files.length && this.files[low].localeCompare(directory) <= 0) {
84+
low++
85+
}
86+
return this.binarySearch(
87+
{ low, high: this.files.length },
88+
midpoint => !this.files[midpoint].startsWith(directory)
89+
)
90+
}
91+
92+
public binarySearch({ low, high }: SearchRange, isGreater: (midpoint: number) => boolean): number {
93+
while (low < high) {
94+
const midpoint = Math.floor(low + (high - low) / 2)
95+
if (isGreater(midpoint)) {
96+
high = midpoint
97+
} else {
98+
low = midpoint + 1
99+
}
100+
}
101+
return high
102+
}
103+
49104
private depth(path: string): number {
50105
let result = 0
51106
for (const char of path) {
@@ -56,3 +111,8 @@ export class FileTree {
56111
return result
57112
}
58113
}
114+
115+
interface SearchRange {
116+
low: number
117+
high: number
118+
}

src/file-system/SourcegraphFileSystemProvider.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ export class SourcegraphFileSystemProvider implements vscode.FileSystemProvider
4040
public readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this.didChangeFile.event
4141
public async stat(vscodeUri: vscode.Uri): Promise<vscode.FileStat> {
4242
const uri = this.sourcegraphUri(vscodeUri)
43-
const files = await this.downloadedFiles(uri)
43+
const files = await this.downloadFiles(uri)
4444
const isFile = uri.path && files.includes(uri.path)
4545
const type = isFile ? vscode.FileType.File : vscode.FileType.Directory
46+
// log.appendLine(
47+
// `stat(${uri.uri}) path=${uri.path || '""'} files.length=${files.length} type=${vscode.FileType[type]}`
48+
// )
4649
const now = Date.now()
4750
return {
4851
// It seems to be OK to return hardcoded values for the timestamps
@@ -114,7 +117,7 @@ export class SourcegraphFileSystemProvider implements vscode.FileSystemProvider
114117
const uri = SourcegraphUri.parse(repositoryUri)
115118
promises.push({
116119
repositoryUri: uri.repositoryUri(),
117-
repositoryName: `${uri.repositoryName}${uri.revisionSuffix()}`,
120+
repositoryName: `${uri.repositoryName}${uri.revisionPart()}`,
118121
fileNames,
119122
})
120123
} catch {
@@ -236,10 +239,6 @@ export class SourcegraphFileSystemProvider implements vscode.FileSystemProvider
236239
return metadata
237240
}
238241

239-
public async downloadedFiles(uri: SourcegraphUri): Promise<string[]> {
240-
return this.fileNamesByRepository.get(uri.repositoryUri()) || []
241-
}
242-
243242
public downloadFiles(uri: SourcegraphUri): Promise<string[]> {
244243
const key = uri.repositoryUri()
245244
const fileNamesByRepository = this.fileNamesByRepository
@@ -249,10 +248,28 @@ export class SourcegraphFileSystemProvider implements vscode.FileSystemProvider
249248
{ repository: uri.repositoryName, revision: uri.revision },
250249
emptyCancelationToken()
251250
)
252-
downloadingFiles.then(
253-
() => this.didDownloadFilenames.fire(key),
254-
() => fileNamesByRepository.delete(key)
255-
)
251+
vscode.window
252+
.withProgress(
253+
{
254+
location: vscode.ProgressLocation.SourceControl,
255+
title: `Downloading files for the repository ${uri.repositoryName}`,
256+
},
257+
async progress => {
258+
try {
259+
await downloadingFiles
260+
this.didDownloadFilenames.fire(key)
261+
} catch (error) {
262+
log.error(`downloadFiles(${key})`, error)
263+
fileNamesByRepository.delete(key)
264+
}
265+
progress.report({ increment: 100 })
266+
}
267+
)
268+
.then(
269+
() => {},
270+
() => {}
271+
)
272+
256273
this.fileNamesByRepository.set(key, downloadingFiles)
257274
}
258275
return downloadingFiles
@@ -263,7 +280,7 @@ export class SourcegraphFileSystemProvider implements vscode.FileSystemProvider
263280
}
264281

265282
public async getFileTree(uri: SourcegraphUri): Promise<FileTree> {
266-
const files = await this.downloadedFiles(uri)
283+
const files = await this.downloadFiles(uri)
267284
return new FileTree(uri, files)
268285
}
269286
}

src/file-system/SourcegraphTreeDataProvider.ts

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as vscode from 'vscode'
22
import { log } from '../log'
33
import { SourcegraphFileSystemProvider } from './SourcegraphFileSystemProvider'
4-
import { emptyCancelationToken } from './emptyCancelationToken'
54
import { SourcegraphUri } from './SourcegraphUri'
65

76
export class SourcegraphTreeDataProvider implements vscode.TreeDataProvider<string> {
@@ -14,6 +13,7 @@ export class SourcegraphTreeDataProvider implements vscode.TreeDataProvider<stri
1413
private treeView: vscode.TreeView<string> | undefined
1514
private activeUri: vscode.Uri | undefined
1615
private didFocusToken = new vscode.CancellationTokenSource()
16+
private treeItemCache = new Map<string, vscode.TreeItem>()
1717
private readonly didChangeTreeData = new vscode.EventEmitter<string | undefined>()
1818
public readonly onDidChangeTreeData: vscode.Event<string | undefined> = this.didChangeTreeData.event
1919

@@ -52,7 +52,11 @@ export class SourcegraphTreeDataProvider implements vscode.TreeDataProvider<stri
5252
}
5353
const uri = SourcegraphUri.parse(uriString)
5454
const tree = await this.fs.getFileTree(uri)
55-
return tree.directChildren(uri.path || '')
55+
const directChildren = tree.directChildren(uri.path || '')
56+
for (const child of directChildren) {
57+
this.treeItemCache.set(child, this.newTreeItem(SourcegraphUri.parse(child), uri, directChildren.length))
58+
}
59+
return directChildren
5660
} catch (error) {
5761
log.error(`getChildren(${uriString || ''})`, error)
5862
return Promise.resolve(undefined)
@@ -75,29 +79,18 @@ export class SourcegraphTreeDataProvider implements vscode.TreeDataProvider<stri
7579
}
7680
}
7781

78-
public async getTreeItem(uriString: string): Promise<vscode.TreeItem> {
82+
public getTreeItem(uriString: string): vscode.TreeItem {
7983
try {
80-
const uri = SourcegraphUri.parse(uriString)
81-
const label = await this.treeItemLabel(uri)
82-
const collapsibleState = await this.getCollapsibleState(uri)
83-
const command = uri.isFile()
84-
? {
85-
command: 'extension.openFile',
86-
title: 'Open file',
87-
arguments: [uri.uri],
88-
}
89-
: undefined
90-
return {
91-
id: uri.uri,
92-
label,
93-
tooltip: uri.uri.replace('sourcegraph://', 'https://'),
94-
collapsibleState,
95-
command,
96-
resourceUri: vscode.Uri.parse(uri.uri),
84+
const fromCache = this.treeItemCache.get(uriString)
85+
if (fromCache) {
86+
return fromCache
9787
}
88+
const uri = SourcegraphUri.parse(uriString)
89+
const parentUri = uri.parentUri()
90+
return this.newTreeItem(uri, parentUri ? SourcegraphUri.parse(parentUri) : undefined, 0)
9891
} catch (error) {
9992
log.error(`getTreeItem(${uriString})`, error)
100-
return Promise.resolve({})
93+
return {}
10194
}
10295
}
10396

@@ -126,33 +119,73 @@ export class SourcegraphTreeDataProvider implements vscode.TreeDataProvider<stri
126119
}
127120
}
128121

129-
private async treeItemLabel(uri: SourcegraphUri): Promise<string> {
122+
private treeItemLabel(uri: SourcegraphUri, parent?: SourcegraphUri): string {
130123
if (uri.path) {
131-
return uri.basename()
132-
}
133-
const metadata = await this.fs.repositoryMetadata(uri.repositoryName, emptyCancelationToken())
134-
let revision = uri.revision
135-
if (metadata?.defaultBranch && (!revision || revision === metadata?.defaultOid)) {
136-
revision = metadata.defaultBranch
124+
if (parent?.path) {
125+
return uri.path.slice(parent.path.length + 1)
126+
}
127+
return uri.path
137128
}
138-
return `${uri.repositoryName}@${revision || ''}`
129+
return `${uri.repositoryName}${uri.revisionPart()}`
139130
}
140131

141-
private async getCollapsibleState(uri: SourcegraphUri): Promise<vscode.TreeItemCollapsibleState> {
142-
if (uri.isFile()) {
143-
return vscode.TreeItemCollapsibleState.None
144-
}
145-
const parentUri = uri.parentUri()
146-
if (parentUri) {
147-
const parent = SourcegraphUri.parse(parentUri)
148-
if (parent.path) {
149-
const tree = await this.fs.getFileTree(parent)
150-
const directChildren = tree.directChildren(parent.path)
151-
if (directChildren && directChildren.length === 1) {
152-
return vscode.TreeItemCollapsibleState.Expanded
153-
}
154-
}
132+
// private collapsibleState(uri: SourcegraphUri): vscode.TreeItemCollapsibleState {
133+
// if (uri.isFile()) {
134+
// return vscode.TreeItemCollapsibleState.None
135+
// }
136+
// const parentUri = uri.parentUri()
137+
// if (parentUri && this.treeItemCache.get(parentUri) === 1) {
138+
// return vscode.TreeItemCollapsibleState.Expanded
139+
// }
140+
// return vscode.TreeItemCollapsibleState.Collapsed
141+
// }
142+
// if (uri.isFile()) {
143+
// return vscode.TreeItemCollapsibleState.None
144+
// }
145+
// const parentUri = uri.parentUri()
146+
// let result = vscode.TreeItemCollapsibleState.Collapsed
147+
// if (parentUri) {
148+
// const parent = SourcegraphUri.parse(parentUri)
149+
// const tree = await this.fs.getFileTree(parent)
150+
// const fromCache = this.directChildrenCount.get(parentUri)
151+
// if (fromCache) {
152+
// return fromCache
153+
// }
154+
// if (parent.path) {
155+
// const directChildren = tree.directChildren(parent.path)
156+
// if (directChildren && directChildren.length === 1) {
157+
// result = vscode.TreeItemCollapsibleState.Expanded
158+
// }
159+
// }
160+
// this.directChildrenCount.set(parentUri, result)
161+
// }
162+
// return result
163+
// }
164+
private newTreeItem(
165+
uri: SourcegraphUri,
166+
parent: SourcegraphUri | undefined,
167+
parentChildrenCount: number
168+
): vscode.TreeItem {
169+
const command = uri.isFile()
170+
? {
171+
command: 'extension.openFile',
172+
title: 'Open file',
173+
arguments: [uri.uri],
174+
}
175+
: undefined
176+
const label = this.treeItemLabel(uri, parent)
177+
178+
return {
179+
id: uri.uri,
180+
label,
181+
tooltip: uri.uri.replace('sourcegraph://', 'https://'),
182+
collapsibleState: uri.isFile()
183+
? vscode.TreeItemCollapsibleState.None
184+
: parentChildrenCount === 1
185+
? vscode.TreeItemCollapsibleState.Expanded
186+
: vscode.TreeItemCollapsibleState.Collapsed,
187+
command,
188+
resourceUri: vscode.Uri.parse(uri.uri),
155189
}
156-
return vscode.TreeItemCollapsibleState.Collapsed
157190
}
158191
}

0 commit comments

Comments
 (0)