Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { onUnexpectedError } from '../../../../base/common/errors.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { IMatch } from '../../../../base/common/filters.js';8import { defaultGenerator } from '../../../../base/common/idGenerator.js';9import { dispose, IDisposable, IReference } from '../../../../base/common/lifecycle.js';10import { ResourceMap } from '../../../../base/common/map.js';11import { basename, extUri } from '../../../../base/common/resources.js';12import * as strings from '../../../../base/common/strings.js';13import { Constants } from '../../../../base/common/uint.js';14import { URI } from '../../../../base/common/uri.js';15import { Position } from '../../../common/core/position.js';16import { IRange, Range } from '../../../common/core/range.js';17import { Location, LocationLink } from '../../../common/languages.js';18import { ITextEditorModel, ITextModelService } from '../../../common/services/resolverService.js';19import { localize } from '../../../../nls.js';2021export class OneReference {2223readonly id: string = defaultGenerator.nextId();2425private _range?: IRange;2627constructor(28readonly isProviderFirst: boolean,29readonly parent: FileReferences,30readonly link: LocationLink,31private _rangeCallback: (ref: OneReference) => void32) { }3334get uri() {35return this.link.uri;36}3738get range(): IRange {39return this._range ?? this.link.targetSelectionRange ?? this.link.range;40}4142set range(value: IRange) {43this._range = value;44this._rangeCallback(this);45}4647get ariaMessage(): string {4849const preview = this.parent.getPreview(this)?.preview(this.range);5051if (!preview) {52return localize(53'aria.oneReference', "in {0} on line {1} at column {2}",54basename(this.uri), this.range.startLineNumber, this.range.startColumn55);56} else {57return localize(58{ 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}",59preview.value, basename(this.uri), this.range.startLineNumber, this.range.startColumn60);61}62}63}6465export class FilePreview implements IDisposable {6667constructor(68private readonly _modelReference: IReference<ITextEditorModel>69) { }7071dispose(): void {72this._modelReference.dispose();73}7475preview(range: IRange, n: number = 8): { value: string; highlight: IMatch } | undefined {76const model = this._modelReference.object.textEditorModel;7778if (!model) {79return undefined;80}8182const { startLineNumber, startColumn, endLineNumber, endColumn } = range;83const word = model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n });84const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn);85const afterRange = new Range(endLineNumber, endColumn, endLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);8687const before = model.getValueInRange(beforeRange).replace(/^\s+/, '');88const inside = model.getValueInRange(range);89const after = model.getValueInRange(afterRange).replace(/\s+$/, '');9091return {92value: before + inside + after,93highlight: { start: before.length, end: before.length + inside.length }94};95}96}9798export class FileReferences implements IDisposable {99100readonly children: OneReference[] = [];101102private _previews = new ResourceMap<FilePreview>();103104constructor(105readonly parent: ReferencesModel,106readonly uri: URI107) { }108109dispose(): void {110dispose(this._previews.values());111this._previews.clear();112}113114getPreview(child: OneReference): FilePreview | undefined {115return this._previews.get(child.uri);116}117118get ariaMessage(): string {119const len = this.children.length;120if (len === 1) {121return localize('aria.fileReferences.1', "1 symbol in {0}, full path {1}", basename(this.uri), this.uri.fsPath);122} else {123return localize('aria.fileReferences.N', "{0} symbols in {1}, full path {2}", len, basename(this.uri), this.uri.fsPath);124}125}126127async resolve(textModelResolverService: ITextModelService): Promise<FileReferences> {128if (this._previews.size !== 0) {129return this;130}131for (const child of this.children) {132if (this._previews.has(child.uri)) {133continue;134}135try {136const ref = await textModelResolverService.createModelReference(child.uri);137this._previews.set(child.uri, new FilePreview(ref));138} catch (err) {139onUnexpectedError(err);140}141}142return this;143}144}145146export class ReferencesModel implements IDisposable {147148private readonly _links: LocationLink[];149private readonly _title: string;150151readonly groups: FileReferences[] = [];152readonly references: OneReference[] = [];153154readonly _onDidChangeReferenceRange = new Emitter<OneReference>();155readonly onDidChangeReferenceRange: Event<OneReference> = this._onDidChangeReferenceRange.event;156157constructor(links: LocationLink[], title: string) {158this._links = links;159this._title = title;160161// grouping and sorting162const [providersFirst] = links;163links.sort(ReferencesModel._compareReferences);164165let current: FileReferences | undefined;166for (const link of links) {167if (!current || !extUri.isEqual(current.uri, link.uri, true)) {168// new group169current = new FileReferences(this, link.uri);170this.groups.push(current);171}172173// append, check for equality first!174if (current.children.length === 0 || ReferencesModel._compareReferences(link, current.children[current.children.length - 1]) !== 0) {175176const oneRef = new OneReference(177providersFirst === link,178current,179link,180ref => this._onDidChangeReferenceRange.fire(ref)181);182this.references.push(oneRef);183current.children.push(oneRef);184}185}186}187188dispose(): void {189dispose(this.groups);190this._onDidChangeReferenceRange.dispose();191this.groups.length = 0;192}193194clone(): ReferencesModel {195return new ReferencesModel(this._links, this._title);196}197198get title(): string {199return this._title;200}201202get isEmpty(): boolean {203return this.groups.length === 0;204}205206get ariaMessage(): string {207if (this.isEmpty) {208return localize('aria.result.0', "No results found");209} else if (this.references.length === 1) {210return localize('aria.result.1', "Found 1 symbol in {0}", this.references[0].uri.fsPath);211} else if (this.groups.length === 1) {212return localize('aria.result.n1', "Found {0} symbols in {1}", this.references.length, this.groups[0].uri.fsPath);213} else {214return localize('aria.result.nm', "Found {0} symbols in {1} files", this.references.length, this.groups.length);215}216}217218nextOrPreviousReference(reference: OneReference, next: boolean): OneReference {219220const { parent } = reference;221222let idx = parent.children.indexOf(reference);223const childCount = parent.children.length;224const groupCount = parent.parent.groups.length;225226if (groupCount === 1 || next && idx + 1 < childCount || !next && idx > 0) {227// cycling within one file228if (next) {229idx = (idx + 1) % childCount;230} else {231idx = (idx + childCount - 1) % childCount;232}233return parent.children[idx];234}235236idx = parent.parent.groups.indexOf(parent);237if (next) {238idx = (idx + 1) % groupCount;239return parent.parent.groups[idx].children[0];240} else {241idx = (idx + groupCount - 1) % groupCount;242return parent.parent.groups[idx].children[parent.parent.groups[idx].children.length - 1];243}244}245246nearestReference(resource: URI, position: Position): OneReference | undefined {247248const nearest = this.references.map((ref, idx) => {249return {250idx,251prefixLen: strings.commonPrefixLength(ref.uri.toString(), resource.toString()),252offsetDist: Math.abs(ref.range.startLineNumber - position.lineNumber) * 100 + Math.abs(ref.range.startColumn - position.column)253};254}).sort((a, b) => {255if (a.prefixLen > b.prefixLen) {256return -1;257} else if (a.prefixLen < b.prefixLen) {258return 1;259} else if (a.offsetDist < b.offsetDist) {260return -1;261} else if (a.offsetDist > b.offsetDist) {262return 1;263} else {264return 0;265}266})[0];267268if (nearest) {269return this.references[nearest.idx];270}271return undefined;272}273274referenceAt(resource: URI, position: Position): OneReference | undefined {275for (const ref of this.references) {276if (ref.uri.toString() === resource.toString()) {277if (Range.containsPosition(ref.range, position)) {278return ref;279}280}281}282return undefined;283}284285firstReference(): OneReference | undefined {286for (const ref of this.references) {287if (ref.isProviderFirst) {288return ref;289}290}291return this.references[0];292}293294private static _compareReferences(a: Location, b: Location): number {295return extUri.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range);296}297}298299300