deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
42
packages/scm/src/browser/dirty-diff/content-lines.spec.ts
Normal file
42
packages/scm/src/browser/dirty-diff/content-lines.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as chai from 'chai';
|
||||
import { ContentLines } from './content-lines';
|
||||
import { expect } from 'chai';
|
||||
chai.use(require('chai-string'));
|
||||
|
||||
describe('content-lines', () => {
|
||||
|
||||
it('array-like access of lines without splitting', () => {
|
||||
const raw = 'abc\ndef\n123\n456';
|
||||
const linesArray = ContentLines.arrayLike(ContentLines.fromString(raw));
|
||||
expect(linesArray[0]).to.be.equal('abc');
|
||||
expect(linesArray[1]).to.be.equal('def');
|
||||
expect(linesArray[2]).to.be.equal('123');
|
||||
expect(linesArray[3]).to.be.equal('456');
|
||||
});
|
||||
|
||||
it('works with CRLF', () => {
|
||||
const raw = 'abc\ndef\r\n123\r456';
|
||||
const linesArray = ContentLines.arrayLike(ContentLines.fromString(raw));
|
||||
expect(linesArray[0]).to.be.equal('abc');
|
||||
expect(linesArray[1]).to.be.equal('def');
|
||||
expect(linesArray[2]).to.be.equal('123');
|
||||
expect(linesArray[3]).to.be.equal('456');
|
||||
});
|
||||
|
||||
});
|
||||
121
packages/scm/src/browser/dirty-diff/content-lines.ts
Normal file
121
packages/scm/src/browser/dirty-diff/content-lines.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TextEditorDocument } from '@theia/editor/lib/browser';
|
||||
|
||||
export interface ContentLines extends ArrayLike<string> {
|
||||
readonly length: number,
|
||||
getLineContent: (line: number) => string,
|
||||
}
|
||||
|
||||
export interface ContentLinesArrayLike extends ContentLines, ArrayLike<string> {
|
||||
[Symbol.iterator]: () => IterableIterator<string>,
|
||||
readonly [n: number]: string;
|
||||
}
|
||||
|
||||
export namespace ContentLines {
|
||||
const NL = '\n'.charCodeAt(0);
|
||||
const CR = '\r'.charCodeAt(0);
|
||||
|
||||
export function fromString(content: string): ContentLines {
|
||||
const computeLineStarts: (s: string) => number[] = s => {
|
||||
const result: number[] = [0];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const chr = s.charCodeAt(i);
|
||||
if (chr === CR) {
|
||||
if (i + 1 < s.length && s.charCodeAt(i + 1) === NL) {
|
||||
result[result.length] = i + 2;
|
||||
i++;
|
||||
} else {
|
||||
result[result.length] = i + 1;
|
||||
}
|
||||
} else if (chr === NL) {
|
||||
result[result.length] = i + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const lineStarts = computeLineStarts(content);
|
||||
|
||||
return {
|
||||
length: lineStarts.length,
|
||||
getLineContent: line => {
|
||||
if (line >= lineStarts.length) {
|
||||
throw new Error('line index out of bounds');
|
||||
}
|
||||
const start = lineStarts[line];
|
||||
let end = (line === lineStarts.length - 1) ? undefined : lineStarts[line + 1] - 1;
|
||||
if (!!end && content.charCodeAt(end - 1) === CR) {
|
||||
end--; // ignore CR at the end
|
||||
}
|
||||
const lineContent = content.substring(start, end);
|
||||
return lineContent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fromTextEditorDocument(document: TextEditorDocument): ContentLines {
|
||||
return {
|
||||
length: document.lineCount,
|
||||
getLineContent: line => document.getLineContent(line + 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function arrayLike(lines: ContentLines): ContentLinesArrayLike {
|
||||
return new Proxy(lines as ContentLines, getProxyHandler()) as ContentLinesArrayLike;
|
||||
}
|
||||
|
||||
function getProxyHandler(): ProxyHandler<ContentLinesArrayLike> {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(target: ContentLines, p: PropertyKey): any {
|
||||
switch (p) {
|
||||
case 'prototype':
|
||||
return undefined;
|
||||
case 'length':
|
||||
return target.length;
|
||||
case 'slice':
|
||||
return (start?: number, end?: number) => {
|
||||
if (start !== undefined) {
|
||||
return [start, (end !== undefined ? end - 1 : target.length - 1)];
|
||||
}
|
||||
return [0, target.length - 1];
|
||||
};
|
||||
case Symbol.iterator:
|
||||
return function* (): IterableIterator<string> {
|
||||
for (let i = 0; i < target.length; i++) {
|
||||
yield target.getLineContent(i);
|
||||
}
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const index = Number.parseInt(p as any);
|
||||
if (Number.isInteger(index)) {
|
||||
if (index >= 0 && index < target.length) {
|
||||
const value = target.getLineContent(index);
|
||||
if (value === undefined) {
|
||||
console.log(target);
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
throw new Error(`get ${String(p)} not implemented`);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
455
packages/scm/src/browser/dirty-diff/diff-computer.spec.ts
Normal file
455
packages/scm/src/browser/dirty-diff/diff-computer.spec.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as chai from 'chai';
|
||||
import { expect } from 'chai';
|
||||
chai.use(require('chai-string'));
|
||||
|
||||
import { DiffComputer, DirtyDiff } from './diff-computer';
|
||||
import { ContentLines } from './content-lines';
|
||||
|
||||
let diffComputer: DiffComputer;
|
||||
|
||||
before(() => {
|
||||
diffComputer = new DiffComputer();
|
||||
});
|
||||
|
||||
describe('dirty-diff-computer', () => {
|
||||
|
||||
it('remove single line', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'FIRST',
|
||||
'SECOND TO-BE-REMOVED',
|
||||
'THIRD'
|
||||
],
|
||||
[
|
||||
'FIRST',
|
||||
'THIRD'
|
||||
],
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 2 },
|
||||
currentRange: { start: 1, end: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 20].forEach(lines => {
|
||||
it(`remove ${formatLines(lines)} at the end`, () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(2)
|
||||
.concat(sequenceOfN(lines, () => 'TO-BE-REMOVED')),
|
||||
sequenceOfN(2),
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 2, end: 2 + lines },
|
||||
currentRange: { start: 2, end: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('remove all lines', () => {
|
||||
const numberOfLines = 10;
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(numberOfLines, () => 'TO-BE-REMOVED'),
|
||||
['']
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: numberOfLines },
|
||||
currentRange: { start: 0, end: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 20].forEach(lines => {
|
||||
it(`remove ${formatLines(lines)} at the beginning`, () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(lines, () => 'TO-BE-REMOVED')
|
||||
.concat(sequenceOfN(2)),
|
||||
sequenceOfN(2),
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: lines },
|
||||
currentRange: { start: 0, end: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 20].forEach(lines => {
|
||||
it(`add ${formatLines(lines)}`, () => {
|
||||
const previous = sequenceOfN(3);
|
||||
const modified = insertIntoArray(previous, 2, ...sequenceOfN(lines, () => 'ADDED LINE'));
|
||||
const dirtyDiff = computeDirtyDiff(previous, modified);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 2, end: 2 },
|
||||
currentRange: { start: 2, end: 2 + lines },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 20].forEach(lines => {
|
||||
it(`add ${formatLines(lines)} at the beginning`, () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(2),
|
||||
sequenceOfN(lines, () => 'ADDED LINE')
|
||||
.concat(sequenceOfN(2))
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: 0 },
|
||||
currentRange: { start: 0, end: lines },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('add lines to empty file', () => {
|
||||
const numberOfLines = 3;
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[''],
|
||||
sequenceOfN(numberOfLines, () => 'ADDED LINE')
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: 0 },
|
||||
currentRange: { start: 0, end: numberOfLines },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('add empty lines', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'1',
|
||||
'2'
|
||||
],
|
||||
[
|
||||
'1',
|
||||
'',
|
||||
'',
|
||||
'2'
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 1 },
|
||||
currentRange: { start: 1, end: 3 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('add empty line after single line', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'1'
|
||||
],
|
||||
[
|
||||
'1',
|
||||
''
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 1 },
|
||||
currentRange: { start: 1, end: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 20].forEach(lines => {
|
||||
it(`add ${formatLines(lines)} (empty) at the end`, () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(2),
|
||||
sequenceOfN(2)
|
||||
.concat(new Array(lines).map(() => ''))
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 2, end: 2 },
|
||||
currentRange: { start: 2, end: 2 + lines },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('add empty and non-empty lines', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'FIRST',
|
||||
'LAST'
|
||||
],
|
||||
[
|
||||
'FIRST',
|
||||
'1. ADDED',
|
||||
'2. ADDED',
|
||||
'3. ADDED',
|
||||
'4. ADDED',
|
||||
'5. ADDED',
|
||||
'LAST'
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 1 },
|
||||
currentRange: { start: 1, end: 6 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
[1, 2, 3, 4, 5].forEach(lines => {
|
||||
it(`add ${formatLines(lines)} after single line`, () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
['0'],
|
||||
['0'].concat(sequenceOfN(lines, () => 'ADDED LINE'))
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 1 },
|
||||
currentRange: { start: 1, end: lines + 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('modify single line', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'FIRST',
|
||||
'TO-BE-MODIFIED',
|
||||
'LAST'
|
||||
],
|
||||
[
|
||||
'FIRST',
|
||||
'MODIFIED',
|
||||
'LAST'
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 2 },
|
||||
currentRange: { start: 1, end: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('modify all lines', () => {
|
||||
const numberOfLines = 10;
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
sequenceOfN(numberOfLines, () => 'TO-BE-MODIFIED'),
|
||||
sequenceOfN(numberOfLines, () => 'MODIFIED')
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: numberOfLines },
|
||||
currentRange: { start: 0, end: numberOfLines },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('modify lines at the end', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4'
|
||||
],
|
||||
[
|
||||
'1',
|
||||
'2-changed',
|
||||
'3-changed'
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 1, end: 4 },
|
||||
currentRange: { start: 1, end: 3 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('multiple diffs', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'TO-BE-CHANGED',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'TO-BE-REMOVED',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9'
|
||||
],
|
||||
[
|
||||
'CHANGED',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'ADDED',
|
||||
''
|
||||
]
|
||||
);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 0, end: 1 },
|
||||
currentRange: { start: 0, end: 1 },
|
||||
},
|
||||
{
|
||||
previousRange: { start: 4, end: 5 },
|
||||
currentRange: { start: 4, end: 4 },
|
||||
},
|
||||
{
|
||||
previousRange: { start: 11, end: 11 },
|
||||
currentRange: { start: 10, end: 12 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('multiple additions', () => {
|
||||
const dirtyDiff = computeDirtyDiff(
|
||||
[
|
||||
'first line',
|
||||
'',
|
||||
'foo changed on master',
|
||||
'bar changed on master',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'last line'
|
||||
],
|
||||
[
|
||||
'first line',
|
||||
'',
|
||||
'foo changed on master',
|
||||
'bar changed on master',
|
||||
'',
|
||||
'NEW TEXT',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'last line',
|
||||
'',
|
||||
''
|
||||
]);
|
||||
expect(dirtyDiff).to.be.deep.equal(<DirtyDiff>{
|
||||
changes: [
|
||||
{
|
||||
previousRange: { start: 5, end: 5 },
|
||||
currentRange: { start: 5, end: 6 },
|
||||
},
|
||||
{
|
||||
previousRange: { start: 8, end: 8 },
|
||||
currentRange: { start: 9, end: 10 },
|
||||
},
|
||||
{
|
||||
previousRange: { start: 9, end: 10 },
|
||||
currentRange: { start: 12, end: 12 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function computeDirtyDiff(previous: string[], modified: string[]): DirtyDiff {
|
||||
const a = ContentLines.arrayLike({
|
||||
length: previous.length,
|
||||
getLineContent: line => {
|
||||
const value = previous[line];
|
||||
if (value === undefined) {
|
||||
console.log(undefined);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const b = ContentLines.arrayLike({
|
||||
length: modified.length,
|
||||
getLineContent: line => {
|
||||
const value = modified[line];
|
||||
if (value === undefined) {
|
||||
console.log(undefined);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
return diffComputer.computeDirtyDiff(a, b);
|
||||
}
|
||||
|
||||
function sequenceOfN(n: number, mapFn: (index: number) => string = i => i.toString()): string[] {
|
||||
return Array.from(new Array(n).keys()).map((value, index) => mapFn(index));
|
||||
}
|
||||
|
||||
function formatLines(n: number): string {
|
||||
return n + ' line' + (n > 1 ? 's' : '');
|
||||
}
|
||||
|
||||
function insertIntoArray(target: string[], start: number, ...items: string[]): string[] {
|
||||
const copy = target.slice(0);
|
||||
copy.splice(start, 0, ...items);
|
||||
return copy;
|
||||
}
|
||||
177
packages/scm/src/browser/dirty-diff/diff-computer.ts
Normal file
177
packages/scm/src/browser/dirty-diff/diff-computer.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as jsdiff from 'diff';
|
||||
import { ContentLinesArrayLike } from './content-lines';
|
||||
import { Position, Range, uinteger } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
|
||||
export class DiffComputer {
|
||||
|
||||
computeDiff(previous: ContentLinesArrayLike, current: ContentLinesArrayLike): DiffResult[] {
|
||||
const diffResult = diffArrays(previous, current);
|
||||
return diffResult;
|
||||
}
|
||||
|
||||
computeDirtyDiff(previous: ContentLinesArrayLike, current: ContentLinesArrayLike): DirtyDiff {
|
||||
const changes: Change[] = [];
|
||||
const diffResult = this.computeDiff(previous, current);
|
||||
let currentRevisionLine = -1;
|
||||
let previousRevisionLine = -1;
|
||||
for (let i = 0; i < diffResult.length; i++) {
|
||||
const change = diffResult[i];
|
||||
const next = diffResult[i + 1];
|
||||
if (change.added) {
|
||||
// case: addition
|
||||
changes.push({ previousRange: LineRange.createEmptyLineRange(previousRevisionLine + 1), currentRange: toLineRange(change) });
|
||||
currentRevisionLine += change.count!;
|
||||
} else if (change.removed && next && next.added) {
|
||||
const isFirstChange = i === 0;
|
||||
const isLastChange = i === diffResult.length - 2;
|
||||
const isNextEmptyLine = next.value.length > 0 && current[next.value[0]].length === 0;
|
||||
const isPrevEmptyLine = change.value.length > 0 && previous[change.value[0]].length === 0;
|
||||
|
||||
if (isFirstChange && isNextEmptyLine) {
|
||||
// special case: removing at the beginning
|
||||
changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(0) });
|
||||
previousRevisionLine += change.count!;
|
||||
} else if (isFirstChange && isPrevEmptyLine) {
|
||||
// special case: adding at the beginning
|
||||
changes.push({ previousRange: LineRange.createEmptyLineRange(0), currentRange: toLineRange(next) });
|
||||
currentRevisionLine += next.count!;
|
||||
} else if (isLastChange && isNextEmptyLine) {
|
||||
changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(currentRevisionLine + 2) });
|
||||
previousRevisionLine += change.count!;
|
||||
} else {
|
||||
// default case is a modification
|
||||
changes.push({ previousRange: toLineRange(change), currentRange: toLineRange(next) });
|
||||
currentRevisionLine += next.count!;
|
||||
previousRevisionLine += change.count!;
|
||||
}
|
||||
i++; // consume next eagerly
|
||||
} else if (change.removed && !(next && next.added)) {
|
||||
// case: removal
|
||||
changes.push({ previousRange: toLineRange(change), currentRange: LineRange.createEmptyLineRange(currentRevisionLine + 1) });
|
||||
previousRevisionLine += change.count!;
|
||||
} else {
|
||||
// case: unchanged region
|
||||
currentRevisionLine += change.count!;
|
||||
previousRevisionLine += change.count!;
|
||||
}
|
||||
}
|
||||
return { changes };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ArrayDiff extends jsdiff.Diff {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override tokenize(value: any): any {
|
||||
return value;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override join(value: any): any {
|
||||
return value;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override removeEmpty(value: any): any {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const arrayDiff = new ArrayDiff();
|
||||
|
||||
/**
|
||||
* Computes diff without copying data.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function diffArrays(oldArr: ContentLinesArrayLike, newArr: ContentLinesArrayLike): DiffResult[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return arrayDiff.diff(oldArr as any, newArr as any) as any;
|
||||
}
|
||||
|
||||
function toLineRange({ value }: DiffResult): LineRange {
|
||||
const [start, end] = value;
|
||||
return LineRange.create(start, end + 1);
|
||||
}
|
||||
|
||||
export interface DiffResult {
|
||||
value: [number, number];
|
||||
count?: number;
|
||||
added?: boolean;
|
||||
removed?: boolean;
|
||||
}
|
||||
|
||||
export interface DirtyDiff {
|
||||
readonly changes: readonly Change[];
|
||||
}
|
||||
|
||||
export interface Change {
|
||||
readonly previousRange: LineRange;
|
||||
readonly currentRange: LineRange;
|
||||
}
|
||||
|
||||
export namespace Change {
|
||||
export function isAddition(change: Change): boolean {
|
||||
return LineRange.isEmpty(change.previousRange);
|
||||
}
|
||||
export function isRemoval(change: Change): boolean {
|
||||
return LineRange.isEmpty(change.currentRange);
|
||||
}
|
||||
export function isModification(change: Change): boolean {
|
||||
return !isAddition(change) && !isRemoval(change);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LineRange {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
export namespace LineRange {
|
||||
export function create(start: number, end: number): LineRange {
|
||||
if (start < 0 || end < 0 || start > end) {
|
||||
throw new Error(`Invalid line range: { start: ${start}, end: ${end} }`);
|
||||
}
|
||||
return { start, end };
|
||||
}
|
||||
export function createSingleLineRange(line: number): LineRange {
|
||||
return create(line, line + 1);
|
||||
}
|
||||
export function createEmptyLineRange(line: number): LineRange {
|
||||
return create(line, line);
|
||||
}
|
||||
export function isEmpty(range: LineRange): boolean {
|
||||
return range.start === range.end;
|
||||
}
|
||||
export function getStartPosition(range: LineRange): Position {
|
||||
if (isEmpty(range)) {
|
||||
return getEndPosition(range);
|
||||
}
|
||||
return Position.create(range.start, 0);
|
||||
}
|
||||
export function getEndPosition(range: LineRange): Position {
|
||||
if (range.end < 1) {
|
||||
return Position.create(0, 0);
|
||||
}
|
||||
return Position.create(range.end - 1, uinteger.MAX_VALUE);
|
||||
}
|
||||
export function toRange(range: LineRange): Range {
|
||||
return Range.create(getStartPosition(range), getEndPosition(range));
|
||||
}
|
||||
export function getLineCount(range: LineRange): number {
|
||||
return range.end - range.start;
|
||||
}
|
||||
}
|
||||
114
packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts
Normal file
114
packages/scm/src/browser/dirty-diff/dirty-diff-decorator.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
EditorDecoration,
|
||||
EditorDecorationOptions,
|
||||
OverviewRulerLane,
|
||||
EditorDecorator,
|
||||
TextEditor,
|
||||
MinimapPosition
|
||||
} from '@theia/editor/lib/browser';
|
||||
import { DirtyDiff, LineRange, Change } from './diff-computer';
|
||||
import { URI } from '@theia/core';
|
||||
|
||||
export enum DirtyDiffDecorationType {
|
||||
AddedLine = 'dirty-diff-added-line',
|
||||
RemovedLine = 'dirty-diff-removed-line',
|
||||
ModifiedLine = 'dirty-diff-modified-line',
|
||||
}
|
||||
|
||||
const AddedLineDecoration = <EditorDecorationOptions>{
|
||||
linesDecorationsClassName: 'dirty-diff-glyph dirty-diff-added-line',
|
||||
overviewRuler: {
|
||||
color: {
|
||||
id: 'editorOverviewRuler.addedForeground'
|
||||
},
|
||||
position: OverviewRulerLane.Left,
|
||||
},
|
||||
minimap: {
|
||||
color: {
|
||||
id: 'minimapGutter.addedBackground'
|
||||
},
|
||||
position: MinimapPosition.Gutter
|
||||
},
|
||||
isWholeLine: true
|
||||
};
|
||||
|
||||
const RemovedLineDecoration = <EditorDecorationOptions>{
|
||||
linesDecorationsClassName: 'dirty-diff-glyph dirty-diff-removed-line',
|
||||
overviewRuler: {
|
||||
color: {
|
||||
id: 'editorOverviewRuler.deletedForeground'
|
||||
},
|
||||
position: OverviewRulerLane.Left,
|
||||
},
|
||||
minimap: {
|
||||
color: {
|
||||
id: 'minimapGutter.deletedBackground'
|
||||
},
|
||||
position: MinimapPosition.Gutter
|
||||
},
|
||||
isWholeLine: false
|
||||
};
|
||||
|
||||
const ModifiedLineDecoration = <EditorDecorationOptions>{
|
||||
linesDecorationsClassName: 'dirty-diff-glyph dirty-diff-modified-line',
|
||||
overviewRuler: {
|
||||
color: {
|
||||
id: 'editorOverviewRuler.modifiedForeground'
|
||||
},
|
||||
position: OverviewRulerLane.Left,
|
||||
},
|
||||
minimap: {
|
||||
color: {
|
||||
id: 'minimapGutter.modifiedBackground'
|
||||
},
|
||||
position: MinimapPosition.Gutter
|
||||
},
|
||||
isWholeLine: true
|
||||
};
|
||||
|
||||
function getEditorDecorationOptions(change: Change): EditorDecorationOptions {
|
||||
if (Change.isAddition(change)) {
|
||||
return AddedLineDecoration;
|
||||
}
|
||||
if (Change.isRemoval(change)) {
|
||||
return RemovedLineDecoration;
|
||||
}
|
||||
return ModifiedLineDecoration;
|
||||
}
|
||||
|
||||
export interface DirtyDiffUpdate extends DirtyDiff {
|
||||
readonly editor: TextEditor;
|
||||
readonly previousRevisionUri?: URI;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DirtyDiffDecorator extends EditorDecorator {
|
||||
|
||||
applyDecorations(update: DirtyDiffUpdate): void {
|
||||
const decorations = update.changes.map(change => this.toDeltaDecoration(change));
|
||||
this.setDecorations(update.editor, decorations);
|
||||
}
|
||||
|
||||
protected toDeltaDecoration(change: Change): EditorDecoration {
|
||||
const range = LineRange.toRange(change.currentRange);
|
||||
const options = getEditorDecorationOptions(change);
|
||||
return { range, options };
|
||||
}
|
||||
}
|
||||
33
packages/scm/src/browser/dirty-diff/dirty-diff-module.ts
Normal file
33
packages/scm/src/browser/dirty-diff/dirty-diff-module.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { DirtyDiffDecorator } from './dirty-diff-decorator';
|
||||
import { DirtyDiffNavigator } from './dirty-diff-navigator';
|
||||
import { DirtyDiffWidget, DirtyDiffWidgetFactory, DirtyDiffWidgetProps } from './dirty-diff-widget';
|
||||
|
||||
import '../../../src/browser/style/dirty-diff.css';
|
||||
|
||||
export function bindDirtyDiff(bind: interfaces.Bind): void {
|
||||
bind(DirtyDiffDecorator).toSelf().inSingletonScope();
|
||||
bind(DirtyDiffNavigator).toSelf().inSingletonScope();
|
||||
bind(DirtyDiffWidgetFactory).toFactory(({ container }) => props => {
|
||||
const child = container.createChild();
|
||||
child.bind(DirtyDiffWidgetProps).toConstantValue(props);
|
||||
child.bind(DirtyDiffWidget).toSelf();
|
||||
return child.get(DirtyDiffWidget);
|
||||
});
|
||||
}
|
||||
291
packages/scm/src/browser/dirty-diff/dirty-diff-navigator.ts
Normal file
291
packages/scm/src/browser/dirty-diff/dirty-diff-navigator.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 1C-Soft LLC and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection, URI } from '@theia/core';
|
||||
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { EditorManager, EditorMouseEvent, MouseTargetType, TextEditor } from '@theia/editor/lib/browser';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { Change, LineRange } from './diff-computer';
|
||||
import { DirtyDiffUpdate } from './dirty-diff-decorator';
|
||||
import { DirtyDiffWidget, DirtyDiffWidgetFactory } from './dirty-diff-widget';
|
||||
|
||||
@injectable()
|
||||
export class DirtyDiffNavigator {
|
||||
|
||||
protected readonly controllers = new Map<TextEditor, DirtyDiffController>();
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(DirtyDiffWidgetFactory)
|
||||
protected readonly widgetFactory: DirtyDiffWidgetFactory;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
const dirtyDiffVisible: ContextKey<boolean> = this.contextKeyService.createKey('dirtyDiffVisible', false);
|
||||
this.editorManager.onActiveEditorChanged(editorWidget => {
|
||||
dirtyDiffVisible.set(editorWidget && this.controllers.get(editorWidget.editor)?.isShowingChange());
|
||||
});
|
||||
this.editorManager.onCreated(editorWidget => {
|
||||
const { editor } = editorWidget;
|
||||
if (editor.uri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
const controller = this.createController(editor);
|
||||
controller.widgetFactory = props => {
|
||||
const widget = this.widgetFactory(props);
|
||||
if (widget.editor === this.editorManager.activeEditor?.editor) {
|
||||
dirtyDiffVisible.set(true);
|
||||
}
|
||||
widget.onDidClose(() => {
|
||||
if (widget.editor === this.editorManager.activeEditor?.editor) {
|
||||
dirtyDiffVisible.set(false);
|
||||
}
|
||||
});
|
||||
return widget;
|
||||
};
|
||||
this.controllers.set(editor, controller);
|
||||
editorWidget.disposed.connect(() => {
|
||||
this.controllers.delete(editor);
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleDirtyDiffUpdate(update: DirtyDiffUpdate): void {
|
||||
const controller = this.controllers.get(update.editor);
|
||||
controller?.handleDirtyDiffUpdate(update);
|
||||
}
|
||||
|
||||
canNavigate(): boolean {
|
||||
return !!this.activeController?.canNavigate();
|
||||
}
|
||||
|
||||
gotoNextChange(): void {
|
||||
this.activeController?.gotoNextChange();
|
||||
}
|
||||
|
||||
gotoPreviousChange(): void {
|
||||
this.activeController?.gotoPreviousChange();
|
||||
}
|
||||
|
||||
canShowChange(): boolean {
|
||||
return !!this.activeController?.canShowChange();
|
||||
}
|
||||
|
||||
showNextChange(): void {
|
||||
this.activeController?.showNextChange();
|
||||
}
|
||||
|
||||
showPreviousChange(): void {
|
||||
this.activeController?.showPreviousChange();
|
||||
}
|
||||
|
||||
isShowingChange(): boolean {
|
||||
return !!this.activeController?.isShowingChange();
|
||||
}
|
||||
|
||||
closeChangePeekView(): void {
|
||||
this.activeController?.closeWidget();
|
||||
}
|
||||
|
||||
protected get activeController(): DirtyDiffController | undefined {
|
||||
const editor = this.editorManager.activeEditor?.editor;
|
||||
return editor && this.controllers.get(editor);
|
||||
}
|
||||
|
||||
protected createController(editor: TextEditor): DirtyDiffController {
|
||||
return new DirtyDiffController(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export class DirtyDiffController implements Disposable {
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
widgetFactory?: DirtyDiffWidgetFactory;
|
||||
protected widget?: DirtyDiffWidget;
|
||||
protected dirtyDiff?: DirtyDiffUpdate;
|
||||
|
||||
constructor(protected readonly editor: TextEditor) {
|
||||
editor.onMouseDown(this.handleEditorMouseDown, this, this.toDispose);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.closeWidget();
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
handleDirtyDiffUpdate(dirtyDiff: DirtyDiffUpdate): void {
|
||||
if (dirtyDiff.editor === this.editor) {
|
||||
this.dirtyDiff = dirtyDiff;
|
||||
if (this.widget) {
|
||||
this.widget.changes = dirtyDiff.changes ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canNavigate(): boolean {
|
||||
return !!this.changes?.length;
|
||||
}
|
||||
|
||||
gotoNextChange(): void {
|
||||
const { editor } = this;
|
||||
const index = this.findNextClosestChange(editor.cursor.line, false);
|
||||
const change = this.changes?.[index];
|
||||
if (change) {
|
||||
const position = LineRange.getStartPosition(change.currentRange);
|
||||
editor.cursor = position;
|
||||
editor.revealPosition(position, { vertical: 'auto' });
|
||||
}
|
||||
}
|
||||
|
||||
gotoPreviousChange(): void {
|
||||
const { editor } = this;
|
||||
const index = this.findPreviousClosestChange(editor.cursor.line, false);
|
||||
const change = this.changes?.[index];
|
||||
if (change) {
|
||||
const position = LineRange.getStartPosition(change.currentRange);
|
||||
editor.cursor = position;
|
||||
editor.revealPosition(position, { vertical: 'auto' });
|
||||
}
|
||||
}
|
||||
|
||||
canShowChange(): boolean {
|
||||
return !!(this.widget || this.widgetFactory && this.editor instanceof MonacoEditor && this.changes?.length && this.previousRevisionUri);
|
||||
}
|
||||
|
||||
showNextChange(): void {
|
||||
if (this.widget) {
|
||||
this.widget.showNextChange();
|
||||
} else {
|
||||
(this.widget = this.createWidget())?.showChange(
|
||||
this.findNextClosestChange(this.editor.cursor.line, true));
|
||||
}
|
||||
}
|
||||
|
||||
showPreviousChange(): void {
|
||||
if (this.widget) {
|
||||
this.widget.showPreviousChange();
|
||||
} else {
|
||||
(this.widget = this.createWidget())?.showChange(
|
||||
this.findPreviousClosestChange(this.editor.cursor.line, true));
|
||||
}
|
||||
}
|
||||
|
||||
isShowingChange(): boolean {
|
||||
return !!this.widget;
|
||||
}
|
||||
|
||||
closeWidget(): void {
|
||||
if (this.widget) {
|
||||
this.widget.dispose();
|
||||
this.widget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected get changes(): readonly Change[] | undefined {
|
||||
return this.dirtyDiff?.changes;
|
||||
}
|
||||
|
||||
protected get previousRevisionUri(): URI | undefined {
|
||||
return this.dirtyDiff?.previousRevisionUri;
|
||||
}
|
||||
|
||||
protected createWidget(): DirtyDiffWidget | undefined {
|
||||
const { widgetFactory, editor, changes, previousRevisionUri } = this;
|
||||
if (widgetFactory && editor instanceof MonacoEditor && changes?.length && previousRevisionUri) {
|
||||
const widget = widgetFactory({ editor, previousRevisionUri });
|
||||
widget.changes = changes;
|
||||
widget.onDidClose(() => {
|
||||
this.widget = undefined;
|
||||
});
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
protected findNextClosestChange(line: number, inclusive: boolean): number {
|
||||
const length = this.changes?.length;
|
||||
if (!length) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
const { currentRange } = this.changes![i];
|
||||
|
||||
if (inclusive) {
|
||||
if (LineRange.getEndPosition(currentRange).line >= line) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
if (LineRange.getStartPosition(currentRange).line > line) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected findPreviousClosestChange(line: number, inclusive: boolean): number {
|
||||
const length = this.changes?.length;
|
||||
if (!length) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = length - 1; i >= 0; i--) {
|
||||
const { currentRange } = this.changes![i];
|
||||
|
||||
if (inclusive) {
|
||||
if (LineRange.getStartPosition(currentRange).line <= line) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
if (LineRange.getEndPosition(currentRange).line < line) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return length - 1;
|
||||
}
|
||||
|
||||
protected handleEditorMouseDown({ event, target }: EditorMouseEvent): void {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const { range, type, element } = target;
|
||||
if (!range || type !== MouseTargetType.GUTTER_LINE_DECORATIONS || !element || element.className.indexOf('dirty-diff-glyph') < 0) {
|
||||
return;
|
||||
}
|
||||
const gutterOffsetX = target.detail.offsetX - (element as HTMLElement).offsetLeft;
|
||||
if (gutterOffsetX < -3 || gutterOffsetX > 3) { // dirty diff decoration on hover is 6px wide
|
||||
return; // to avoid colliding with folding
|
||||
}
|
||||
const index = this.findNextClosestChange(range.start.line, true);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (index === this.widget?.currentChangeIndex) {
|
||||
this.closeWidget();
|
||||
return;
|
||||
}
|
||||
if (!this.widget) {
|
||||
this.widget = this.createWidget();
|
||||
}
|
||||
this.widget?.showChange(index);
|
||||
}
|
||||
}
|
||||
413
packages/scm/src/browser/dirty-diff/dirty-diff-widget.ts
Normal file
413
packages/scm/src/browser/dirty-diff/dirty-diff-widget.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 1C-Soft LLC and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { CommandMenu, Disposable, Emitter, Event, MenuModelRegistry, MenuPath, URI, nls } from '@theia/core';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { MonacoEditorPeekViewWidget, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground }
|
||||
from '@theia/monaco/lib/browser/monaco-editor-peek-view-widget';
|
||||
import { Change, LineRange } from './diff-computer';
|
||||
import { ScmColors } from '../scm-colors';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
export const SCM_CHANGE_TITLE_MENU: MenuPath = ['scm-change-title-menu'];
|
||||
/** Reserved for plugin contributions, corresponds to contribution point 'scm/change/title'. */
|
||||
export const PLUGIN_SCM_CHANGE_TITLE_MENU: MenuPath = ['plugin-scm-change-title-menu'];
|
||||
|
||||
export const DirtyDiffWidgetProps = Symbol('DirtyDiffWidgetProps');
|
||||
export interface DirtyDiffWidgetProps {
|
||||
readonly editor: MonacoEditor;
|
||||
readonly previousRevisionUri: URI;
|
||||
}
|
||||
|
||||
export const DirtyDiffWidgetFactory = Symbol('DirtyDiffWidgetFactory');
|
||||
export type DirtyDiffWidgetFactory = (props: DirtyDiffWidgetProps) => DirtyDiffWidget;
|
||||
|
||||
@injectable()
|
||||
export class DirtyDiffWidget implements Disposable {
|
||||
|
||||
private readonly onDidCloseEmitter = new Emitter<unknown>();
|
||||
readonly onDidClose: Event<unknown> = this.onDidCloseEmitter.event;
|
||||
protected index: number = -1;
|
||||
private peekView: DirtyDiffPeekView;
|
||||
private diffEditorPromise: Promise<MonacoDiffEditor>;
|
||||
protected _changes?: readonly Change[];
|
||||
|
||||
constructor(
|
||||
@inject(DirtyDiffWidgetProps) protected readonly props: DirtyDiffWidgetProps,
|
||||
@inject(MonacoEditorProvider) readonly editorProvider: MonacoEditorProvider,
|
||||
@inject(ContextKeyService) readonly contextKeyService: ContextKeyService,
|
||||
@inject(MenuModelRegistry) readonly menuModelRegistry: MenuModelRegistry,
|
||||
) { }
|
||||
|
||||
@postConstruct()
|
||||
create(): void {
|
||||
this.peekView = new DirtyDiffPeekView(this);
|
||||
this.peekView.onDidClose(e => this.onDidCloseEmitter.fire(e));
|
||||
this.diffEditorPromise = this.peekView.create();
|
||||
}
|
||||
|
||||
get changes(): readonly Change[] {
|
||||
return this._changes ?? [];
|
||||
}
|
||||
|
||||
set changes(changes: readonly Change[]) {
|
||||
this.handleChangedChanges(changes);
|
||||
}
|
||||
|
||||
get editor(): MonacoEditor {
|
||||
return this.props.editor;
|
||||
}
|
||||
|
||||
get uri(): URI {
|
||||
return this.editor.uri;
|
||||
}
|
||||
|
||||
get previousRevisionUri(): URI {
|
||||
return this.props.previousRevisionUri;
|
||||
}
|
||||
|
||||
get currentChange(): Change | undefined {
|
||||
return this.changes[this.index];
|
||||
}
|
||||
|
||||
get currentChangeIndex(): number {
|
||||
return this.index;
|
||||
}
|
||||
|
||||
protected handleChangedChanges(updated: readonly Change[]): void {
|
||||
if (!updated.length) {
|
||||
return this.dispose();
|
||||
}
|
||||
if (this.currentChange) {
|
||||
const { previousRange: { start, end } } = this.currentChange;
|
||||
// Same change or first after it.
|
||||
const newIndex = updated.findIndex(candidate => (candidate.previousRange.start === start && candidate.previousRange.end === end)
|
||||
|| candidate.previousRange.start > start);
|
||||
if (newIndex !== -1) {
|
||||
this.index = newIndex;
|
||||
} else {
|
||||
this.index = Math.min(this.index, updated.length - 1);
|
||||
}
|
||||
this.showCurrentChange();
|
||||
} else {
|
||||
this.index = -1;
|
||||
}
|
||||
this._changes = updated;
|
||||
this.updateHeading();
|
||||
}
|
||||
|
||||
async showChange(index: number): Promise<void> {
|
||||
await this.checkCreated();
|
||||
if (index >= 0 && index < this.changes.length) {
|
||||
this.index = index;
|
||||
this.showCurrentChange();
|
||||
}
|
||||
}
|
||||
|
||||
showNextChange(): void {
|
||||
this.checkCreated();
|
||||
const index = this.index;
|
||||
const length = this.changes.length;
|
||||
if (length > 0 && (index < 0 || length > 1)) {
|
||||
this.index = index < 0 ? 0 : cycle(index, 1, length);
|
||||
this.showCurrentChange();
|
||||
}
|
||||
}
|
||||
|
||||
showPreviousChange(): void {
|
||||
this.checkCreated();
|
||||
const index = this.index;
|
||||
const length = this.changes.length;
|
||||
if (length > 0 && (index < 0 || length > 1)) {
|
||||
this.index = index < 0 ? length - 1 : cycle(index, -1, length);
|
||||
this.showCurrentChange();
|
||||
}
|
||||
}
|
||||
|
||||
async getContentWithSelectedChanges(predicate: (change: Change, index: number, changes: readonly Change[]) => boolean): Promise<string> {
|
||||
await this.checkCreated();
|
||||
const changes = this.changes.filter(predicate);
|
||||
const { diffEditor } = await this.diffEditorPromise!;
|
||||
const diffEditorModel = diffEditor.getModel()!;
|
||||
return applyChanges(changes, diffEditorModel.original, diffEditorModel.modified);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.peekView?.dispose();
|
||||
this.onDidCloseEmitter.dispose();
|
||||
}
|
||||
|
||||
protected showCurrentChange(): void {
|
||||
this.updateHeading();
|
||||
const { previousRange, currentRange } = this.changes[this.index];
|
||||
this.peekView.show(Position.create(LineRange.getEndPosition(currentRange).line, 0),
|
||||
this.computeHeightInLines());
|
||||
this.diffEditorPromise.then(({ diffEditor }) => {
|
||||
let startLine = LineRange.getStartPosition(currentRange).line;
|
||||
let endLine = LineRange.getEndPosition(currentRange).line;
|
||||
if (LineRange.isEmpty(currentRange)) { // the change is a removal
|
||||
++endLine;
|
||||
} else if (!LineRange.isEmpty(previousRange)) { // the change is a modification
|
||||
--startLine;
|
||||
++endLine;
|
||||
}
|
||||
diffEditor.revealLinesInCenter(startLine + 1, endLine + 1, // monaco line numbers are 1-based
|
||||
monaco.editor.ScrollType.Immediate);
|
||||
});
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
protected updateHeading(): void {
|
||||
this.peekView.setTitle(this.computePrimaryHeading(), this.computeSecondaryHeading());
|
||||
}
|
||||
|
||||
protected computePrimaryHeading(): string {
|
||||
return this.uri.path.base;
|
||||
}
|
||||
|
||||
protected computeSecondaryHeading(): string {
|
||||
const index = this.index + 1;
|
||||
const length = this.changes.length;
|
||||
return length > 1 ? nls.localizeByDefault('{0} of {1} changes', index, length) :
|
||||
nls.localizeByDefault('{0} of {1} change', index, length);
|
||||
}
|
||||
|
||||
protected computeHeightInLines(): number {
|
||||
const editor = this.editor.getControl();
|
||||
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const editorHeight = editor.getLayoutInfo().height;
|
||||
const editorHeightInLines = Math.floor(editorHeight / lineHeight);
|
||||
|
||||
const { previousRange, currentRange } = this.changes[this.index];
|
||||
const changeHeightInLines = LineRange.getLineCount(currentRange) + LineRange.getLineCount(previousRange);
|
||||
|
||||
return Math.min(changeHeightInLines + /* padding */ 8, Math.floor(editorHeightInLines / 3));
|
||||
}
|
||||
|
||||
protected async checkCreated(): Promise<MonacoDiffEditor> {
|
||||
return this.diffEditorPromise;
|
||||
}
|
||||
}
|
||||
|
||||
function cycle(index: number, offset: -1 | 1, length: number): number {
|
||||
return (index + offset + length) % length;
|
||||
}
|
||||
|
||||
// adapted from https://github.com/microsoft/vscode/blob/823d54f86ee13eb357bc6e8e562e89d793f3c43b/extensions/git/src/staging.ts
|
||||
function applyChanges(changes: readonly Change[], original: monaco.editor.ITextModel, modified: monaco.editor.ITextModel): string {
|
||||
const result: string[] = [];
|
||||
let currentLine = 1;
|
||||
|
||||
for (const change of changes) {
|
||||
const { previousRange, currentRange } = change;
|
||||
|
||||
const isInsertion = LineRange.isEmpty(previousRange);
|
||||
const isDeletion = LineRange.isEmpty(currentRange);
|
||||
|
||||
const convert = (range: LineRange): [number, number] => {
|
||||
let startLineNumber;
|
||||
let endLineNumber;
|
||||
if (!LineRange.isEmpty(range)) {
|
||||
startLineNumber = range.start + 1;
|
||||
endLineNumber = range.end;
|
||||
} else {
|
||||
startLineNumber = range.start;
|
||||
endLineNumber = 0;
|
||||
}
|
||||
return [startLineNumber, endLineNumber];
|
||||
};
|
||||
|
||||
const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange);
|
||||
const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange);
|
||||
|
||||
let toLine = isInsertion ? originalStartLineNumber + 1 : originalStartLineNumber;
|
||||
let toCharacter = 1;
|
||||
|
||||
// if this is a deletion at the very end of the document,
|
||||
// we need to account for a newline at the end of the last line,
|
||||
// which may have been deleted
|
||||
if (isDeletion && originalEndLineNumber === original.getLineCount()) {
|
||||
toLine--;
|
||||
toCharacter = original.getLineMaxColumn(toLine);
|
||||
}
|
||||
|
||||
result.push(original.getValueInRange(new monaco.Range(currentLine, 1, toLine, toCharacter)));
|
||||
|
||||
if (!isDeletion) {
|
||||
let fromLine = modifiedStartLineNumber;
|
||||
let fromCharacter = 1;
|
||||
|
||||
// if this is an insertion at the very end of the document,
|
||||
// we must start the next range after the last character of the previous line,
|
||||
// in order to take the correct eol
|
||||
if (isInsertion && originalStartLineNumber === original.getLineCount()) {
|
||||
fromLine--;
|
||||
fromCharacter = modified.getLineMaxColumn(fromLine);
|
||||
}
|
||||
|
||||
result.push(modified.getValueInRange(new monaco.Range(fromLine, fromCharacter, modifiedEndLineNumber + 1, 1)));
|
||||
}
|
||||
|
||||
currentLine = isInsertion ? originalStartLineNumber + 1 : originalEndLineNumber + 1;
|
||||
}
|
||||
|
||||
result.push(original.getValueInRange(new monaco.Range(currentLine, 1, original.getLineCount() + 1, 1)));
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
class DirtyDiffPeekView extends MonacoEditorPeekViewWidget {
|
||||
|
||||
private diffEditor?: MonacoDiffEditor;
|
||||
private height?: number;
|
||||
|
||||
constructor(readonly widget: DirtyDiffWidget) {
|
||||
super(widget.editor, { isResizeable: true, showArrow: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' });
|
||||
}
|
||||
|
||||
override async create(): Promise<MonacoDiffEditor> {
|
||||
try {
|
||||
this.bodyElement = document.createElement('div');
|
||||
this.bodyElement.classList.add('body');
|
||||
const diffEditor = await this.widget.editorProvider.createEmbeddedDiffEditor(this.editor, this.bodyElement, this.widget.previousRevisionUri);
|
||||
this.diffEditor = diffEditor;
|
||||
this.toDispose.push(diffEditor);
|
||||
super.create();
|
||||
return new Promise(resolve => {
|
||||
// The diff computation is asynchronous and may complete before or after we register the listener.
|
||||
// This can happen when the file is already open in another editor, causing the model to be cached
|
||||
// and the diff to compute almost instantly. To handle this race condition, we check if the diff
|
||||
// is already available before waiting for onDidUpdateDiff.
|
||||
// setTimeout is needed because the non-side-by-side diff editor might still not have created the view zones;
|
||||
// otherwise, the first change shown might not be properly revealed in the diff editor.
|
||||
// See also https://github.com/microsoft/vscode/blob/b30900b56c4b3ca6c65d7ab92032651f4cb23f15/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts#L248
|
||||
if (diffEditor.diffEditor.getLineChanges()) {
|
||||
setTimeout(() => resolve(diffEditor));
|
||||
return;
|
||||
}
|
||||
const disposable = diffEditor.diffEditor.onDidUpdateDiff(() => setTimeout(() => {
|
||||
resolve(diffEditor);
|
||||
disposable.dispose();
|
||||
}));
|
||||
});
|
||||
} catch (e) {
|
||||
this.dispose();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
override show(rangeOrPos: Range | Position, heightInLines: number): void {
|
||||
const borderColor = this.getBorderColor();
|
||||
this.style({
|
||||
arrowColor: borderColor,
|
||||
frameColor: borderColor,
|
||||
headerBackgroundColor: peekViewTitleBackground,
|
||||
primaryHeadingColor: peekViewTitleForeground,
|
||||
secondaryHeadingColor: peekViewTitleInfoForeground
|
||||
});
|
||||
this.updateActions();
|
||||
super.show(rangeOrPos, heightInLines);
|
||||
}
|
||||
|
||||
private getBorderColor(): string {
|
||||
const { currentChange } = this.widget;
|
||||
if (!currentChange) {
|
||||
return peekViewBorder;
|
||||
}
|
||||
if (Change.isAddition(currentChange)) {
|
||||
return ScmColors.editorGutterAddedBackground;
|
||||
} else if (Change.isRemoval(currentChange)) {
|
||||
return ScmColors.editorGutterDeletedBackground;
|
||||
} else {
|
||||
return ScmColors.editorGutterModifiedBackground;
|
||||
}
|
||||
}
|
||||
|
||||
private updateActions(): void {
|
||||
this.clearActions();
|
||||
const { contextKeyService, menuModelRegistry } = this.widget;
|
||||
contextKeyService.with({ originalResourceScheme: this.widget.previousRevisionUri.scheme }, () => {
|
||||
for (const menuPath of [SCM_CHANGE_TITLE_MENU, PLUGIN_SCM_CHANGE_TITLE_MENU]) {
|
||||
const menu = menuModelRegistry.getMenu(menuPath);
|
||||
if (menu) {
|
||||
for (const item of menu.children) {
|
||||
if (CommandMenu.is(item)) {
|
||||
const { id, label, icon } = item;
|
||||
const itemPath = [...menuPath, id];
|
||||
if (icon && item.isVisible(itemPath, contextKeyService, undefined, this.widget)) {
|
||||
// Close editor on successful contributed action.
|
||||
// https://github.com/microsoft/vscode/blob/1.99.3/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts#L357-L361
|
||||
this.addAction(id, label, icon, item.isEnabled(itemPath, this.widget), () => {
|
||||
item.run(itemPath, this.widget).then(() => this.dispose());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.addAction('dirtydiff.next', nls.localizeByDefault('Show Next Change'), codicon('arrow-down'), true,
|
||||
() => this.widget.showNextChange());
|
||||
this.addAction('dirtydiff.previous', nls.localizeByDefault('Show Previous Change'), codicon('arrow-up'), true,
|
||||
() => this.widget.showPreviousChange());
|
||||
this.addAction('peekview.close', nls.localizeByDefault('Close'), codicon('close'), true,
|
||||
() => this.dispose());
|
||||
}
|
||||
|
||||
protected override fillContainer(container: HTMLElement): void {
|
||||
this.setCssClass('peekview-widget');
|
||||
|
||||
this.headElement = document.createElement('div');
|
||||
this.headElement.classList.add('head');
|
||||
|
||||
container.appendChild(this.headElement);
|
||||
container.appendChild(this.bodyElement!);
|
||||
|
||||
this.fillHead(this.headElement);
|
||||
}
|
||||
|
||||
protected override fillHead(container: HTMLElement): void {
|
||||
super.fillHead(container, true);
|
||||
}
|
||||
|
||||
protected override doLayoutBody(height: number, width: number): void {
|
||||
super.doLayoutBody(height, width);
|
||||
this.layout(height, width);
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
protected override onWidth(width: number): void {
|
||||
super.onWidth(width);
|
||||
const { height } = this;
|
||||
if (height !== undefined) {
|
||||
this.layout(height, width);
|
||||
}
|
||||
}
|
||||
|
||||
private layout(height: number, width: number): void {
|
||||
this.diffEditor?.diffEditor.layout({ height, width });
|
||||
}
|
||||
|
||||
protected override doRevealRange(range: Range): void {
|
||||
this.editor.revealPosition(Position.create(range.end.line, 0), { vertical: 'centerIfOutsideViewport' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user