mirror of https://github.com/microsoft/vscode.git
Moves ARC telemetry tracking to core (#253919)
This commit is contained in:
parent
cc1eecb9f0
commit
c6f916e443
|
@ -27,7 +27,7 @@ export { observableFromEventOpts } from './observables/observableFromEvent.js';
|
|||
export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js';
|
||||
export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js';
|
||||
export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js';
|
||||
export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore } from './utils/runOnChange.js';
|
||||
export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore, type RemoveUndefined } from './utils/runOnChange.js';
|
||||
export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js';
|
||||
export { observableFromEvent } from './observables/observableFromEvent.js';
|
||||
export { observableValue } from './observables/observableValue.js';
|
||||
|
|
|
@ -33,6 +33,8 @@ import { mainWindow } from '../../../base/browser/window.js';
|
|||
import { WindowIntervalTimer } from '../../../base/browser/dom.js';
|
||||
import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js';
|
||||
import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js';
|
||||
import { StringEdit } from '../../common/core/edits/stringEdit.js';
|
||||
import { OffsetRange } from '../../common/core/ranges/offsetRange.js';
|
||||
|
||||
/**
|
||||
* Stop the worker if it was not needed for 5 min.
|
||||
|
@ -180,6 +182,17 @@ export abstract class EditorWorkerService extends Disposable implements IEditorW
|
|||
}
|
||||
}
|
||||
|
||||
public async computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit> {
|
||||
try {
|
||||
const worker = await this._workerWithResources([]);
|
||||
const edit = await worker.$computeStringDiff(original, modified, options, algorithm);
|
||||
return StringEdit.fromJson(edit);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return StringEdit.replace(OffsetRange.ofLength(original.length), modified); // approximation
|
||||
}
|
||||
}
|
||||
|
||||
public canNavigateValueSet(resource: URI): boolean {
|
||||
return (canSyncModel(this._modelService, resource));
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export abstract class BaseEdit<T extends BaseReplacement<T>, TEdit extends BaseE
|
|||
* Normalizes the edit by removing empty replacements and joining touching replacements (if the replacements allow joining).
|
||||
* Two edits have an equal normalized edit if and only if they have the same effect on any input.
|
||||
*
|
||||
* 
|
||||
* 
|
||||
*
|
||||
* Invariant:
|
||||
* ```
|
||||
|
@ -90,7 +90,7 @@ export abstract class BaseEdit<T extends BaseReplacement<T>, TEdit extends BaseE
|
|||
/**
|
||||
* Combines two edits into one with the same effect.
|
||||
*
|
||||
* 
|
||||
* 
|
||||
*
|
||||
* Invariant:
|
||||
* ```
|
||||
|
@ -183,6 +183,22 @@ export abstract class BaseEdit<T extends BaseReplacement<T>, TEdit extends BaseE
|
|||
return this._createNew(result).normalize();
|
||||
}
|
||||
|
||||
public decomposeSplit(shouldBeInE1: (repl: T) => boolean): { e1: TEdit; e2: TEdit } {
|
||||
const e1: T[] = [];
|
||||
const e2: T[] = [];
|
||||
|
||||
let e2delta = 0;
|
||||
for (const edit of this.replacements) {
|
||||
if (shouldBeInE1(edit)) {
|
||||
e1.push(edit);
|
||||
e2delta += edit.getNewLength() - edit.replaceRange.length;
|
||||
} else {
|
||||
e2.push(edit.slice(edit.replaceRange.delta(e2delta), new OffsetRange(0, edit.getNewLength())));
|
||||
}
|
||||
}
|
||||
return { e1: this._createNew(e1), e2: this._createNew(e2) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the range of each replacement in the applied value.
|
||||
*/
|
||||
|
|
|
@ -5,58 +5,26 @@
|
|||
|
||||
import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js';
|
||||
import { OffsetRange } from '../ranges/offsetRange.js';
|
||||
import { StringText } from '../text/abstractText.js';
|
||||
import { BaseEdit, BaseReplacement } from './edit.js';
|
||||
|
||||
/**
|
||||
* Represents a set of replacements to a string.
|
||||
* All these replacements are applied at once.
|
||||
*/
|
||||
export class StringEdit extends BaseEdit<StringReplacement, StringEdit> {
|
||||
public static readonly empty = new StringEdit([]);
|
||||
|
||||
public static create(replacements: readonly StringReplacement[]): StringEdit {
|
||||
return new StringEdit(replacements);
|
||||
export abstract class BaseStringEdit<T extends BaseStringReplacement<T> = BaseStringReplacement<any>, TEdit extends BaseStringEdit<T, TEdit> = BaseStringEdit<any, any>> extends BaseEdit<T, TEdit> {
|
||||
get TReplacement(): T {
|
||||
throw new Error('TReplacement is not defined for BaseStringEdit');
|
||||
}
|
||||
|
||||
public static single(replacement: StringReplacement): StringEdit {
|
||||
return new StringEdit([replacement]);
|
||||
}
|
||||
|
||||
public static replace(range: OffsetRange, replacement: string): StringEdit {
|
||||
return new StringEdit([new StringReplacement(range, replacement)]);
|
||||
}
|
||||
|
||||
public static insert(offset: number, replacement: string): StringEdit {
|
||||
return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]);
|
||||
}
|
||||
|
||||
public static delete(range: OffsetRange): StringEdit {
|
||||
return new StringEdit([new StringReplacement(range, '')]);
|
||||
}
|
||||
|
||||
public static fromJson(data: ISerializedStringEdit): StringEdit {
|
||||
return new StringEdit(data.map(StringReplacement.fromJson));
|
||||
}
|
||||
|
||||
public static compose(edits: readonly StringEdit[]): StringEdit {
|
||||
public static composeOrUndefined<T extends BaseStringEdit>(edits: readonly T[]): T | undefined {
|
||||
if (edits.length === 0) {
|
||||
return StringEdit.empty;
|
||||
return undefined;
|
||||
}
|
||||
let result = edits[0];
|
||||
for (let i = 1; i < edits.length; i++) {
|
||||
result = result.compose(edits[i]);
|
||||
result = result.compose(edits[i]) as any;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(replacements: readonly StringReplacement[]) {
|
||||
super(replacements);
|
||||
}
|
||||
|
||||
protected override _createNew(replacements: readonly StringReplacement[]): StringEdit {
|
||||
return new StringEdit(replacements);
|
||||
}
|
||||
|
||||
public apply(base: string): string {
|
||||
const resultText: string[] = [];
|
||||
let pos = 0;
|
||||
|
@ -162,39 +130,41 @@ export class StringEdit extends BaseEdit<StringReplacement, StringEdit> {
|
|||
public normalizeEOL(eol: '\r\n' | '\n'): StringEdit {
|
||||
return new StringEdit(this.replacements.map(edit => edit.normalizeEOL(eol)));
|
||||
}
|
||||
|
||||
/**
|
||||
* If `e1.apply(source) === e2.apply(source)`, then `e1.normalizeOnSource(source).equals(e2.normalizeOnSource(source))`.
|
||||
*/
|
||||
public normalizeOnSource(source: string): StringEdit {
|
||||
const result = this.apply(source);
|
||||
|
||||
const edit = StringReplacement.replace(OffsetRange.ofLength(source.length), result);
|
||||
const e = edit.removeCommonSuffixAndPrefix(source);
|
||||
if (e.isEmpty) {
|
||||
return StringEdit.empty;
|
||||
}
|
||||
return e.toEdit();
|
||||
}
|
||||
|
||||
removeCommonSuffixAndPrefix(source: string): TEdit {
|
||||
return this._createNew(this.replacements.map(e => e.removeCommonSuffixAndPrefix(source))).normalize();
|
||||
}
|
||||
|
||||
applyOnText(docContents: StringText): StringText {
|
||||
return new StringText(this.apply(docContents.value));
|
||||
}
|
||||
|
||||
public mapData<TData extends IEditData<TData>>(f: (replacement: T) => TData): AnnotatedStringEdit<TData> {
|
||||
return new AnnotatedStringEdit(
|
||||
this.replacements.map(e => new AnnotatedStringReplacement(
|
||||
e.replaceRange,
|
||||
e.newText,
|
||||
f(e)
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: Be careful when changing this type, as it is used for serialization!
|
||||
*/
|
||||
export type ISerializedStringEdit = ISerializedStringReplacement[];
|
||||
|
||||
/**
|
||||
* Warning: Be careful when changing this type, as it is used for serialization!
|
||||
*/
|
||||
export interface ISerializedStringReplacement {
|
||||
txt: string;
|
||||
pos: number;
|
||||
len: number;
|
||||
}
|
||||
|
||||
export class StringReplacement extends BaseReplacement<StringReplacement> {
|
||||
public static insert(offset: number, text: string): StringReplacement {
|
||||
return new StringReplacement(OffsetRange.emptyAt(offset), text);
|
||||
}
|
||||
|
||||
public static replace(range: OffsetRange, text: string): StringReplacement {
|
||||
return new StringReplacement(range, text);
|
||||
}
|
||||
|
||||
public static delete(range: OffsetRange): StringReplacement {
|
||||
return new StringReplacement(range, '');
|
||||
}
|
||||
|
||||
public static fromJson(data: ISerializedStringReplacement): StringReplacement {
|
||||
return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt);
|
||||
}
|
||||
|
||||
export abstract class BaseStringReplacement<T extends BaseStringReplacement<T> = BaseStringReplacement<any>> extends BaseReplacement<T> {
|
||||
constructor(
|
||||
range: OffsetRange,
|
||||
public readonly newText: string,
|
||||
|
@ -202,20 +172,8 @@ export class StringReplacement extends BaseReplacement<StringReplacement> {
|
|||
super(range);
|
||||
}
|
||||
|
||||
override equals(other: StringReplacement): boolean {
|
||||
return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText;
|
||||
}
|
||||
|
||||
getNewLength(): number { return this.newText.length; }
|
||||
|
||||
tryJoinTouching(other: StringReplacement): StringReplacement | undefined {
|
||||
return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText);
|
||||
}
|
||||
|
||||
slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement {
|
||||
return new StringReplacement(range, rangeInReplacement.substring(this.newText));
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `${this.replaceRange} -> "${this.newText}"`;
|
||||
}
|
||||
|
@ -254,6 +212,155 @@ export class StringReplacement extends BaseReplacement<StringReplacement> {
|
|||
const newText = this.newText.replace(/\r\n|\n/g, eol);
|
||||
return new StringReplacement(this.replaceRange, newText);
|
||||
}
|
||||
|
||||
public removeCommonSuffixAndPrefix(source: string): T {
|
||||
return this.removeCommonSuffix(source).removeCommonPrefix(source);
|
||||
}
|
||||
|
||||
public removeCommonPrefix(source: string): T {
|
||||
const oldText = this.replaceRange.substring(source);
|
||||
|
||||
const prefixLen = commonPrefixLength(oldText, this.newText);
|
||||
if (prefixLen === 0) {
|
||||
return this as unknown as T;
|
||||
}
|
||||
|
||||
return this.slice(this.replaceRange.deltaStart(prefixLen), new OffsetRange(prefixLen, this.newText.length));
|
||||
}
|
||||
|
||||
public removeCommonSuffix(source: string): T {
|
||||
const oldText = this.replaceRange.substring(source);
|
||||
|
||||
const suffixLen = commonSuffixLength(oldText, this.newText);
|
||||
if (suffixLen === 0) {
|
||||
return this as unknown as T;
|
||||
}
|
||||
return this.slice(this.replaceRange.deltaEnd(-suffixLen), new OffsetRange(0, this.newText.length - suffixLen));
|
||||
}
|
||||
|
||||
public toEdit(): StringEdit {
|
||||
return new StringEdit([this]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a set of replacements to a string.
|
||||
* All these replacements are applied at once.
|
||||
*/
|
||||
export class StringEdit extends BaseStringEdit<StringReplacement, StringEdit> {
|
||||
public static readonly empty = new StringEdit([]);
|
||||
|
||||
public static create(replacements: readonly StringReplacement[]): StringEdit {
|
||||
return new StringEdit(replacements);
|
||||
}
|
||||
|
||||
public static single(replacement: StringReplacement): StringEdit {
|
||||
return new StringEdit([replacement]);
|
||||
}
|
||||
|
||||
public static replace(range: OffsetRange, replacement: string): StringEdit {
|
||||
return new StringEdit([new StringReplacement(range, replacement)]);
|
||||
}
|
||||
|
||||
public static insert(offset: number, replacement: string): StringEdit {
|
||||
return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]);
|
||||
}
|
||||
|
||||
public static delete(range: OffsetRange): StringEdit {
|
||||
return new StringEdit([new StringReplacement(range, '')]);
|
||||
}
|
||||
|
||||
public static fromJson(data: ISerializedStringEdit): StringEdit {
|
||||
return new StringEdit(data.map(StringReplacement.fromJson));
|
||||
}
|
||||
|
||||
public static compose(edits: readonly StringEdit[]): StringEdit {
|
||||
if (edits.length === 0) {
|
||||
return StringEdit.empty;
|
||||
}
|
||||
let result = edits[0];
|
||||
for (let i = 1; i < edits.length; i++) {
|
||||
result = result.compose(edits[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The replacements are applied in order!
|
||||
* Equals `StringEdit.compose(replacements.map(r => r.toEdit()))`, but is much more performant.
|
||||
*/
|
||||
public static composeSequentialReplacements(replacements: readonly StringReplacement[]): StringEdit {
|
||||
let edit = StringEdit.empty;
|
||||
let curEditReplacements: StringReplacement[] = []; // These are reverse sorted
|
||||
|
||||
for (const r of replacements) {
|
||||
const last = curEditReplacements.at(-1);
|
||||
if (!last || r.replaceRange.isBefore(last.replaceRange)) {
|
||||
// Detect subsequences of reverse sorted replacements
|
||||
curEditReplacements.push(r);
|
||||
} else {
|
||||
// Once the subsequence is broken, compose the current replacements and look for a new subsequence.
|
||||
edit = edit.compose(StringEdit.create(curEditReplacements.reverse()));
|
||||
curEditReplacements = [r];
|
||||
}
|
||||
}
|
||||
|
||||
edit = edit.compose(StringEdit.create(curEditReplacements.reverse()));
|
||||
return edit;
|
||||
}
|
||||
|
||||
constructor(replacements: readonly StringReplacement[]) {
|
||||
super(replacements);
|
||||
}
|
||||
|
||||
protected override _createNew(replacements: readonly StringReplacement[]): StringEdit {
|
||||
return new StringEdit(replacements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: Be careful when changing this type, as it is used for serialization!
|
||||
*/
|
||||
export type ISerializedStringEdit = ISerializedStringReplacement[];
|
||||
|
||||
/**
|
||||
* Warning: Be careful when changing this type, as it is used for serialization!
|
||||
*/
|
||||
export interface ISerializedStringReplacement {
|
||||
txt: string;
|
||||
pos: number;
|
||||
len: number;
|
||||
}
|
||||
|
||||
export class StringReplacement extends BaseStringReplacement<StringReplacement> {
|
||||
public static insert(offset: number, text: string): StringReplacement {
|
||||
return new StringReplacement(OffsetRange.emptyAt(offset), text);
|
||||
}
|
||||
|
||||
public static replace(range: OffsetRange, text: string): StringReplacement {
|
||||
return new StringReplacement(range, text);
|
||||
}
|
||||
|
||||
public static delete(range: OffsetRange): StringReplacement {
|
||||
return new StringReplacement(range, '');
|
||||
}
|
||||
|
||||
public static fromJson(data: ISerializedStringReplacement): StringReplacement {
|
||||
return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt);
|
||||
}
|
||||
|
||||
override equals(other: StringReplacement): boolean {
|
||||
return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText;
|
||||
}
|
||||
|
||||
override tryJoinTouching(other: StringReplacement): StringReplacement | undefined {
|
||||
return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText);
|
||||
}
|
||||
|
||||
override slice(range: OffsetRange, rangeInReplacement: OffsetRange): StringReplacement {
|
||||
return new StringReplacement(range, rangeInReplacement.substring(this.newText));
|
||||
}
|
||||
}
|
||||
|
||||
export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit): OffsetRange[] {
|
||||
|
@ -322,3 +429,106 @@ export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data associated to a single edit, which survives certain edit operations.
|
||||
*/
|
||||
export interface IEditData<T> {
|
||||
join(other: T): T | undefined;
|
||||
}
|
||||
|
||||
export class VoidEditData implements IEditData<VoidEditData> {
|
||||
join(other: VoidEditData): VoidEditData | undefined {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a set of replacements to a string.
|
||||
* All these replacements are applied at once.
|
||||
*/
|
||||
export class AnnotatedStringEdit<T extends IEditData<T>> extends BaseStringEdit<AnnotatedStringReplacement<T>, AnnotatedStringEdit<T>> {
|
||||
public static readonly empty = new AnnotatedStringEdit<never>([]);
|
||||
|
||||
public static create<T extends IEditData<T>>(replacements: readonly AnnotatedStringReplacement<T>[]): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit(replacements);
|
||||
}
|
||||
|
||||
public static single<T extends IEditData<T>>(replacement: AnnotatedStringReplacement<T>): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit([replacement]);
|
||||
}
|
||||
|
||||
public static replace<T extends IEditData<T>>(range: OffsetRange, replacement: string, data: T): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, replacement, data)]);
|
||||
}
|
||||
|
||||
public static insert<T extends IEditData<T>>(offset: number, replacement: string, data: T): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit([new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), replacement, data)]);
|
||||
}
|
||||
|
||||
public static delete<T extends IEditData<T>>(range: OffsetRange, data: T): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, '', data)]);
|
||||
}
|
||||
|
||||
public static compose<T extends IEditData<T>>(edits: readonly AnnotatedStringEdit<T>[]): AnnotatedStringEdit<T> {
|
||||
if (edits.length === 0) {
|
||||
return AnnotatedStringEdit.empty;
|
||||
}
|
||||
let result = edits[0];
|
||||
for (let i = 1; i < edits.length; i++) {
|
||||
result = result.compose(edits[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(replacements: readonly AnnotatedStringReplacement<T>[]) {
|
||||
super(replacements);
|
||||
}
|
||||
|
||||
protected override _createNew(replacements: readonly AnnotatedStringReplacement<T>[]): AnnotatedStringEdit<T> {
|
||||
return new AnnotatedStringEdit<T>(replacements);
|
||||
}
|
||||
|
||||
toStringEdit(): StringEdit {
|
||||
return new StringEdit(this.replacements.map(e => new StringReplacement(e.replaceRange, e.newText)));
|
||||
}
|
||||
}
|
||||
|
||||
export class AnnotatedStringReplacement<T extends IEditData<T>> extends BaseStringReplacement<AnnotatedStringReplacement<T>> {
|
||||
public static insert<T extends IEditData<T>>(offset: number, text: string, data: T): AnnotatedStringReplacement<T> {
|
||||
return new AnnotatedStringReplacement<T>(OffsetRange.emptyAt(offset), text, data);
|
||||
}
|
||||
|
||||
public static replace<T extends IEditData<T>>(range: OffsetRange, text: string, data: T): AnnotatedStringReplacement<T> {
|
||||
return new AnnotatedStringReplacement<T>(range, text, data);
|
||||
}
|
||||
|
||||
public static delete<T extends IEditData<T>>(range: OffsetRange, data: T): AnnotatedStringReplacement<T> {
|
||||
return new AnnotatedStringReplacement<T>(range, '', data);
|
||||
}
|
||||
|
||||
constructor(
|
||||
range: OffsetRange,
|
||||
newText: string,
|
||||
public readonly data: T
|
||||
) {
|
||||
super(range, newText);
|
||||
}
|
||||
|
||||
override equals(other: AnnotatedStringReplacement<T>): boolean {
|
||||
return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText && this.data === other.data;
|
||||
}
|
||||
|
||||
tryJoinTouching(other: AnnotatedStringReplacement<T>): AnnotatedStringReplacement<T> | undefined {
|
||||
const joined = this.data.join(other.data);
|
||||
if (joined === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new AnnotatedStringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText, joined);
|
||||
}
|
||||
|
||||
slice(range: OffsetRange, rangeInReplacement?: OffsetRange): AnnotatedStringReplacement<T> {
|
||||
return new AnnotatedStringReplacement(range, rangeInReplacement ? rangeInReplacement.substring(this.newText) : this.newText, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,3 +17,8 @@ _setPositionOffsetTransformerDependencies({
|
|||
TextEdit: TextEdit,
|
||||
TextLength: TextLength,
|
||||
});
|
||||
|
||||
// TODO@hediet this is dept and needs to go. See https://github.com/microsoft/vscode/issues/251126.
|
||||
export function ensureDependenciesAreSet(): void {
|
||||
// Noop
|
||||
}
|
||||
|
|
|
@ -194,6 +194,17 @@ function isValidLineNumber(lineNumber: number, lines: string[]): boolean {
|
|||
* Also contains inner range mappings.
|
||||
*/
|
||||
export class DetailedLineRangeMapping extends LineRangeMapping {
|
||||
public static toTextEdit(mapping: readonly DetailedLineRangeMapping[], modified: AbstractText): TextEdit {
|
||||
const replacements: TextReplacement[] = [];
|
||||
for (const m of mapping) {
|
||||
for (const r of m.innerChanges ?? []) {
|
||||
const replacement = r.toTextEdit(modified);
|
||||
replacements.push(replacement);
|
||||
}
|
||||
}
|
||||
return new TextEdit(replacements);
|
||||
}
|
||||
|
||||
public static fromRangeMappings(rangeMappings: RangeMapping[]): DetailedLineRangeMapping {
|
||||
const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange)));
|
||||
const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange)));
|
||||
|
|
|
@ -28,6 +28,9 @@ import { computeDefaultDocumentColors } from '../languages/defaultDocumentColors
|
|||
import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from './findSectionHeaders.js';
|
||||
import { IRawModelData, IWorkerTextModelSyncChannelServer } from './textModelSync/textModelSync.protocol.js';
|
||||
import { ICommonModel, WorkerTextModelSyncServer } from './textModelSync/textModelSync.impl.js';
|
||||
import { ISerializedStringEdit } from '../core/edits/stringEdit.js';
|
||||
import { StringText } from '../core/text/abstractText.js';
|
||||
import { ensureDependenciesAreSet } from '../core/text/positionToOffset.js';
|
||||
|
||||
export interface IMirrorModel extends IMirrorTextModel {
|
||||
readonly uri: URI;
|
||||
|
@ -201,6 +204,24 @@ export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelSer
|
|||
return diffComputer.computeDiff().changes;
|
||||
}
|
||||
|
||||
public $computeStringDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): ISerializedStringEdit {
|
||||
const diffAlgorithm: ILinesDiffComputer = algorithm === 'advanced' ? linesDiffComputers.getDefault() : linesDiffComputers.getLegacy();
|
||||
|
||||
ensureDependenciesAreSet();
|
||||
|
||||
const originalText = new StringText(original);
|
||||
const originalLines = originalText.getLines();
|
||||
const modifiedText = new StringText(modified);
|
||||
const modifiedLines = modifiedText.getLines();
|
||||
|
||||
const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: options.maxComputationTimeMs, computeMoves: false, extendToSubwords: false });
|
||||
|
||||
const textEdit = DetailedLineRangeMapping.toTextEdit(result.changes, modifiedText);
|
||||
const strEdit = originalText.getTransformer().getStringEdit(textEdit);
|
||||
|
||||
return strEdit.toJson();
|
||||
}
|
||||
|
||||
// ---- END diff --------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { UnicodeHighlighterOptions } from './unicodeTextModelHighlighter.js';
|
|||
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import type { EditorWorker } from './editorWebWorker.js';
|
||||
import { SectionHeader, FindSectionHeaderOptions } from './findSectionHeaders.js';
|
||||
import { StringEdit } from '../core/edits/stringEdit.js';
|
||||
|
||||
export const IEditorWorkerService = createDecorator<IEditorWorkerService>('editorWorkerService');
|
||||
|
||||
|
@ -32,6 +33,8 @@ export interface IEditorWorkerService {
|
|||
computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean): Promise<TextEdit[] | undefined>;
|
||||
computeHumanReadableDiff(resource: URI, edits: TextEdit[] | null | undefined): Promise<TextEdit[] | undefined>;
|
||||
|
||||
computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit>;
|
||||
|
||||
canComputeWordRanges(resource: URI): boolean;
|
||||
computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null>;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { TextEdit, IInplaceReplaceSupportResult, IColorInformation } from '../..
|
|||
import { IDocumentDiff, IDocumentDiffProviderOptions } from '../../../common/diff/documentDiffProvider.js';
|
||||
import { IChange } from '../../../common/diff/legacyLinesDiffComputer.js';
|
||||
import { SectionHeader } from '../../../common/services/findSectionHeaders.js';
|
||||
import { StringEdit } from '../../../common/core/edits/stringEdit.js';
|
||||
|
||||
export class TestEditorWorkerService implements IEditorWorkerService {
|
||||
|
||||
|
@ -28,4 +29,8 @@ export class TestEditorWorkerService implements IEditorWorkerService {
|
|||
async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<IInplaceReplaceSupportResult | null> { return null; }
|
||||
async findSectionHeaders(uri: URI): Promise<SectionHeader[]> { return []; }
|
||||
async computeDefaultDocumentColors(uri: URI): Promise<IColorInformation[] | null> { return null; }
|
||||
|
||||
computeStringEditFromDiff(original: string, modified: string, options: { maxComputationTimeMs: number }, algorithm: DiffAlgorithmName): Promise<StringEdit> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,10 @@ export interface ITelemetryService {
|
|||
setExperimentProperty(name: string, value: string): void;
|
||||
}
|
||||
|
||||
export function telemetryLevelEnabled(service: ITelemetryService, level: TelemetryLevel): boolean {
|
||||
return service.telemetryLevel >= level;
|
||||
}
|
||||
|
||||
export interface ITelemetryEndpoint {
|
||||
id: string;
|
||||
aiKey: string;
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { sumBy } from '../../../../base/common/arrays.js';
|
||||
import { AnnotatedStringEdit, BaseStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js';
|
||||
|
||||
/**
|
||||
* The ARC (accepted and retained characters) counts how many characters inserted by the initial suggestion (trackedEdit)
|
||||
* stay unmodified after a certain amount of time after acceptance.
|
||||
*/
|
||||
export class ArcTracker {
|
||||
private _updatedTrackedEdit: AnnotatedStringEdit<IsTrackedEditData>;
|
||||
|
||||
constructor(
|
||||
public readonly originalText: string,
|
||||
private readonly _trackedEdit: BaseStringEdit,
|
||||
) {
|
||||
const eNormalized = _trackedEdit.removeCommonSuffixPrefix(originalText);
|
||||
this._updatedTrackedEdit = eNormalized.mapData(() => new IsTrackedEditData(true));
|
||||
}
|
||||
|
||||
handleEdits(edit: BaseStringEdit): void {
|
||||
const e = edit.mapData(_d => new IsTrackedEditData(false));
|
||||
const composedEdit = this._updatedTrackedEdit.compose(e);
|
||||
const onlyTrackedEdit = composedEdit.decomposeSplit(e => !e.data.isTrackedEdit).e2;
|
||||
this._updatedTrackedEdit = onlyTrackedEdit;
|
||||
}
|
||||
|
||||
getAcceptedRestrainedCharactersCount(): number {
|
||||
const s = sumBy(this._updatedTrackedEdit.replacements, e => e.getNewLength());
|
||||
return s;
|
||||
}
|
||||
|
||||
getOriginalCharacterCount(): number {
|
||||
return sumBy(this._trackedEdit.replacements, e => e.getNewLength());
|
||||
}
|
||||
|
||||
getDebugState(): unknown {
|
||||
return {
|
||||
edits: this._updatedTrackedEdit.replacements.map(e => ({
|
||||
range: e.replaceRange.toString(),
|
||||
newText: e.newText,
|
||||
isTrackedEdit: e.data.isTrackedEdit,
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class IsTrackedEditData implements IEditData<IsTrackedEditData> {
|
||||
constructor(
|
||||
public readonly isTrackedEdit: boolean
|
||||
) { }
|
||||
|
||||
join(data: IsTrackedEditData): IsTrackedEditData | undefined {
|
||||
if (this.isTrackedEdit !== data.isTrackedEdit) {
|
||||
return undefined;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,424 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AsyncIterableObject, raceTimeout } from '../../../../base/common/async.js';
|
||||
import { CachedFunction } from '../../../../base/common/cache.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IObservableWithChange, ISettableObservable, observableValue, RemoveUndefined, runOnChange } from '../../../../base/common/observable.js';
|
||||
import { AnnotatedStringEdit, IEditData } from '../../../../editor/common/core/edits/stringEdit.js';
|
||||
import { StringText } from '../../../../editor/common/core/text/abstractText.js';
|
||||
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
|
||||
import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js';
|
||||
import { IObservableDocument } from './observableWorkspace.js';
|
||||
|
||||
export interface IDocumentWithAnnotatedEdits<TEditData extends IEditData<TEditData> = EditSourceData> {
|
||||
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
|
||||
waitForQueue(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a document that is a delayed copy of the original document,
|
||||
* but with edits annotated with the source of the edit.
|
||||
*/
|
||||
export class DocumentWithAnnotatedEdits extends Disposable implements IDocumentWithAnnotatedEdits<EditReasonData> {
|
||||
public readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<EditReasonData> }>;
|
||||
|
||||
constructor(private readonly _originalDoc: IObservableDocument) {
|
||||
super();
|
||||
|
||||
const v = this.value = observableValue(this, _originalDoc.value.get());
|
||||
|
||||
this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {
|
||||
const eComposed = AnnotatedStringEdit.compose(edits.map(e => {
|
||||
const editSourceData = new EditReasonData(e.reason);
|
||||
return e.mapData(() => editSourceData);
|
||||
}));
|
||||
|
||||
v.set(val, undefined, { edit: eComposed });
|
||||
}));
|
||||
}
|
||||
|
||||
public waitForQueue(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only joins touching edits if the source and the metadata is the same.
|
||||
*/
|
||||
export class EditReasonData implements IEditData<EditReasonData> {
|
||||
public readonly source;
|
||||
public readonly key;
|
||||
|
||||
constructor(
|
||||
public readonly editReason: TextModelEditReason
|
||||
) {
|
||||
this.key = this.editReason.toKey(1);
|
||||
this.source = EditSourceBase.create(this.editReason);
|
||||
}
|
||||
|
||||
join(data: EditReasonData): EditReasonData | undefined {
|
||||
if (this.editReason !== data.editReason) {
|
||||
return undefined;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
toEditSourceData(): EditSourceData {
|
||||
return new EditSourceData(this.key, this.source);
|
||||
}
|
||||
}
|
||||
|
||||
export class EditSourceData implements IEditData<EditSourceData> {
|
||||
constructor(
|
||||
public readonly key: string,
|
||||
public readonly source: EditSource,
|
||||
) { }
|
||||
|
||||
join(data: EditSourceData): EditSourceData | undefined {
|
||||
if (this.key !== data.key) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.source !== data.source) {
|
||||
return undefined;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EditSourceBase {
|
||||
private static _cache = new CachedFunction({ getCacheKey: v => v.toString() }, (arg: EditSource) => arg);
|
||||
|
||||
public static create(reason: TextModelEditReason): EditSource {
|
||||
const data = reason.metadata;
|
||||
switch (data.source) {
|
||||
case 'reloadFromDisk':
|
||||
return this._cache.get(new ExternalEditSource());
|
||||
case 'inlineCompletionPartialAccept':
|
||||
case 'inlineCompletionAccept': {
|
||||
const type = 'type' in data ? data.type : undefined;
|
||||
if ('$nes' in data && data.$nes) {
|
||||
return this._cache.get(new InlineSuggestEditSource('nes', data.$extensionId ?? '', type));
|
||||
}
|
||||
return this._cache.get(new InlineSuggestEditSource('completion', data.$extensionId ?? '', type));
|
||||
}
|
||||
case 'snippet':
|
||||
return this._cache.get(new IdeEditSource('suggest'));
|
||||
case 'unknown':
|
||||
if (!data.name) {
|
||||
return this._cache.get(new UnknownEditSource());
|
||||
}
|
||||
switch (data.name) {
|
||||
case 'formatEditsCommand':
|
||||
return this._cache.get(new IdeEditSource('format'));
|
||||
}
|
||||
return this._cache.get(new UnknownEditSource());
|
||||
|
||||
case 'Chat.applyEdits':
|
||||
return this._cache.get(new ChatEditSource('sidebar'));
|
||||
case 'inlineChat.applyEdits':
|
||||
return this._cache.get(new ChatEditSource('inline'));
|
||||
case 'cursor':
|
||||
return this._cache.get(new UserEditSource());
|
||||
default:
|
||||
return this._cache.get(new UnknownEditSource());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract getColor(): string;
|
||||
}
|
||||
|
||||
export type EditSource = InlineSuggestEditSource | ChatEditSource | IdeEditSource | UserEditSource | UnknownEditSource | ExternalEditSource;
|
||||
|
||||
export class InlineSuggestEditSource extends EditSourceBase {
|
||||
public readonly category = 'ai';
|
||||
public readonly feature = 'inlineSuggest';
|
||||
constructor(
|
||||
public readonly kind: 'completion' | 'nes',
|
||||
public readonly extensionId: string,
|
||||
public readonly type: 'word' | 'line' | undefined,
|
||||
) { super(); }
|
||||
|
||||
override toString() { return `${this.category}/${this.feature}/${this.kind}/${this.extensionId}/${this.type}`; }
|
||||
|
||||
public getColor(): string { return '#00ff0033'; }
|
||||
}
|
||||
|
||||
class ChatEditSource extends EditSourceBase {
|
||||
public readonly category = 'ai';
|
||||
public readonly feature = 'chat';
|
||||
constructor(
|
||||
public readonly kind: 'sidebar' | 'inline',
|
||||
) { super(); }
|
||||
|
||||
override toString() { return `${this.category}/${this.feature}/${this.kind}`; }
|
||||
|
||||
public getColor(): string { return '#00ff0066'; }
|
||||
}
|
||||
|
||||
class IdeEditSource extends EditSourceBase {
|
||||
public readonly category = 'ide';
|
||||
constructor(
|
||||
public readonly feature: 'suggest' | 'format' | string,
|
||||
) { super(); }
|
||||
|
||||
override toString() { return `${this.category}/${this.feature}`; }
|
||||
|
||||
public getColor(): string { return this.feature === 'format' ? '#0000ff33' : '#80808033'; }
|
||||
}
|
||||
|
||||
class UserEditSource extends EditSourceBase {
|
||||
public readonly category = 'user';
|
||||
constructor() { super(); }
|
||||
|
||||
override toString() { return this.category; }
|
||||
|
||||
public getColor(): string { return '#d3d3d333'; }
|
||||
}
|
||||
|
||||
/** Caused by external tools that trigger a reload from disk */
|
||||
class ExternalEditSource extends EditSourceBase {
|
||||
public readonly category = 'external';
|
||||
constructor() { super(); }
|
||||
|
||||
override toString() { return this.category; }
|
||||
|
||||
public getColor(): string { return '#009ab254'; }
|
||||
}
|
||||
|
||||
class UnknownEditSource extends EditSourceBase {
|
||||
public readonly category = 'unknown';
|
||||
constructor() { super(); }
|
||||
|
||||
override toString() { return this.category; }
|
||||
|
||||
public getColor(): string { return '#ff000033'; }
|
||||
}
|
||||
|
||||
export class CombineStreamedChanges<TEditData extends EditSourceData & IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {
|
||||
private readonly _value: ISettableObservable<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
|
||||
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
|
||||
private readonly _runStore = this._register(new DisposableStore());
|
||||
private _runQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(
|
||||
private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,
|
||||
@IEditorWorkerService private readonly _diffService: IEditorWorkerService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.value = this._value = observableValue(this, _originalDoc.value.get());
|
||||
this._restart();
|
||||
|
||||
this._diffService.computeStringEditFromDiff('foo', 'last.value.value', { maxComputationTimeMs: 500 }, 'advanced');
|
||||
}
|
||||
|
||||
async _restart(): Promise<void> {
|
||||
this._runStore.clear();
|
||||
const iterator = iterateChangesFromObservable(this._originalDoc.value, this._runStore)[Symbol.asyncIterator]();
|
||||
const p = this._runQueue;
|
||||
this._runQueue = this._runQueue.then(() => this._run(iterator));
|
||||
await p;
|
||||
}
|
||||
|
||||
private async _run(iterator: AsyncIterator<{ value: StringText; prevValue: StringText; change: { edit: AnnotatedStringEdit<TEditData> }[] }, any, any>) {
|
||||
const reader = new AsyncReader(iterator);
|
||||
while (true) {
|
||||
let peeked = await reader.peek();
|
||||
if (peeked === AsyncReaderEndOfStream) {
|
||||
return;
|
||||
} else if (isChatEdit(peeked)) {
|
||||
const first = peeked;
|
||||
|
||||
let last = first;
|
||||
let chatEdit = AnnotatedStringEdit.empty as AnnotatedStringEdit<TEditData>;
|
||||
|
||||
do {
|
||||
reader.readSyncOrThrow();
|
||||
last = peeked;
|
||||
chatEdit = chatEdit.compose(AnnotatedStringEdit.compose(peeked.change.map(c => c.edit)));
|
||||
if (!await reader.waitForBufferTimeout(1000)) {
|
||||
break;
|
||||
}
|
||||
peeked = reader.peekSyncOrThrow();
|
||||
} while (peeked !== AsyncReaderEndOfStream && isChatEdit(peeked));
|
||||
|
||||
if (!chatEdit.isEmpty()) {
|
||||
const data = chatEdit.replacements[0].data;
|
||||
const diffEdit = await this._diffService.computeStringEditFromDiff(first.prevValue.value, last.value.value, { maxComputationTimeMs: 500 }, 'advanced');
|
||||
const edit = diffEdit.mapData(_e => data);
|
||||
this._value.set(last.value, undefined, { edit });
|
||||
}
|
||||
} else {
|
||||
reader.readSyncOrThrow();
|
||||
const e = AnnotatedStringEdit.compose(peeked.change.map(c => c.edit));
|
||||
this._value.set(peeked.value, undefined, { edit: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async waitForQueue(): Promise<void> {
|
||||
await this._originalDoc.waitForQueue();
|
||||
await this._restart();
|
||||
}
|
||||
}
|
||||
|
||||
function isChatEdit(next: { value: StringText; change: { edit: AnnotatedStringEdit<EditSourceData> }[] }) {
|
||||
return next.change.every(c => c.edit.replacements.every(e => {
|
||||
if (e.data.source.category === 'ai' && e.data.source.feature === 'chat') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
function iterateChangesFromObservable<T, TChange>(obs: IObservableWithChange<T, TChange>, store: DisposableStore): AsyncIterable<{ value: T; prevValue: T; change: RemoveUndefined<TChange>[] }> {
|
||||
return new AsyncIterableObject<{ value: T; prevValue: T; change: RemoveUndefined<TChange>[] }>((e) => {
|
||||
store.add(runOnChange(obs, (value, prevValue, change) => {
|
||||
e.emitOne({ value, prevValue, change: change });
|
||||
}));
|
||||
|
||||
return new Promise((res) => {
|
||||
store.add(toDisposable(() => {
|
||||
res(undefined);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class MinimizeEditsProcessor<TEditData extends IEditData<TEditData>> extends Disposable implements IDocumentWithAnnotatedEdits<TEditData> {
|
||||
readonly value: IObservableWithChange<StringText, { edit: AnnotatedStringEdit<TEditData> }>;
|
||||
|
||||
constructor(
|
||||
private readonly _originalDoc: IDocumentWithAnnotatedEdits<TEditData>,
|
||||
) {
|
||||
super();
|
||||
|
||||
const v = this.value = observableValue(this, _originalDoc.value.get());
|
||||
|
||||
let prevValue: string = this._originalDoc.value.get().value;
|
||||
this._register(runOnChange(this._originalDoc.value, (val, _prevVal, edits) => {
|
||||
const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit));
|
||||
|
||||
const e = eComposed.removeCommonSuffixAndPrefix(prevValue);
|
||||
prevValue = val.value;
|
||||
|
||||
v.set(val, undefined, { edit: e });
|
||||
}));
|
||||
}
|
||||
|
||||
async waitForQueue(): Promise<void> {
|
||||
await this._originalDoc.waitForQueue();
|
||||
}
|
||||
}
|
||||
|
||||
export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream');
|
||||
|
||||
export class AsyncReader<T> {
|
||||
private _buffer: T[] = [];
|
||||
private _atEnd = false;
|
||||
|
||||
public get endOfStream(): boolean { return this._buffer.length === 0 && this._atEnd; }
|
||||
|
||||
constructor(
|
||||
private readonly _source: AsyncIterator<T>
|
||||
) {
|
||||
}
|
||||
|
||||
private async _extendBuffer(): Promise<void> {
|
||||
if (this._atEnd) {
|
||||
return;
|
||||
}
|
||||
const { value, done } = await this._source.next();
|
||||
if (done) {
|
||||
this._atEnd = true;
|
||||
} else {
|
||||
this._buffer.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
public async peek(): Promise<T | typeof AsyncReaderEndOfStream> {
|
||||
if (this._buffer.length === 0 && !this._atEnd) {
|
||||
await this._extendBuffer();
|
||||
}
|
||||
if (this._buffer.length === 0) {
|
||||
return AsyncReaderEndOfStream;
|
||||
}
|
||||
return this._buffer[0];
|
||||
}
|
||||
|
||||
public peekSyncOrThrow(): T | typeof AsyncReaderEndOfStream {
|
||||
if (this._buffer.length === 0) {
|
||||
if (this._atEnd) {
|
||||
return AsyncReaderEndOfStream;
|
||||
}
|
||||
throw new Error('No more elements');
|
||||
}
|
||||
|
||||
return this._buffer[0];
|
||||
}
|
||||
|
||||
public readSyncOrThrow(): T | typeof AsyncReaderEndOfStream {
|
||||
if (this._buffer.length === 0) {
|
||||
if (this._atEnd) {
|
||||
return AsyncReaderEndOfStream;
|
||||
}
|
||||
throw new Error('No more elements');
|
||||
}
|
||||
|
||||
return this._buffer.shift()!;
|
||||
}
|
||||
|
||||
public async peekNextTimeout(timeoutMs: number): Promise<T | typeof AsyncReaderEndOfStream | undefined> {
|
||||
if (this._buffer.length === 0 && !this._atEnd) {
|
||||
await raceTimeout(this._extendBuffer(), timeoutMs);
|
||||
}
|
||||
if (this._atEnd) {
|
||||
return AsyncReaderEndOfStream;
|
||||
}
|
||||
if (this._buffer.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return this._buffer[0];
|
||||
}
|
||||
|
||||
public async waitForBufferTimeout(timeoutMs: number): Promise<boolean> {
|
||||
if (this._buffer.length > 0 || this._atEnd) {
|
||||
return true;
|
||||
}
|
||||
const result = await raceTimeout(this._extendBuffer().then(() => true), timeoutMs);
|
||||
return result !== undefined;
|
||||
}
|
||||
|
||||
public async read(): Promise<T | typeof AsyncReaderEndOfStream> {
|
||||
if (this._buffer.length === 0 && !this._atEnd) {
|
||||
await this._extendBuffer();
|
||||
}
|
||||
if (this._buffer.length === 0) {
|
||||
return AsyncReaderEndOfStream;
|
||||
}
|
||||
return this._buffer.shift()!;
|
||||
}
|
||||
|
||||
public async readWhile(predicate: (value: T) => boolean, callback: (element: T) => unknown): Promise<void> {
|
||||
do {
|
||||
const piece = await this.peek();
|
||||
if (piece === AsyncReaderEndOfStream) {
|
||||
break;
|
||||
}
|
||||
if (!predicate(piece)) {
|
||||
break;
|
||||
}
|
||||
await this.read(); // consume
|
||||
await callback(piece);
|
||||
} while (true);
|
||||
}
|
||||
|
||||
public async consumeToEnd(): Promise<void> {
|
||||
while (!this.endOfStream) {
|
||||
await this.read();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { CachedFunction } from '../../../../base/common/cache.js';
|
||||
import { MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun, mapObservableArrayCached, derived, IObservable, ISettableObservable, observableValue, derivedWithSetter, observableSignalFromEvent, observableFromEvent } from '../../../../base/common/observable.js';
|
||||
import { isDefined } from '../../../../base/common/types.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { DynamicCssRules } from '../../../../editor/browser/editorDom.js';
|
||||
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
|
||||
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
|
||||
import { IModelDeltaDecoration } from '../../../../editor/common/model.js';
|
||||
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
|
||||
import { EditSource } from './documentWithAnnotatedEdits.js';
|
||||
import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js';
|
||||
import { EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js';
|
||||
import { VSCodeWorkspace } from './vscodeObservableWorkspace.js';
|
||||
|
||||
export class EditTrackingFeature extends Disposable {
|
||||
|
||||
private readonly _editSourceTrackingShowDecorations;
|
||||
private readonly _editSourceTrackingShowStatusBar;
|
||||
private readonly _showStateInMarkdownDoc = 'editTelemetry.showDebugDetails';
|
||||
private readonly _toggleDecorations = 'editTelemetry.toggleDebugDecorations';
|
||||
|
||||
constructor(
|
||||
private readonly _workspace: VSCodeWorkspace,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IStatusbarService private readonly _statusbarService: IStatusbarService,
|
||||
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editSourceTrackingShowDecorations = makeSettable(observableConfigValue(EDIT_TELEMETRY_SHOW_DECORATIONS, false, this._configurationService));
|
||||
this._editSourceTrackingShowStatusBar = observableConfigValue(EDIT_TELEMETRY_SHOW_STATUS_BAR, false, this._configurationService);
|
||||
|
||||
const onDidAddGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidAddGroup);
|
||||
const onDidRemoveGroupSignal = observableSignalFromEvent(this, this._editorGroupsService.onDidRemoveGroup);
|
||||
const groups = derived(this, reader => {
|
||||
onDidAddGroupSignal.read(reader);
|
||||
onDidRemoveGroupSignal.read(reader);
|
||||
return this._editorGroupsService.groups;
|
||||
});
|
||||
const visibleUris: IObservable<Map<string, URI>> = mapObservableArrayCached(this, groups, g => {
|
||||
const editors = observableFromEvent(this, g.onDidModelChange, () => g.editors);
|
||||
return editors.map(e => e.map(editor => EditorResourceAccessor.getCanonicalUri(editor)));
|
||||
}).map((editors, reader) => {
|
||||
const map = new Map<string, URI>();
|
||||
for (const urisObs of editors) {
|
||||
for (const uri of urisObs.read(reader)) {
|
||||
if (isDefined(uri)) {
|
||||
map.set(uri.toString(), uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const impl = this._register(this._instantiationService.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => {
|
||||
const map = visibleUris.read(reader);
|
||||
return map.get(doc.uri.toString()) !== undefined;
|
||||
}));
|
||||
|
||||
this._register(autorun((reader) => {
|
||||
if (!this._editSourceTrackingShowDecorations.read(reader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleEditors = observableFromEvent(this, this._editorService.onDidVisibleEditorsChange, () => this._editorService.visibleTextEditorControls);
|
||||
|
||||
mapObservableArrayCached(this, visibleEditors, (editor, store) => {
|
||||
if (editor instanceof CodeEditorWidget) {
|
||||
const obsEditor = observableCodeEditor(editor);
|
||||
|
||||
const cssStyles = new DynamicCssRules(editor);
|
||||
const decorations = new CachedFunction((source: EditSource) => {
|
||||
const r = store.add(cssStyles.createClassNameRef({
|
||||
backgroundColor: source.getColor(),
|
||||
}));
|
||||
return r.className;
|
||||
});
|
||||
|
||||
store.add(obsEditor.setDecorations(derived(reader => {
|
||||
const uri = obsEditor.model.read(reader)?.uri;
|
||||
if (!uri) { return []; }
|
||||
const doc = this._workspace.getDocument(uri);
|
||||
if (!doc) { return []; }
|
||||
const docsState = impl.docsState.read(reader).get(doc);
|
||||
if (!docsState) { return []; }
|
||||
|
||||
const ranges = (docsState.longtermTracker.read(reader)?.getTrackedRanges(reader)) ?? [];
|
||||
|
||||
return ranges.map<IModelDeltaDecoration>(r => ({
|
||||
range: doc.value.get().getTransformer().getRange(r.range),
|
||||
options: {
|
||||
description: 'editSourceTracking',
|
||||
inlineClassName: decorations.get(r.source),
|
||||
}
|
||||
}));
|
||||
})));
|
||||
}
|
||||
}).recomputeInitiallyAndOnChange(reader.store);
|
||||
}));
|
||||
|
||||
this._register(autorun(reader => {
|
||||
if (!this._editSourceTrackingShowStatusBar.read(reader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusBarItem = reader.store.add(this._statusbarService.addEntry(
|
||||
{
|
||||
name: '',
|
||||
text: '',
|
||||
command: this._showStateInMarkdownDoc,
|
||||
tooltip: 'Edit Source Tracking',
|
||||
ariaLabel: '',
|
||||
},
|
||||
'editTelemetry',
|
||||
StatusbarAlignment.RIGHT,
|
||||
100
|
||||
));
|
||||
|
||||
const sumChangedCharacters = derived(reader => {
|
||||
const docs = impl.docsState.read(reader);
|
||||
let sum = 0;
|
||||
for (const state of docs.values()) {
|
||||
const t = state.longtermTracker.read(reader);
|
||||
if (!t) { continue; }
|
||||
const d = state.getTelemetryData(t.getTrackedRanges(reader));
|
||||
sum += d.totalModifiedCharactersInFinalState;
|
||||
}
|
||||
return sum;
|
||||
});
|
||||
|
||||
const tooltipMarkdownString = derived(reader => {
|
||||
const docs = impl.docsState.read(reader);
|
||||
const docsDataInTooltip: string[] = [];
|
||||
const editSources: EditSource[] = [];
|
||||
for (const [doc, state] of docs) {
|
||||
const tracker = state.longtermTracker.read(reader);
|
||||
if (!tracker) {
|
||||
continue;
|
||||
}
|
||||
const trackedRanges = tracker.getTrackedRanges(reader);
|
||||
const data = state.getTelemetryData(trackedRanges);
|
||||
if (data.totalModifiedCharactersInFinalState === 0) {
|
||||
continue; // Don't include unmodified documents in tooltip
|
||||
}
|
||||
|
||||
editSources.push(...trackedRanges.map(r => r.source));
|
||||
|
||||
// Filter out unmodified properties as these are not interesting to see in the hover
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([_, value]) => !(typeof value === 'number') || value !== 0)
|
||||
);
|
||||
|
||||
docsDataInTooltip.push([
|
||||
`### ${doc.uri.fsPath}`,
|
||||
'```json',
|
||||
JSON.stringify(filteredData, undefined, '\t'),
|
||||
'```',
|
||||
'\n'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
let tooltipContent: string;
|
||||
if (docsDataInTooltip.length === 0) {
|
||||
tooltipContent = 'No modified documents';
|
||||
} else if (docsDataInTooltip.length <= 3) {
|
||||
tooltipContent = docsDataInTooltip.join('\n\n');
|
||||
} else {
|
||||
const lastThree = docsDataInTooltip.slice(-3);
|
||||
tooltipContent = '...\n\n' + lastThree.join('\n\n');
|
||||
}
|
||||
|
||||
const agenda = this._createEditSourceAgenda(editSources);
|
||||
|
||||
const tooltipWithCommand = new MarkdownString(tooltipContent + '\n\n[View Details](command:' + this._showStateInMarkdownDoc + ')');
|
||||
tooltipWithCommand.appendMarkdown('\n\n' + agenda + '\n\nToggle decorations: [Click here](command:' + this._toggleDecorations + ')');
|
||||
tooltipWithCommand.isTrusted = { enabledCommands: [this._toggleDecorations] };
|
||||
tooltipWithCommand.supportHtml = true;
|
||||
|
||||
return tooltipWithCommand;
|
||||
});
|
||||
|
||||
reader.store.add(autorun(reader => {
|
||||
statusBarItem.update({
|
||||
name: 'editTelemetry',
|
||||
text: `$(edit) ${sumChangedCharacters.read(reader)} chars inserted`,
|
||||
ariaLabel: `Edit Source Tracking: ${sumChangedCharacters.read(reader)} modified characters`,
|
||||
tooltip: tooltipMarkdownString.read(reader),
|
||||
command: this._showStateInMarkdownDoc,
|
||||
});
|
||||
}));
|
||||
|
||||
reader.store.add(CommandsRegistry.registerCommand(this._toggleDecorations, () => {
|
||||
this._editSourceTrackingShowDecorations.set(!this._editSourceTrackingShowDecorations.get(), undefined);
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
private _createEditSourceAgenda(editSources: EditSource[]): string {
|
||||
// Collect all edit sources from the tracked documents
|
||||
const editSourcesSeen = new Set<string>();
|
||||
const editSourceInfo = [];
|
||||
for (const editSource of editSources) {
|
||||
if (!editSourcesSeen.has(editSource.toString())) {
|
||||
editSourcesSeen.add(editSource.toString());
|
||||
editSourceInfo.push({ name: editSource.toString(), color: editSource.getColor() });
|
||||
}
|
||||
}
|
||||
|
||||
const agendaItems = editSourceInfo.map(info =>
|
||||
`<span style="background-color:${info.color};border-radius:3px;">${info.name}</span>`
|
||||
);
|
||||
|
||||
return agendaItems.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
export function makeSettable<T>(obs: IObservable<T>): ISettableObservable<T> {
|
||||
const overrideObs = observableValue<T | undefined>('overrideObs', undefined);
|
||||
return derivedWithSetter(overrideObs, (reader) => {
|
||||
return overrideObs.read(reader) ?? obs.read(reader);
|
||||
}, (value, tx) => {
|
||||
overrideObs.set(value, tx);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,451 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { reverseOrder, compareBy, numberComparator, sumBy } from '../../../../base/common/arrays.js';
|
||||
import { IntervalTimer, TimeoutTimer } from '../../../../base/common/async.js';
|
||||
import { toDisposable, DisposableStore, Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { mapObservableArrayCached, derived, IReader, IObservable, observableSignal, runOnChange, IObservableWithChange, observableValue, transaction, derivedObservableWithCache } from '../../../../base/common/observable.js';
|
||||
import { isDefined } from '../../../../base/common/types.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { AnnotatedStringEdit, BaseStringEdit } from '../../../../editor/common/core/edits/stringEdit.js';
|
||||
import { StringText } from '../../../../editor/common/core/text/abstractText.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { ISCMRepository, ISCMService } from '../../scm/common/scm.js';
|
||||
import { ArcTracker } from './arcTracker.js';
|
||||
import { CombineStreamedChanges, DocumentWithAnnotatedEdits, EditReasonData, EditSource, EditSourceData, IDocumentWithAnnotatedEdits, MinimizeEditsProcessor } from './documentWithAnnotatedEdits.js';
|
||||
import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js';
|
||||
import { ObservableWorkspace, IObservableDocument } from './observableWorkspace.js';
|
||||
|
||||
export class EditSourceTrackingImpl extends Disposable {
|
||||
public readonly docsState;
|
||||
|
||||
constructor(
|
||||
private readonly _workspace: ObservableWorkspace,
|
||||
private readonly _docIsVisible: (doc: IObservableDocument, reader: IReader) => boolean,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const scmBridge = this._instantiationService.createInstance(ScmBridge);
|
||||
|
||||
this.docsState = mapObservableArrayCached(this, this._workspace.documents, (doc, store) => {
|
||||
const docIsVisible = derived(reader => this._docIsVisible(doc, reader));
|
||||
const wasEverVisible = derivedObservableWithCache<boolean>(this, (reader, lastVal) => lastVal || docIsVisible.read(reader));
|
||||
return wasEverVisible.map(v => v ? [doc, store.add(this._instantiationService.createInstance(TrackedDocumentInfo, doc, docIsVisible, scmBridge))] as const : undefined);
|
||||
}).recomputeInitiallyAndOnChange(this._store).map((entries, reader) => new Map(entries.map(e => e.read(reader)).filter(isDefined)));
|
||||
}
|
||||
}
|
||||
|
||||
class ScmBridge {
|
||||
constructor(
|
||||
@ISCMService private readonly _scmService: ISCMService
|
||||
) { }
|
||||
|
||||
public async getRepo(uri: URI): Promise<ScmRepoBridge | undefined> {
|
||||
const repo = this._scmService.getRepository(uri);
|
||||
if (!repo) {
|
||||
return undefined;
|
||||
}
|
||||
return new ScmRepoBridge(repo);
|
||||
}
|
||||
}
|
||||
|
||||
class ScmRepoBridge {
|
||||
public readonly headBranchNameObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.name);
|
||||
public readonly headCommitHashObs: IObservable<string | undefined> = derived(reader => this._repo.provider.historyProvider.read(reader)?.historyItemRef.read(reader)?.revision);
|
||||
|
||||
constructor(
|
||||
private readonly _repo: ISCMRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
async isIgnored(uri: URI): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class TrackedDocumentInfo extends Disposable {
|
||||
public readonly longtermTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;
|
||||
public readonly windowedTracker: IObservable<DocumentEditSourceTracker<undefined> | undefined>;
|
||||
|
||||
private readonly _repo: Promise<ScmRepoBridge | undefined>;
|
||||
|
||||
constructor(
|
||||
private readonly _doc: IObservableDocument,
|
||||
docIsVisible: IObservable<boolean>,
|
||||
private readonly _scm: ScmBridge,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Use the listener service and special events from core to annotate where an edit came from (is async)
|
||||
let processedDoc: IDocumentWithAnnotatedEdits<EditReasonData> = this._store.add(new DocumentWithAnnotatedEdits(_doc));
|
||||
// Combine streaming edits into one and make edit smaller
|
||||
processedDoc = this._store.add(this._instantiationService.createInstance((CombineStreamedChanges<EditReasonData>), processedDoc));
|
||||
// Remove common suffix and prefix from edits
|
||||
processedDoc = this._store.add(new MinimizeEditsProcessor(processedDoc));
|
||||
|
||||
const docWithJustReason = createDocWithJustReason(processedDoc, this._store);
|
||||
|
||||
const longtermResetSignal = observableSignal('resetSignal');
|
||||
|
||||
this.longtermTracker = derived((reader) => {
|
||||
longtermResetSignal.read(reader);
|
||||
|
||||
const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));
|
||||
reader.store.add(toDisposable(() => {
|
||||
// send long term document telemetry
|
||||
if (!t.isEmpty()) {
|
||||
this.sendTelemetry('longterm', t.getTrackedRanges());
|
||||
}
|
||||
t.dispose();
|
||||
}));
|
||||
return t;
|
||||
}).recomputeInitiallyAndOnChange(this._store);
|
||||
|
||||
this._store.add(new IntervalTimer()).cancelAndSet(() => {
|
||||
// Reset after 10 hours
|
||||
longtermResetSignal.trigger(undefined);
|
||||
}, 10 * 60 * 60 * 1000);
|
||||
|
||||
(async () => {
|
||||
const repo = await this._scm.getRepo(_doc.uri);
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
}
|
||||
// Reset on branch change or commit
|
||||
if (repo) {
|
||||
this._store.add(runOnChange(repo.headCommitHashObs, () => {
|
||||
longtermResetSignal.trigger(undefined);
|
||||
}));
|
||||
this._store.add(runOnChange(repo.headBranchNameObs, () => {
|
||||
longtermResetSignal.trigger(undefined);
|
||||
}));
|
||||
}
|
||||
|
||||
this._store.add(this._instantiationService.createInstance(ArcTelemetrySender, processedDoc, repo));
|
||||
})();
|
||||
|
||||
const resetSignal = observableSignal('resetSignal');
|
||||
|
||||
this.windowedTracker = derived((reader) => {
|
||||
if (!docIsVisible.read(reader)) {
|
||||
return undefined;
|
||||
}
|
||||
resetSignal.read(reader);
|
||||
|
||||
reader.store.add(new TimeoutTimer(() => {
|
||||
// Reset after 5 minutes
|
||||
resetSignal.trigger(undefined);
|
||||
}, 5 * 60 * 1000));
|
||||
|
||||
const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined));
|
||||
reader.store.add(toDisposable(async () => {
|
||||
// send long term document telemetry
|
||||
this.sendTelemetry('5minWindow', t.getTrackedRanges());
|
||||
t.dispose();
|
||||
}));
|
||||
|
||||
return t;
|
||||
}).recomputeInitiallyAndOnChange(this._store);
|
||||
|
||||
this._repo = this._scm.getRepo(_doc.uri);
|
||||
}
|
||||
|
||||
async sendTelemetry(mode: 'longterm' | '5minWindow', ranges: readonly TrackedEdit[]) {
|
||||
if (ranges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.getTelemetryData(ranges);
|
||||
const isTrackedByGit = await data.isTrackedByGit;
|
||||
|
||||
const statsUuid = generateUuid();
|
||||
|
||||
this._telemetryService.publicLog2<{
|
||||
mode: string;
|
||||
languageId: string;
|
||||
statsUuid: string;
|
||||
nesModifiedCount: number;
|
||||
inlineCompletionsCopilotModifiedCount: number;
|
||||
inlineCompletionsNESModifiedCount: number;
|
||||
otherAIModifiedCount: number;
|
||||
unknownModifiedCount: number;
|
||||
userModifiedCount: number;
|
||||
ideModifiedCount: number;
|
||||
totalModifiedCharacters: number;
|
||||
externalModifiedCount: number;
|
||||
isTrackedByGit: number;
|
||||
}, {
|
||||
owner: 'hediet';
|
||||
comment: 'Reports distribution of AI vs user edited characters.';
|
||||
|
||||
mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' };
|
||||
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
|
||||
statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' };
|
||||
|
||||
nesModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true };
|
||||
inlineCompletionsCopilotModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions copilot modified characters'; isMeasurement: true };
|
||||
inlineCompletionsNESModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of inline completions nes modified characters'; isMeasurement: true };
|
||||
otherAIModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of other AI modified characters'; isMeasurement: true };
|
||||
unknownModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of unknown modified characters'; isMeasurement: true };
|
||||
userModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of user modified characters'; isMeasurement: true };
|
||||
ideModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of IDE modified characters'; isMeasurement: true };
|
||||
totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true };
|
||||
externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true };
|
||||
isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' };
|
||||
}>('editTelemetry.editSources.stats', {
|
||||
mode,
|
||||
languageId: this._doc.languageId.get(),
|
||||
statsUuid: statsUuid,
|
||||
nesModifiedCount: data.nesModifiedCount,
|
||||
inlineCompletionsCopilotModifiedCount: data.inlineCompletionsCopilotModifiedCount,
|
||||
inlineCompletionsNESModifiedCount: data.inlineCompletionsNESModifiedCount,
|
||||
otherAIModifiedCount: data.otherAIModifiedCount,
|
||||
unknownModifiedCount: data.unknownModifiedCount,
|
||||
userModifiedCount: data.userModifiedCount,
|
||||
ideModifiedCount: data.ideModifiedCount,
|
||||
totalModifiedCharacters: data.totalModifiedCharactersInFinalState,
|
||||
externalModifiedCount: data.externalModifiedCount,
|
||||
isTrackedByGit: isTrackedByGit ? 1 : 0,
|
||||
});
|
||||
|
||||
|
||||
const sums = sumByCategory(ranges, r => r.range.length, r => r.sourceKey);
|
||||
const entries = Object.entries(sums).filter(([key, value]) => value !== undefined);
|
||||
entries.sort(reverseOrder(compareBy(([key, value]) => value!, numberComparator)));
|
||||
entries.length = mode === 'longterm' ? 30 : 10;
|
||||
|
||||
for (const [key, value] of Object.entries(sums)) {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
this._telemetryService.publicLog2<{
|
||||
mode: string;
|
||||
reasonKey: string;
|
||||
languageId: string;
|
||||
statsUuid: string;
|
||||
modifiedCount: number;
|
||||
totalModifiedCount: number;
|
||||
}, {
|
||||
owner: 'hediet';
|
||||
comment: 'Reports distribution of various edit kinds.';
|
||||
|
||||
reasonKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the edit.' };
|
||||
mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' };
|
||||
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' };
|
||||
statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' };
|
||||
|
||||
modifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of nes modified characters'; isMeasurement: true };
|
||||
totalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total number of characters'; isMeasurement: true };
|
||||
}>('editTelemetry.editSources.details', {
|
||||
mode,
|
||||
reasonKey: key,
|
||||
languageId: this._doc.languageId.get(),
|
||||
statsUuid: statsUuid,
|
||||
modifiedCount: value,
|
||||
totalModifiedCount: data.totalModifiedCharactersInFinalState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTelemetryData(ranges: readonly TrackedEdit[]) {
|
||||
const getEditCategory = (source: EditSource) => {
|
||||
if (source.category === 'ai' && source.kind === 'nes') { return 'nes'; }
|
||||
if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot') { return 'inlineCompletionsCopilot'; }
|
||||
if (source.category === 'ai' && source.kind === 'completion' && source.extensionId === 'github.copilot-chat') { return 'inlineCompletionsNES'; }
|
||||
if (source.category === 'ai' && source.kind === 'completion') { return 'inlineCompletionsOther'; }
|
||||
if (source.category === 'ai') { return 'otherAI'; }
|
||||
if (source.category === 'user') { return 'user'; }
|
||||
if (source.category === 'ide') { return 'ide'; }
|
||||
if (source.category === 'external') { return 'external'; }
|
||||
if (source.category === 'unknown') { return 'unknown'; }
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const sums = sumByCategory(ranges, r => r.range.length, r => getEditCategory(r.source));
|
||||
const totalModifiedCharactersInFinalState = sumBy(ranges, r => r.range.length);
|
||||
|
||||
return {
|
||||
nesModifiedCount: sums.nes ?? 0,
|
||||
inlineCompletionsCopilotModifiedCount: sums.inlineCompletionsCopilot ?? 0,
|
||||
inlineCompletionsNESModifiedCount: sums.inlineCompletionsNES ?? 0,
|
||||
otherAIModifiedCount: sums.otherAI ?? 0,
|
||||
userModifiedCount: sums.user ?? 0,
|
||||
ideModifiedCount: sums.ide ?? 0,
|
||||
unknownModifiedCount: sums.unknown ?? 0,
|
||||
externalModifiedCount: sums.external ?? 0,
|
||||
totalModifiedCharactersInFinalState,
|
||||
languageId: this._doc.languageId.get(),
|
||||
isTrackedByGit: this._repo.then(async (repo) => !!repo && !await repo.isIgnored(this._doc.uri)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapObservableDelta<T, TDelta, TDeltaNew>(obs: IObservableWithChange<T, TDelta>, mapFn: (value: TDelta) => TDeltaNew, store: DisposableStore): IObservableWithChange<T, TDeltaNew> {
|
||||
const obsResult = observableValue<T, TDeltaNew>('mapped', obs.get());
|
||||
store.add(runOnChange(obs, (value, _prevValue, changes) => {
|
||||
transaction(tx => {
|
||||
for (const c of changes) {
|
||||
obsResult.set(value, tx, mapFn(c));
|
||||
}
|
||||
});
|
||||
}));
|
||||
return obsResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing the metadata allows touching edits from the same source to merged, even if they were caused by different actions (e.g. two user edits).
|
||||
*/
|
||||
function createDocWithJustReason(docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditReasonData>, store: DisposableStore): IDocumentWithAnnotatedEdits<EditSourceData> {
|
||||
const docWithJustReason: IDocumentWithAnnotatedEdits<EditSourceData> = {
|
||||
value: mapObservableDelta(docWithAnnotatedEdits.value, edit => ({ edit: edit.edit.mapData(d => d.data.toEditSourceData()) }), store),
|
||||
waitForQueue: () => docWithAnnotatedEdits.waitForQueue(),
|
||||
};
|
||||
return docWithJustReason;
|
||||
}
|
||||
|
||||
class ArcTelemetrySender extends Disposable {
|
||||
constructor(
|
||||
docWithAnnotatedEdits: IDocumentWithAnnotatedEdits<EditReasonData>,
|
||||
scmRepoBridge: ScmRepoBridge | undefined,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => {
|
||||
const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit));
|
||||
if (edit.replacements.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const singleEdit = edit.replacements[0];
|
||||
const data = singleEdit.data.editReason.metadata;
|
||||
if (data?.source !== 'inlineCompletionAccept') {
|
||||
return;
|
||||
}
|
||||
|
||||
const docWithJustReason = createDocWithJustReason(docWithAnnotatedEdits, this._store);
|
||||
const reporter = this._instantiationService.createInstance(ArcTelemetryReporter, docWithJustReason, scmRepoBridge, singleEdit.toEdit(), res => {
|
||||
|
||||
res.telemetryService.publicLog2<{
|
||||
extensionId: string;
|
||||
opportunityId: string;
|
||||
didBranchChange: number;
|
||||
timeDelayMs: number;
|
||||
arc: number;
|
||||
originalCharCount: number;
|
||||
}, {
|
||||
owner: 'hediet';
|
||||
comment: 'Reports the accepted and retained character count for an inline completion/edit.';
|
||||
|
||||
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id (copilot or copilot-chat); which provided this inline completion.' };
|
||||
opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline completion or NES.' };
|
||||
|
||||
didBranchChange: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates if the branch changed in the meantime. If the branch changed (value is 1); this event should probably be ignored.' };
|
||||
timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' };
|
||||
arc: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The accepted and restrained character count.' };
|
||||
originalCharCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The original character count before any edits.' };
|
||||
}>('editTelemetry.reportInlineEditArc', {
|
||||
extensionId: data.$extensionId ?? '',
|
||||
opportunityId: data.$$requestUuid ?? 'unknown',
|
||||
didBranchChange: res.didBranchChange ? 1 : 0,
|
||||
timeDelayMs: res.timeDelayMs,
|
||||
arc: res.arc,
|
||||
originalCharCount: res.originalCharCount,
|
||||
});
|
||||
});
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
reporter.cancel();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export interface EditTelemetryData {
|
||||
telemetryService: ITelemetryService;
|
||||
timeDelayMs: number;
|
||||
didBranchChange: boolean;
|
||||
arc: number;
|
||||
originalCharCount: number;
|
||||
}
|
||||
|
||||
export class ArcTelemetryReporter {
|
||||
private readonly _store = new DisposableStore();
|
||||
private readonly _arcTracker;
|
||||
private readonly _initialBranchName: string | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _document: { value: IObservableWithChange<StringText, { edit: BaseStringEdit }> },
|
||||
// _markedEdits -> document.value
|
||||
private readonly _gitRepo: ScmRepoBridge | undefined,
|
||||
private readonly _trackedEdit: BaseStringEdit,
|
||||
private readonly _sendTelemetryEvent: (res: EditTelemetryData) => void,
|
||||
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
this._arcTracker = new ArcTracker(this._document.value.get().value, this._trackedEdit);
|
||||
|
||||
this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => {
|
||||
const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit));
|
||||
if (edit) {
|
||||
this._arcTracker.handleEdits(edit);
|
||||
}
|
||||
}));
|
||||
|
||||
this._initialBranchName = this._gitRepo?.headBranchNameObs.get();
|
||||
|
||||
// This aligns with github inline completions
|
||||
this._reportAfter(30 * 1000);
|
||||
this._reportAfter(120 * 1000);
|
||||
this._reportAfter(300 * 1000);
|
||||
this._reportAfter(600 * 1000);
|
||||
// track up to 15min to allow for slower edit responses from legacy SD endpoint
|
||||
this._reportAfter(900 * 1000, () => {
|
||||
this._store.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private _reportAfter(timeoutMs: number, cb?: () => void) {
|
||||
const timer = new TimeoutTimer(() => {
|
||||
this._report(timeoutMs);
|
||||
timer.dispose();
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
}, timeoutMs);
|
||||
this._store.add(timer);
|
||||
}
|
||||
|
||||
private _report(timeMs: number): void {
|
||||
const currentBranch = this._gitRepo?.headBranchNameObs.get();
|
||||
const didBranchChange = currentBranch !== this._initialBranchName;
|
||||
|
||||
this._sendTelemetryEvent({
|
||||
telemetryService: this._telemetryService,
|
||||
timeDelayMs: timeMs,
|
||||
didBranchChange,
|
||||
arc: this._arcTracker.getAcceptedRestrainedCharactersCount(),
|
||||
originalCharCount: this._arcTracker.getOriginalCharacterCount(),
|
||||
});
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function sumByCategory<T, TCategory extends string>(items: readonly T[], getValue: (item: T) => number, getCategory: (item: T) => TCategory): Record<TCategory, number | undefined> {
|
||||
return items.reduce((acc, item) => {
|
||||
const category = getCategory(item);
|
||||
acc[category] = (acc[category] || 0) + getValue(item);
|
||||
return acc;
|
||||
}, {} as any as Record<TCategory, number>);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { EditTelemetryService } from './editTelemetryService.js';
|
||||
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
registerWorkbenchContribution2('EditTelemetryService', EditTelemetryService, WorkbenchPhase.AfterRestored);
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'task',
|
||||
order: 100,
|
||||
title: localize('editTelemetry', "Edit Telemetry"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
[EDIT_TELEMETRY_SETTING_ID]: {
|
||||
markdownDescription: localize('telemetry.editStats.enabled', "Controls whether to enable telemetry for edit statistics (only sends statistics if general telemetry is enabled)."),
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
tags: ['experimental'],
|
||||
},
|
||||
[EDIT_TELEMETRY_SHOW_STATUS_BAR]: {
|
||||
markdownDescription: localize('telemetry.editStats.showStatusBar', "Controls whether to show the status bar for edit telemetry."),
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
tags: ['experimental'],
|
||||
},
|
||||
[EDIT_TELEMETRY_SHOW_DECORATIONS]: {
|
||||
markdownDescription: localize('telemetry.editStats.showDecorations', "Controls whether to show decorations for edit telemetry."),
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
tags: ['experimental'],
|
||||
},
|
||||
}
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
|
||||
import { ITelemetryService, TelemetryLevel, telemetryLevelEnabled } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { EditTrackingFeature } from './editSourceTrackingFeature.js';
|
||||
import { EDIT_TELEMETRY_SETTING_ID } from './settings.js';
|
||||
import { VSCodeWorkspace } from './vscodeObservableWorkspace.js';
|
||||
|
||||
export class EditTelemetryService extends Disposable {
|
||||
private readonly _editSourceTrackingEnabled;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService);
|
||||
|
||||
this._register(autorun(r => {
|
||||
const enabled = this._editSourceTrackingEnabled.read(r);
|
||||
if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = this._instantiationService.createInstance(VSCodeWorkspace);
|
||||
|
||||
r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace));
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { observableSignal, runOnChange, IReader } from '../../../../base/common/observable.js';
|
||||
import { AnnotatedStringEdit } from '../../../../editor/common/core/edits/stringEdit.js';
|
||||
import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
|
||||
import { IDocumentWithAnnotatedEdits, EditSourceData, EditSource } from './documentWithAnnotatedEdits.js';
|
||||
|
||||
/**
|
||||
* Tracks a single document.
|
||||
*/
|
||||
export class DocumentEditSourceTracker<T = void> extends Disposable {
|
||||
private _edits: AnnotatedStringEdit<EditSourceData> = AnnotatedStringEdit.empty;
|
||||
private _pendingExternalEdits: AnnotatedStringEdit<EditSourceData> = AnnotatedStringEdit.empty;
|
||||
|
||||
private readonly _update = observableSignal(this);
|
||||
|
||||
constructor(
|
||||
private readonly _doc: IDocumentWithAnnotatedEdits,
|
||||
public readonly data: T,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(runOnChange(this._doc.value, (_val, _prevVal, edits) => {
|
||||
const eComposed = AnnotatedStringEdit.compose(edits.map(e => e.edit));
|
||||
if (eComposed.replacements.every(e => e.data.source.category === 'external')) {
|
||||
if (this._edits.isEmpty()) {
|
||||
// Ignore initial external edits
|
||||
} else {
|
||||
// queue pending external edits
|
||||
this._pendingExternalEdits = this._pendingExternalEdits.compose(eComposed);
|
||||
}
|
||||
} else {
|
||||
if (!this._pendingExternalEdits.isEmpty()) {
|
||||
this._edits = this._edits.compose(this._pendingExternalEdits);
|
||||
this._pendingExternalEdits = AnnotatedStringEdit.empty;
|
||||
}
|
||||
this._edits = this._edits.compose(eComposed);
|
||||
}
|
||||
|
||||
this._update.trigger(undefined);
|
||||
}));
|
||||
}
|
||||
|
||||
async waitForQueue(): Promise<void> {
|
||||
await this._doc.waitForQueue();
|
||||
}
|
||||
|
||||
getTrackedRanges(reader?: IReader): TrackedEdit[] {
|
||||
this._update.read(reader);
|
||||
const ranges = this._edits.getNewRanges();
|
||||
return ranges.map((r, idx) => {
|
||||
const e = this._edits.replacements[idx];
|
||||
const reason = e.data.source;
|
||||
const te = new TrackedEdit(e.replaceRange, r, reason, e.data.key);
|
||||
return te;
|
||||
});
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._edits.isEmpty();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._edits = AnnotatedStringEdit.empty;
|
||||
}
|
||||
|
||||
public _getDebugVisualization() {
|
||||
const ranges = this.getTrackedRanges();
|
||||
const txt = this._doc.value.get().value;
|
||||
|
||||
return {
|
||||
...{ $fileExtension: 'text.w' },
|
||||
'value': txt,
|
||||
'decorations': ranges.map(r => {
|
||||
return {
|
||||
range: [r.range.start, r.range.endExclusive],
|
||||
color: r.source.getColor(),
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TrackedEdit {
|
||||
constructor(
|
||||
public readonly originalRange: OffsetRange,
|
||||
public readonly range: OffsetRange,
|
||||
public readonly source: EditSource,
|
||||
public readonly sourceKey: string,
|
||||
) { }
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservableWithChange, derivedHandleChanges, derivedWithStore, observableValue, autorunWithStore, runOnChange, IObservable } from '../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { StringEdit } from '../../../../editor/common/core/edits/stringEdit.js';
|
||||
import { StringText } from '../../../../editor/common/core/text/abstractText.js';
|
||||
import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js';
|
||||
|
||||
export abstract class ObservableWorkspace {
|
||||
abstract get documents(): IObservableWithChange<readonly IObservableDocument[]>;
|
||||
|
||||
|
||||
getFirstOpenDocument(): IObservableDocument | undefined {
|
||||
return this.documents.get()[0];
|
||||
}
|
||||
|
||||
getDocument(documentId: URI): IObservableDocument | undefined {
|
||||
return this.documents.get().find(d => d.uri.toString() === documentId.toString());
|
||||
}
|
||||
|
||||
private _version = 0;
|
||||
|
||||
/**
|
||||
* Is fired when any open document changes.
|
||||
*/
|
||||
public readonly onDidOpenDocumentChange = derivedHandleChanges({
|
||||
owner: this,
|
||||
changeTracker: {
|
||||
createChangeSummary: () => ({ didChange: false }),
|
||||
handleChange: (ctx, changeSummary) => {
|
||||
if (!ctx.didChange(this.documents)) {
|
||||
changeSummary.didChange = true; // A document changed
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}, (reader, changeSummary) => {
|
||||
const docs = this.documents.read(reader);
|
||||
for (const d of docs) {
|
||||
d.value.read(reader); // add dependency
|
||||
}
|
||||
if (changeSummary.didChange) {
|
||||
this._version++; // to force a change
|
||||
}
|
||||
return this._version;
|
||||
|
||||
// TODO@hediet make this work:
|
||||
/*
|
||||
const docs = this.openDocuments.read(reader);
|
||||
for (const d of docs) {
|
||||
if (reader.readChangesSinceLastRun(d.value).length > 0) {
|
||||
reader.reportChange(d);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
*/
|
||||
});
|
||||
|
||||
public readonly lastActiveDocument = derivedWithStore((_reader, store) => {
|
||||
const obs = observableValue('lastActiveDocument', undefined as IObservableDocument | undefined);
|
||||
store.add(autorunWithStore((reader, store) => {
|
||||
const docs = this.documents.read(reader);
|
||||
for (const d of docs) {
|
||||
store.add(runOnChange(d.value, () => {
|
||||
obs.set(d, undefined);
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return obs;
|
||||
}).flatten();
|
||||
}
|
||||
|
||||
export interface IObservableDocument {
|
||||
readonly uri: URI;
|
||||
readonly value: IObservableWithChange<StringText, StringEditWithReason>;
|
||||
|
||||
/**
|
||||
* Increases whenever the value changes. Is also used to reference document states from the past.
|
||||
*/
|
||||
readonly version: IObservable<number>;
|
||||
readonly languageId: IObservable<string>;
|
||||
}
|
||||
|
||||
export class StringEditWithReason extends StringEdit {
|
||||
constructor(
|
||||
replacements: StringEdit['replacements'],
|
||||
public readonly reason: TextModelEditReason,
|
||||
) {
|
||||
super(replacements);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const EDIT_TELEMETRY_SETTING_ID = 'telemetry.editStats.enabled';
|
||||
export const EDIT_TELEMETRY_SHOW_DECORATIONS = 'telemetry.editStats.showDecorations';
|
||||
export const EDIT_TELEMETRY_SHOW_STATUS_BAR = 'telemetry.editStats.showStatusBar';
|
|
@ -0,0 +1,91 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { onUnexpectedError } from '../../../../base/common/errors.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { derived, IObservable, IObservableWithChange, mapObservableArrayCached, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
|
||||
import { isDefined } from '../../../../base/common/types.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { StringText } from '../../../../editor/common/core/text/abstractText.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { offsetEditFromContentChanges } from '../../../../editor/common/model/textModelStringEdit.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from './observableWorkspace.js';
|
||||
|
||||
export class VSCodeWorkspace extends ObservableWorkspace implements IDisposable {
|
||||
private readonly _documents;
|
||||
public get documents() { return this._documents; }
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly _textModelService: IModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const onModelAdded = observableSignalFromEvent(this, this._textModelService.onModelAdded);
|
||||
const onModelRemoved = observableSignalFromEvent(this, this._textModelService.onModelRemoved);
|
||||
|
||||
const models = derived(this, reader => {
|
||||
onModelAdded.read(reader);
|
||||
onModelRemoved.read(reader);
|
||||
const models = this._textModelService.getModels();
|
||||
return models;
|
||||
});
|
||||
|
||||
const documents = mapObservableArrayCached(this, models, (m, store) => {
|
||||
if (m.isTooLargeForSyncing()) {
|
||||
return undefined;
|
||||
}
|
||||
return store.add(new VSCodeDocument(m));
|
||||
}).recomputeInitiallyAndOnChange(this._store).map(d => d.filter(isDefined));
|
||||
|
||||
this._documents = documents;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class VSCodeDocument extends Disposable implements IObservableDocument {
|
||||
get uri(): URI { return this.textModel.uri; }
|
||||
private readonly _value;
|
||||
private readonly _version;
|
||||
private readonly _languageId;
|
||||
get value(): IObservableWithChange<StringText, StringEditWithReason> { return this._value; }
|
||||
get version(): IObservable<number> { return this._version; }
|
||||
get languageId(): IObservable<string> { return this._languageId; }
|
||||
|
||||
constructor(
|
||||
public readonly textModel: ITextModel,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._value = observableValue<StringText, StringEditWithReason>(this, new StringText(this.textModel.getValue()));
|
||||
this._version = observableValue(this, this.textModel.getVersionId());
|
||||
this._languageId = observableValue(this, this.textModel.getLanguageId());
|
||||
|
||||
this._register(this.textModel.onDidChangeContent((e) => {
|
||||
transaction(tx => {
|
||||
const edit = offsetEditFromContentChanges(e.changes);
|
||||
if (e.detailedReasons.length !== 1) {
|
||||
onUnexpectedError(new Error(`Unexpected number of detailed reasons: ${e.detailedReasons.length}`));
|
||||
}
|
||||
|
||||
const change = new StringEditWithReason(edit.replacements, e.detailedReasons[0]);
|
||||
|
||||
this._value.set(new StringText(this.textModel.getValue()), tx, change);
|
||||
this._version.set(this.textModel.getVersionId(), tx);
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(this.textModel.onDidChangeLanguage(e => {
|
||||
transaction(tx => {
|
||||
this._languageId.set(this.textModel.getLanguageId(), tx);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -417,6 +417,8 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js';
|
|||
// Drop or paste into
|
||||
import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js';
|
||||
|
||||
// Edit Telemetry
|
||||
import './contrib/editTelemetry/browser/editTelemetry.contribution.js';
|
||||
|
||||
|
||||
//#endregion
|
||||
|
|
Loading…
Reference in New Issue