Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Commit 2426ea8

Browse files
committed
sketch up some new concepts
1 parent 5aec900 commit 2426ea8

File tree

3 files changed

+366
-1
lines changed

3 files changed

+366
-1
lines changed

src/inputs/locations.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { del, getPreviewChunks } from '../models';
8+
import { SymbolTreeInput } from '../tree';
9+
10+
export class LocationTreeInput implements SymbolTreeInput {
11+
12+
constructor(
13+
readonly title: string,
14+
readonly uri: vscode.Uri,
15+
readonly position: vscode.Position,
16+
private readonly _command: vscode.Command,
17+
) { }
18+
19+
resolve() {
20+
const result = Promise.resolve(vscode.commands.executeCommand<vscode.Location[] | vscode.LocationLink[]>(this._command.command, ...(this._command.arguments ?? [])));
21+
const model = new LocationsModel(result);
22+
23+
return {
24+
provider: model,
25+
get message() { return model.message; }
26+
};
27+
}
28+
}
29+
30+
class LocationsModel implements Required<vscode.TreeDataProvider<FileItem | ReferenceItem>>{
31+
32+
private _onDidChange = new vscode.EventEmitter<FileItem | ReferenceItem | undefined>();
33+
readonly onDidChangeTreeData = this._onDidChange.event;
34+
35+
_items?: FileItem[];
36+
_ready: Promise<any>;
37+
38+
constructor(locations: Promise<vscode.Location[] | vscode.LocationLink[] | undefined>) {
39+
40+
this._ready = locations.then(locations => {
41+
const items: FileItem[] = [];
42+
if (locations) {
43+
let last: FileItem | undefined;
44+
for (const item of locations.sort(LocationsModel._compareLocations)) {
45+
const loc = item instanceof vscode.Location
46+
? item
47+
: new vscode.Location(item.targetUri, item.targetRange);
48+
49+
if (!last || LocationsModel._compareUriIgnoreFragment(last.uri, loc.uri) !== 0) {
50+
last = new FileItem(loc.uri.with({ fragment: '' }), [], this);
51+
items.push(last);
52+
}
53+
last.references.push(new ReferenceItem(loc, last));
54+
}
55+
}
56+
this._items = items;
57+
});
58+
}
59+
60+
private static _compareUriIgnoreFragment(a: vscode.Uri, b: vscode.Uri): number {
61+
let aStr = a.with({ fragment: '' }).toString();
62+
let bStr = b.with({ fragment: '' }).toString();
63+
if (aStr < bStr) {
64+
return -1;
65+
} else if (aStr > bStr) {
66+
return 1;
67+
}
68+
return 0;
69+
}
70+
71+
private static _compareLocations(a: vscode.Location | vscode.LocationLink, b: vscode.Location | vscode.LocationLink): number {
72+
let aUri = a instanceof vscode.Location ? a.uri : a.targetUri;
73+
let bUri = b instanceof vscode.Location ? b.uri : b.targetUri;
74+
if (aUri.toString() < bUri.toString()) {
75+
return -1;
76+
} else if (aUri.toString() > bUri.toString()) {
77+
return 1;
78+
}
79+
80+
let aRange = a instanceof vscode.Location ? a.range : a.targetRange;
81+
let bRange = b instanceof vscode.Location ? b.range : b.targetRange;
82+
if (aRange.start.isBefore(bRange.start)) {
83+
return -1;
84+
} else if (aRange.start.isAfter(bRange.start)) {
85+
return 1;
86+
} else {
87+
return 0;
88+
}
89+
}
90+
91+
private _assertResolved(): asserts this is Required<LocationsModel> {
92+
if (!this._items) {
93+
throw Error('items NOT resolved yet');
94+
}
95+
}
96+
97+
// --- adapter
98+
99+
get message() {
100+
if (!this._items) {
101+
return undefined;
102+
}
103+
const total = this._items.reduce((prev, cur) => prev + cur.references.length, 0);
104+
const files = this._items.length;
105+
if (total === 1 && files === 1) {
106+
return `${total} result in ${files} file`;
107+
} else if (total === 1) {
108+
return `${total} result in ${files} files`;
109+
} else if (files === 1) {
110+
return `${total} results in ${files} file`;
111+
} else {
112+
return `${total} results in ${files} files`;
113+
}
114+
}
115+
116+
next(item: FileItem): FileItem {
117+
this._assertResolved();
118+
const idx = this._items.indexOf(item);
119+
const next = idx + 1 % this._items.length;
120+
return this._items[next];
121+
}
122+
123+
previous(item: FileItem): FileItem {
124+
this._assertResolved();
125+
const idx = this._items.indexOf(item);
126+
const prev = idx - 1 + this._items.length % this._items.length;
127+
return this._items[prev];
128+
}
129+
130+
remove(item: FileItem | ReferenceItem) {
131+
this._assertResolved();
132+
if (item instanceof FileItem) {
133+
del(this._items, item);
134+
this._onDidChange.fire(undefined);
135+
} else {
136+
del(item.file.references, item);
137+
if (item.file.references.length === 0) {
138+
del(this._items, item.file);
139+
this._onDidChange.fire(undefined);
140+
} else {
141+
this._onDidChange.fire(item.file);
142+
}
143+
}
144+
}
145+
146+
async asCopyText() {
147+
this._assertResolved();
148+
let result = '';
149+
for (const item of this._items) {
150+
result += `${await item.asCopyText()}\n`;
151+
}
152+
return result;
153+
}
154+
155+
// --- data provider mechanics
156+
157+
async getTreeItem(element: FileItem | ReferenceItem) {
158+
if (element instanceof FileItem) {
159+
// files
160+
const result = new vscode.TreeItem(element.uri);
161+
result.contextValue = 'file-item';
162+
result.description = true;
163+
result.iconPath = vscode.ThemeIcon.File;
164+
result.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
165+
return result;
166+
167+
} else {
168+
// references
169+
const { range } = element.location;
170+
const doc = await element.getDocument(true);
171+
const { before, inside, after } = getPreviewChunks(doc, range);
172+
173+
const label: vscode.TreeItemLabel = {
174+
label: before + inside + after,
175+
highlights: [[before.length, before.length + inside.length]]
176+
};
177+
178+
const result = new vscode.TreeItem2(label);
179+
result.collapsibleState = vscode.TreeItemCollapsibleState.None;
180+
result.contextValue = 'reference-item';
181+
result.command = { command: 'references-view.show', title: 'Open Reference', arguments: [element] };
182+
return result;
183+
}
184+
}
185+
186+
async getChildren(element?: FileItem | ReferenceItem) {
187+
188+
await this._ready;
189+
190+
if (!element) {
191+
return this._items;
192+
}
193+
if (element instanceof FileItem) {
194+
return element.references;
195+
}
196+
return undefined;
197+
}
198+
199+
getParent(element: FileItem | ReferenceItem) {
200+
return element instanceof ReferenceItem ? element.file : undefined;
201+
}
202+
}
203+
204+
class FileItem {
205+
206+
constructor(
207+
readonly uri: vscode.Uri,
208+
readonly references: Array<ReferenceItem>,
209+
readonly model: LocationsModel
210+
) { }
211+
212+
// --- adapter
213+
214+
remove(): void {
215+
this.model.remove(this);
216+
}
217+
218+
async asCopyText() {
219+
let result = `${vscode.workspace.asRelativePath(this.uri)}\n`;
220+
for (let ref of this.references) {
221+
result += ` ${await ref.asCopyText()}\n`;
222+
}
223+
return result;
224+
}
225+
}
226+
227+
class ReferenceItem {
228+
229+
private _document: Thenable<vscode.TextDocument> | undefined;
230+
231+
constructor(
232+
readonly location: vscode.Location,
233+
readonly file: FileItem,
234+
) { }
235+
236+
async getDocument(warmUpNext?: boolean) {
237+
if (!this._document) {
238+
this._document = vscode.workspace.openTextDocument(this.location.uri);
239+
}
240+
if (warmUpNext) {
241+
// load next document once this document has been loaded
242+
const next = this.file.model.next(this.file);
243+
if (next !== this.file) {
244+
vscode.workspace.openTextDocument(next.uri);
245+
}
246+
}
247+
return this._document;
248+
}
249+
250+
// --- adapter
251+
252+
remove(): void {
253+
this.file.model.remove(this);
254+
}
255+
256+
async asCopyText() {
257+
let doc = await this.getDocument();
258+
let chunks = getPreviewChunks(doc, this.location.range, 21, false);
259+
return `${this.location.range.start.line + 1}, ${this.location.range.start.character + 1}: ${chunks.before + chunks.inside + chunks.after}`;
260+
}
261+
}

src/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode';
77
import { HistoryItem, WordAnchor } from './history';
88

9-
function del<T>(array: T[], e: T): void {
9+
export function del<T>(array: T[], e: T): void {
1010
const idx = array.indexOf(e);
1111
if (idx >= 0) {
1212
array.splice(idx, 1);

src/tree.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
8+
interface ActiveTreeDataProviderWrapper {
9+
provider: Required<vscode.TreeDataProvider<any>>;
10+
}
11+
12+
class TreeDataProviderDelegate implements vscode.TreeDataProvider<undefined> {
13+
14+
provider?: Required<vscode.TreeDataProvider<any>>;
15+
16+
private _providerListener?: vscode.Disposable;
17+
private _onDidChange = new vscode.EventEmitter<any>();
18+
19+
readonly onDidChangeTreeData = this._onDidChange.event;
20+
21+
update(provider: Required<vscode.TreeDataProvider<any>>) {
22+
23+
this._providerListener?.dispose();
24+
this._providerListener = undefined;
25+
26+
this.provider = provider;
27+
this._providerListener = provider.onDidChangeTreeData
28+
? provider.onDidChangeTreeData(this._onDidChange.fire, this._onDidChange)
29+
: undefined;
30+
31+
this._onDidChange.fire();
32+
}
33+
34+
getTreeItem(element: unknown): vscode.TreeItem | Thenable<vscode.TreeItem> {
35+
this._assertProvider();
36+
return this.provider.getTreeItem(element);
37+
}
38+
39+
getChildren(parent?: unknown | undefined) {
40+
this._assertProvider();
41+
return this.provider.getChildren(parent);
42+
}
43+
44+
getParent(element: unknown) {
45+
this._assertProvider();
46+
return this.provider.getParent(element);
47+
}
48+
49+
private _assertProvider(): asserts this is ActiveTreeDataProviderWrapper {
50+
if (!this.provider) {
51+
throw new Error('MISSING provider');
52+
}
53+
}
54+
}
55+
56+
export interface SymbolTreeInput {
57+
58+
title: string;
59+
uri: vscode.Uri;
60+
position: vscode.Position;
61+
62+
resolve(): {
63+
message: string | undefined,
64+
provider: Required<vscode.TreeDataProvider<unknown>>;
65+
};
66+
}
67+
68+
export class SymbolsTree {
69+
70+
readonly viewId = 'references-view.tree';
71+
72+
private readonly _tree: vscode.TreeView<unknown>;
73+
private readonly _provider = new TreeDataProviderDelegate();
74+
75+
private _sessionDisposable?: vscode.Disposable;
76+
77+
constructor() {
78+
this._tree = vscode.window.createTreeView<unknown>(this.viewId, {
79+
treeDataProvider: this._provider,
80+
showCollapseAll: true
81+
});
82+
}
83+
84+
setInput(input: SymbolTreeInput) {
85+
86+
this._sessionDisposable?.dispose();
87+
const listener: vscode.Disposable[] = [];
88+
89+
this._tree.title = input.title;
90+
const model = input.resolve();
91+
this._provider.update(model.provider);
92+
93+
listener.push(model.provider.onDidChangeTreeData(() => {
94+
this._tree.title = input.title;
95+
this._tree.message = model.message;
96+
}));
97+
98+
if (typeof ((model.provider as unknown) as vscode.Disposable).dispose === 'function') {
99+
listener.push((model.provider as unknown) as vscode.Disposable);
100+
}
101+
102+
this._sessionDisposable = vscode.Disposable.from(...listener);
103+
}
104+
}

0 commit comments

Comments
 (0)