Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts
3296 views
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 { onUnexpectedError } from '../../../../base/common/errors.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { IMatch } from '../../../../base/common/filters.js';
9
import { defaultGenerator } from '../../../../base/common/idGenerator.js';
10
import { dispose, IDisposable, IReference } from '../../../../base/common/lifecycle.js';
11
import { ResourceMap } from '../../../../base/common/map.js';
12
import { basename, extUri } from '../../../../base/common/resources.js';
13
import * as strings from '../../../../base/common/strings.js';
14
import { Constants } from '../../../../base/common/uint.js';
15
import { URI } from '../../../../base/common/uri.js';
16
import { Position } from '../../../common/core/position.js';
17
import { IRange, Range } from '../../../common/core/range.js';
18
import { Location, LocationLink } from '../../../common/languages.js';
19
import { ITextEditorModel, ITextModelService } from '../../../common/services/resolverService.js';
20
import { localize } from '../../../../nls.js';
21
22
export class OneReference {
23
24
readonly id: string = defaultGenerator.nextId();
25
26
private _range?: IRange;
27
28
constructor(
29
readonly isProviderFirst: boolean,
30
readonly parent: FileReferences,
31
readonly link: LocationLink,
32
private _rangeCallback: (ref: OneReference) => void
33
) { }
34
35
get uri() {
36
return this.link.uri;
37
}
38
39
get range(): IRange {
40
return this._range ?? this.link.targetSelectionRange ?? this.link.range;
41
}
42
43
set range(value: IRange) {
44
this._range = value;
45
this._rangeCallback(this);
46
}
47
48
get ariaMessage(): string {
49
50
const preview = this.parent.getPreview(this)?.preview(this.range);
51
52
if (!preview) {
53
return localize(
54
'aria.oneReference', "in {0} on line {1} at column {2}",
55
basename(this.uri), this.range.startLineNumber, this.range.startColumn
56
);
57
} else {
58
return localize(
59
{ key: 'aria.oneReference.preview', comment: ['Placeholders are: 0: filename, 1:line number, 2: column number, 3: preview snippet of source code'] }, "{0} in {1} on line {2} at column {3}",
60
preview.value, basename(this.uri), this.range.startLineNumber, this.range.startColumn
61
);
62
}
63
}
64
}
65
66
export class FilePreview implements IDisposable {
67
68
constructor(
69
private readonly _modelReference: IReference<ITextEditorModel>
70
) { }
71
72
dispose(): void {
73
this._modelReference.dispose();
74
}
75
76
preview(range: IRange, n: number = 8): { value: string; highlight: IMatch } | undefined {
77
const model = this._modelReference.object.textEditorModel;
78
79
if (!model) {
80
return undefined;
81
}
82
83
const { startLineNumber, startColumn, endLineNumber, endColumn } = range;
84
const word = model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n });
85
const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn);
86
const afterRange = new Range(endLineNumber, endColumn, endLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);
87
88
const before = model.getValueInRange(beforeRange).replace(/^\s+/, '');
89
const inside = model.getValueInRange(range);
90
const after = model.getValueInRange(afterRange).replace(/\s+$/, '');
91
92
return {
93
value: before + inside + after,
94
highlight: { start: before.length, end: before.length + inside.length }
95
};
96
}
97
}
98
99
export class FileReferences implements IDisposable {
100
101
readonly children: OneReference[] = [];
102
103
private _previews = new ResourceMap<FilePreview>();
104
105
constructor(
106
readonly parent: ReferencesModel,
107
readonly uri: URI
108
) { }
109
110
dispose(): void {
111
dispose(this._previews.values());
112
this._previews.clear();
113
}
114
115
getPreview(child: OneReference): FilePreview | undefined {
116
return this._previews.get(child.uri);
117
}
118
119
get ariaMessage(): string {
120
const len = this.children.length;
121
if (len === 1) {
122
return localize('aria.fileReferences.1', "1 symbol in {0}, full path {1}", basename(this.uri), this.uri.fsPath);
123
} else {
124
return localize('aria.fileReferences.N', "{0} symbols in {1}, full path {2}", len, basename(this.uri), this.uri.fsPath);
125
}
126
}
127
128
async resolve(textModelResolverService: ITextModelService): Promise<FileReferences> {
129
if (this._previews.size !== 0) {
130
return this;
131
}
132
for (const child of this.children) {
133
if (this._previews.has(child.uri)) {
134
continue;
135
}
136
try {
137
const ref = await textModelResolverService.createModelReference(child.uri);
138
this._previews.set(child.uri, new FilePreview(ref));
139
} catch (err) {
140
onUnexpectedError(err);
141
}
142
}
143
return this;
144
}
145
}
146
147
export class ReferencesModel implements IDisposable {
148
149
private readonly _links: LocationLink[];
150
private readonly _title: string;
151
152
readonly groups: FileReferences[] = [];
153
readonly references: OneReference[] = [];
154
155
readonly _onDidChangeReferenceRange = new Emitter<OneReference>();
156
readonly onDidChangeReferenceRange: Event<OneReference> = this._onDidChangeReferenceRange.event;
157
158
constructor(links: LocationLink[], title: string) {
159
this._links = links;
160
this._title = title;
161
162
// grouping and sorting
163
const [providersFirst] = links;
164
links.sort(ReferencesModel._compareReferences);
165
166
let current: FileReferences | undefined;
167
for (const link of links) {
168
if (!current || !extUri.isEqual(current.uri, link.uri, true)) {
169
// new group
170
current = new FileReferences(this, link.uri);
171
this.groups.push(current);
172
}
173
174
// append, check for equality first!
175
if (current.children.length === 0 || ReferencesModel._compareReferences(link, current.children[current.children.length - 1]) !== 0) {
176
177
const oneRef = new OneReference(
178
providersFirst === link,
179
current,
180
link,
181
ref => this._onDidChangeReferenceRange.fire(ref)
182
);
183
this.references.push(oneRef);
184
current.children.push(oneRef);
185
}
186
}
187
}
188
189
dispose(): void {
190
dispose(this.groups);
191
this._onDidChangeReferenceRange.dispose();
192
this.groups.length = 0;
193
}
194
195
clone(): ReferencesModel {
196
return new ReferencesModel(this._links, this._title);
197
}
198
199
get title(): string {
200
return this._title;
201
}
202
203
get isEmpty(): boolean {
204
return this.groups.length === 0;
205
}
206
207
get ariaMessage(): string {
208
if (this.isEmpty) {
209
return localize('aria.result.0', "No results found");
210
} else if (this.references.length === 1) {
211
return localize('aria.result.1', "Found 1 symbol in {0}", this.references[0].uri.fsPath);
212
} else if (this.groups.length === 1) {
213
return localize('aria.result.n1', "Found {0} symbols in {1}", this.references.length, this.groups[0].uri.fsPath);
214
} else {
215
return localize('aria.result.nm', "Found {0} symbols in {1} files", this.references.length, this.groups.length);
216
}
217
}
218
219
nextOrPreviousReference(reference: OneReference, next: boolean): OneReference {
220
221
const { parent } = reference;
222
223
let idx = parent.children.indexOf(reference);
224
const childCount = parent.children.length;
225
const groupCount = parent.parent.groups.length;
226
227
if (groupCount === 1 || next && idx + 1 < childCount || !next && idx > 0) {
228
// cycling within one file
229
if (next) {
230
idx = (idx + 1) % childCount;
231
} else {
232
idx = (idx + childCount - 1) % childCount;
233
}
234
return parent.children[idx];
235
}
236
237
idx = parent.parent.groups.indexOf(parent);
238
if (next) {
239
idx = (idx + 1) % groupCount;
240
return parent.parent.groups[idx].children[0];
241
} else {
242
idx = (idx + groupCount - 1) % groupCount;
243
return parent.parent.groups[idx].children[parent.parent.groups[idx].children.length - 1];
244
}
245
}
246
247
nearestReference(resource: URI, position: Position): OneReference | undefined {
248
249
const nearest = this.references.map((ref, idx) => {
250
return {
251
idx,
252
prefixLen: strings.commonPrefixLength(ref.uri.toString(), resource.toString()),
253
offsetDist: Math.abs(ref.range.startLineNumber - position.lineNumber) * 100 + Math.abs(ref.range.startColumn - position.column)
254
};
255
}).sort((a, b) => {
256
if (a.prefixLen > b.prefixLen) {
257
return -1;
258
} else if (a.prefixLen < b.prefixLen) {
259
return 1;
260
} else if (a.offsetDist < b.offsetDist) {
261
return -1;
262
} else if (a.offsetDist > b.offsetDist) {
263
return 1;
264
} else {
265
return 0;
266
}
267
})[0];
268
269
if (nearest) {
270
return this.references[nearest.idx];
271
}
272
return undefined;
273
}
274
275
referenceAt(resource: URI, position: Position): OneReference | undefined {
276
for (const ref of this.references) {
277
if (ref.uri.toString() === resource.toString()) {
278
if (Range.containsPosition(ref.range, position)) {
279
return ref;
280
}
281
}
282
}
283
return undefined;
284
}
285
286
firstReference(): OneReference | undefined {
287
for (const ref of this.references) {
288
if (ref.isProviderFirst) {
289
return ref;
290
}
291
}
292
return this.references[0];
293
}
294
295
private static _compareReferences(a: Location, b: Location): number {
296
return extUri.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range);
297
}
298
}
299
300