Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP/POC] add diff completion item #5764

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions demo/kitchen-sink/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,34 @@ env.editor.showCommandLine = function(val) {
this.cmdLine.setValue(val, 1);
};

env.editor.completers = [
{
getCompletions: function (editor, session, pos, prefix, callback) {
var completions = [ {
caption: "diff item",
diff: [
{
replaceRange: {
start: {row: 15, column: 0},
end: {row: 18, column: 10},
},
replaceContent: "hey"
},
{
replaceRange: {
start: {row: 29, column: 0},
end: {row: 36, column: 10},
},
replaceContent: "there\nhave\na\nnice\nday"
}
]
}
];
callback(null, completions);
}
}
];

/**
* This demonstrates how you can define commands and bind shortcuts to them.
*/
Expand Down
23 changes: 21 additions & 2 deletions src/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* @property {string} [value] - The text that would be inserted when selecting this completion.
* @property {import("../ace-internal").Ace.Completer} [completer]
* @property {boolean} [hideInlinePreview]
* @property {[{replaceRange: import("../ace-internal").Ace.IRange, replaceContent: string}]} [diff]
* @export
*/

Expand All @@ -52,6 +53,12 @@
* @export
*/

/**
* @typedef {BaseCompletion & {diff: [{replaceRange: import("../ace-internal").Ace.IRange, replaceContent: string}]}} DiffCompletion
* @property {[{replaceRange: import("../ace-internal").Ace.IRange, replaceContent: string}]} diff
* @export
*/

/**
* Represents a suggested text snippet intended to complete a user's input
* @typedef Completion
Expand Down Expand Up @@ -165,6 +172,7 @@
if (this.inlineRenderer) {
this.inlineRenderer.hide();
}
this.editor.renderer.removeGhostDiff();
this.hideDocTooltip();
this.stickySelectionTimer.cancel();
this.popupTimer.cancel();
Expand All @@ -179,7 +187,11 @@
$onPopupChange(hide) {
if (this.inlineRenderer && this.inlineEnabled) {
var completion = hide ? null : this.popup.getData(this.popup.getRow());
this.$updateGhostText(completion);
if (completion.diff) {
this.editor.renderer.setGhostDiff(completion.diff)

Check failure on line 191 in src/autocomplete.js

View workflow job for this annotation

GitHub Actions / build (16.x)

Missing semicolon
} else {
this.$updateGhostText(completion);
}
// If the mouse is over the tooltip, and we're changing selection on hover don't
// move the tooltip while hovering over the popup.
if (this.popup.isMouseOver && this.setSelectOnHover) {
Expand Down Expand Up @@ -887,7 +899,14 @@
if (data.snippet) {
snippetManager.insertSnippet(editor, data.snippet);
}
else {
else if (data.diff) {
// We assume diffs are sorted, so we insert them in reverse order
// so that the insertion doesn't affect the other insertions.
data.diff.reverse().forEach(function(diff) {
editor.session.removeFullLines(diff.replaceRange.start.row, diff.replaceRange.end.row);
editor.session.doc.insertFullLines(diff.replaceRange.start.row, diff.replaceContent.split("\n"));
});
} else {
this.$insertString(editor, data);
}
if (data.completer && data.completer.onInsert && typeof data.completer.onInsert == "function") {
Expand Down
14 changes: 13 additions & 1 deletion src/css/editor-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,4 +701,16 @@ module.exports = `

.ace_hidden_token {
display: none;
}`;
}

.ace_diff_removed {
background-color: red;
opacity: 0.2;
position: absolute;
}

.ace_diff_added_container {
background-color: green;
}
`;

99 changes: 78 additions & 21 deletions src/virtual_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ var FontMetrics = require("./layer/font_metrics").FontMetrics;
var EventEmitter = require("./lib/event_emitter").EventEmitter;
var editorCss = require("./css/editor-css");
var Decorator = require("./layer/decorators").Decorator;
var Range = require("./range").Range;
var { MarkerGroup } = require("./marker_group");

var useragent = require("./lib/useragent");
const isTextToken = require("./layer/text_util").isTextToken;
Expand Down Expand Up @@ -1792,27 +1794,7 @@ class VirtualRenderer {
// If there are tokens to the right of the cursor, hide those.
var hiddenTokens = this.hideTokensAfterPosition(insertPosition.row, insertPosition.column);

var lastLineDiv;
textChunks.slice(1).forEach(el => {
var chunkDiv = dom.createElement("div");
var chunkSpan = dom.createElement("span");
chunkSpan.className = "ace_ghost_text";

// If the line is wider than the viewport, wrap the line
if (el.wrapped) chunkDiv.className = "ghost_text_line_wrapped";

// If a given line doesn't have text (e.g. it's a line of whitespace), set a space as the
// textcontent so that browsers render the empty line div.
if (el.text.length === 0) el.text = " ";

chunkSpan.appendChild(dom.createTextNode(el.text));
chunkDiv.appendChild(chunkSpan);
widgetDiv.appendChild(chunkDiv);

// Overwrite lastLineDiv every iteration so at the end it points to
// the last added element.
lastLineDiv = chunkDiv;
});
var lastLineDiv = this.createGhostTextWidget(textChunks.slice(1), widgetDiv)

// Add the hidden tokens to the last line of the ghost text.
hiddenTokens.forEach(token => {
Expand Down Expand Up @@ -1852,6 +1834,81 @@ class VirtualRenderer {

}

/**
* Calculates and organizes text into wrapped chunks. Initially splits the text by newline characters,
* then further processes each line based on display tokens and session settings for tab size and wrapping limits.
*
* @param {{text: string, wrapped: boolean}[]} textChunks
* @param {HTMLDivElement} widgetDiv
* @return {HTMLDivElement}
*/
createGhostTextWidget(textChunks, widgetDiv) {
var lastLineDiv;
textChunks.forEach(el => {
var chunkDiv = dom.createElement("div");
var chunkSpan = dom.createElement("span");
chunkSpan.className = "ace_ghost_text";

// If the line is wider than the viewport, wrap the line
if (el.wrapped) chunkDiv.className = "ghost_text_line_wrapped";

// If a given line doesn't have text (e.g. it's a line of whitespace), set a space as the
// textcontent so that browsers render the empty line div.
if (el.text.length === 0) el.text = " ";

chunkSpan.appendChild(dom.createTextNode(el.text));
chunkDiv.appendChild(chunkSpan);
widgetDiv.appendChild(chunkDiv);

// Overwrite lastLineDiv every iteration so at the end it points to
// the last added element.
lastLineDiv = chunkDiv;
});
return lastLineDiv;
}

$ghostDiffWidgets = [];

/**
* @param {[{replaceRange: import("../ace-internal").Ace.IRange, replaceContent: string}]} diffs
*/
setGhostDiff(diffs) {
if (!this.diffRemovalsMarkerGroup || this.diffRemovalsMarkerGroup.session !== this.session) {
this.diffRemovalsMarkerGroup = new MarkerGroup(this.session, {markerType: "fullLine"});
}

var markers = [];

diffs.forEach((diff) => {
var range = Range.fromPoints(diff.replaceRange.start, diff.replaceRange.end);
markers.push({range, className: "ace_diff_removed"})

var textChunks = this.$calculateWrappedTextChunks(diff.replaceContent, diff.replaceRange.start);

var widgetDiv = dom.createElement("div");
this.createGhostTextWidget(textChunks, widgetDiv)
let ghostDiffWidget = {
el: widgetDiv,
row: diff.replaceRange.end.row,
column: 0,
className: "ace_diff_added_container"
};
this.session.widgetManager.addLineWidget(ghostDiffWidget);
this.$ghostDiffWidgets.push(ghostDiffWidget)
})
this.diffRemovalsMarkerGroup.setMarkers(markers);
}

removeGhostDiff() {
if (this.diffRemovalsMarkerGroup) {
this.diffRemovalsMarkerGroup.setMarkers([]);
}
this.$ghostDiffWidgets.forEach((w) => {
this.session.widgetManager.removeLineWidget(w);
})
this.$ghostDiffWidgets = [];
}

/**
* Calculates and organizes text into wrapped chunks. Initially splits the text by newline characters,
* then further processes each line based on display tokens and session settings for tab size and wrapping limits.
Expand Down
15 changes: 15 additions & 0 deletions types/ace-modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3254,13 +3254,27 @@ declare module "ace-code/src/autocomplete" {
value?: string;
completer?: import("ace-code").Ace.Completer;
hideInlinePreview?: boolean;
diff?: [
{
replaceRange: import("ace-code").Ace.IRange;
replaceContent: string;
}
];
};
export type SnippetCompletion = BaseCompletion & {
snippet: string;
};
export type ValueCompletion = BaseCompletion & {
value: string;
};
export type DiffCompletion = BaseCompletion & {
diff: [
{
replaceRange: import("ace-code").Ace.IRange;
replaceContent: string;
}
];
};
/**
* Represents a suggested text snippet intended to complete a user's input
*/
Expand Down Expand Up @@ -3337,6 +3351,7 @@ declare module "ace-code/src/search_highlight" {
constructor(regExp: any, clazz: string, type?: string);
clazz: string;
type: string;
docLen: number;
setRegexp(regExp: any): void;
regExp: any;
cache: any[];
Expand Down
Loading