Skip to content

Commit

Permalink
Merge pull request #2692 from pbwolf/2611-paredit-D+2691
Browse files Browse the repository at this point in the history
For #2691, stabilize deleteForward by reducing asyncness

* Fixes #2611 
* Fixes #2691
  • Loading branch information
PEZ authored Jan 8, 2025
2 parents b8bcccd + da7d69e commit 5a0707c
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 240 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changes to Calva.

## [Unreleased]

- Fix: [Paredit garbles while backspacing rapidly](https://github.com/BetterThanTomorrow/calva/issues/2611)
- Fix: [Paredit garbles when deleteForward is repeated rapidly](https://github.com/BetterThanTomorrow/calva/issues/2691)
- Fix: [Del key, after emptying a comment line, then imbalances the next form](https://github.com/BetterThanTomorrow/calva/issues/2686)

## [2.0.482] - 2024-12-03
Expand Down
101 changes: 77 additions & 24 deletions src/calva-fmt/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export async function indentPosition(position: vscode.Position, document: vscode
}
}

export async function formatRangeEdits(
export function formatRangeEdits(
document: vscode.TextDocument,
originalRange: vscode.Range
): Promise<vscode.TextEdit[] | undefined> {
): vscode.TextEdit[] | undefined {
const mirrorDoc = getDocument(document);
const startIndex = document.offsetAt(originalRange.start);
const cursor = mirrorDoc.getTokenCursor(startIndex);
Expand All @@ -63,7 +63,7 @@ export async function formatRangeEdits(
const trailingWs = originalText.match(/\s*$/)[0];
const missingTexts = cursorDocUtils.getMissingBrackets(originalText);
const healedText = `${missingTexts.prepend}${originalText.trim()}${missingTexts.append}`;
const formattedHealedText = await formatCode(healedText, document.eol);
const formattedHealedText = formatCode(healedText, document.eol);
const leadingEolPos = leadingWs.lastIndexOf(eol);
const startIndent =
leadingEolPos === -1
Expand All @@ -86,7 +86,7 @@ export async function formatRangeEdits(

export async function formatRange(document: vscode.TextDocument, range: vscode.Range) {
const wsEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit();
const edits = await formatRangeEdits(document, range);
const edits = formatRangeEdits(document, range);

if (isUndefined(edits)) {
console.error('formatRangeEdits returned undefined!', cloneDeep({ document, range }));
Expand All @@ -97,14 +97,22 @@ export async function formatRange(document: vscode.TextDocument, range: vscode.R
return vscode.workspace.applyEdit(wsEdit);
}

export async function formatPositionInfo(
export function formatPositionInfo(
editor: vscode.TextEditor,
onType: boolean = false,
extraConfig: CljFmtConfig = {}
) {
const doc: vscode.TextDocument = editor.document;
const index = doc.offsetAt(editor.selections[0].active);
const cursor = getDocument(doc).getTokenCursor(index);
const mDoc = getDocument(doc);

if (mDoc.model.documentVersion != doc.version) {
console.warn(
'Model for formatPositionInfo is out of sync with document; will not reformat now'
);
return;
}
const cursor = mDoc.getTokenCursor(index);

const formatRange = _calculateFormatRange(extraConfig, cursor, index);
if (!formatRange) {
Expand All @@ -122,7 +130,7 @@ export async function formatPositionInfo(
_convertEolNumToStringNotation(doc.eol),
onType,
{
...(await config.getConfig()),
...config.getConfigNow(),
...extraConfig,
'comment-form?': cursor.getFunctionName() === 'comment',
}
Expand Down Expand Up @@ -206,9 +214,13 @@ export async function formatPosition(
onType: boolean = false,
extraConfig: CljFmtConfig = {}
): Promise<boolean> {
// Stop trying if ever the document version changes - don't want to trample User's work
const doc: vscode.TextDocument = editor.document,
formattedInfo = await formatPositionInfo(editor, onType, extraConfig);
if (formattedInfo && formattedInfo.previousText != formattedInfo.formattedText) {
documentVersion = editor.document.version,
formattedInfo = formatPositionInfo(editor, onType, extraConfig);
if (documentVersion != editor.document.version) {
return;
} else if (formattedInfo && formattedInfo.previousText != formattedInfo.formattedText) {
return editor
.edit(
(textEditorEdit) => {
Expand All @@ -217,16 +229,19 @@ export async function formatPosition(
{ undoStopAfter: false, undoStopBefore: false }
)
.then((onFulfilled: boolean) => {
editor.selections = [
new vscode.Selection(
doc.positionAt(formattedInfo.newIndex),
doc.positionAt(formattedInfo.newIndex)
),
];
if (onFulfilled) {
if (documentVersion + 1 == editor.document.version) {
editor.selections = [
new vscode.Selection(
doc.positionAt(formattedInfo.newIndex),
doc.positionAt(formattedInfo.newIndex)
),
];
}
}
return onFulfilled;
});
}
if (formattedInfo) {
} else if (formattedInfo) {
return new Promise((resolve, _reject) => {
if (formattedInfo.newIndex != formattedInfo.previousIndex) {
editor.selections = [
Expand All @@ -238,16 +253,54 @@ export async function formatPosition(
}
resolve(true);
});
}
if (!onType && !outputWindow.isResultsDoc(doc)) {
} else if (!onType && !outputWindow.isResultsDoc(doc)) {
return formatRange(
doc,
new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length))
);
} else {
return new Promise((resolve, _reject) => {
resolve(true);
});
}
}

// Debounce format-as-you-type and toss it aside if User seems still to be working
let scheduledFormatCircumstances = undefined;
const scheduledFormatDelayMs = 250;

function formatPositionCallback(extraConfig: CljFmtConfig) {
if (
scheduledFormatCircumstances &&
vscode.window.activeTextEditor === scheduledFormatCircumstances['editor'] &&
vscode.window.activeTextEditor.document.version ==
scheduledFormatCircumstances['documentVersion']
) {
formatPosition(scheduledFormatCircumstances['editor'], true, extraConfig).finally(() => {
scheduledFormatCircumstances = undefined;
});
}
// do not anull scheduledFormatCircumstances. Another callback might have been scheduled
}

export function scheduleFormatAsType(editor: vscode.TextEditor, extraConfig: CljFmtConfig = {}) {
const expectedDocumentVersionUponCallback = 1 + editor.document.version;
if (
!scheduledFormatCircumstances ||
expectedDocumentVersionUponCallback != scheduledFormatCircumstances['documentVersion']
) {
// Unschedule (if scheduled) & reschedule: best effort to reformat at a quiet time
if (scheduledFormatCircumstances?.timeoutId) {
clearTimeout(scheduledFormatCircumstances?.timeoutId);
}
scheduledFormatCircumstances = {
editor: editor,
documentVersion: expectedDocumentVersionUponCallback,
timeoutId: setTimeout(function () {
formatPositionCallback(extraConfig);
}, scheduledFormatDelayMs),
};
}
return new Promise((resolve, _reject) => {
resolve(true);
});
}

export function formatPositionCommand(editor: vscode.TextEditor) {
Expand All @@ -262,11 +315,11 @@ export function trimWhiteSpacePositionCommand(editor: vscode.TextEditor) {
void formatPosition(editor, false, { 'remove-multiple-non-indenting-spaces?': true });
}

export async function formatCode(code: string, eol: number) {
export function formatCode(code: string, eol: number) {
const d = {
'range-text': code,
eol: _convertEolNumToStringNotation(eol),
config: await config.getConfig(),
config: config.getConfigNow(),
};
const result = jsify(formatText(d));
if (!result['error']) {
Expand Down
121 changes: 72 additions & 49 deletions src/cursor-doc/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Scanner, Token, ScannerState } from './clojure-lexer';
import { LispTokenCursor } from './token-cursor';
import { deepEqual as equal } from '../util/object';
import { isNumber, isUndefined } from 'lodash';
import { TextDocument, Selection } from 'vscode';
import { TextDocument, Selection, TextEditorEdit } from 'vscode';
import _ = require('lodash');

let scanner: Scanner;
Expand Down Expand Up @@ -244,6 +244,7 @@ export type ModelEditOptions = {
formatDepth?: number;
skipFormat?: boolean;
selections?: ModelEditSelection[];
builder?: TextEditorEdit;
};

export interface EditableModel {
Expand All @@ -257,6 +258,15 @@ export interface EditableModel {
*/
edit: (edits: ModelEdit<ModelEditFunction>[], options: ModelEditOptions) => Thenable<boolean>;

/**
* Performs a model edit batch "synchronously",
* using the TextEditorEdit at the 'builder' key of options if applicable.
* For some EditableModel's these are performed as one atomic set of edits.
* @param edits What to do
* @param options The TextEditorEdit (at the 'builder' key, if applicable) and other options
*/
editNow: (edits: ModelEdit<ModelEditFunction>[], options: ModelEditOptions) => void;

getText: (start: number, end: number, mustBeWithin?: boolean) => string;
getLineText: (line: number) => string;
getOffsetForLine: (line: number) => number;
Expand All @@ -275,8 +285,6 @@ export interface EditableDocument {
getTokenCursor: (offset?: number, previous?: boolean) => LispTokenCursor;
insertString: (text: string) => void;
getSelectionText: () => string;
delete: () => Thenable<boolean>;
backspace: () => Thenable<boolean>;
}

/** The underlying model for the REPL readline. */
Expand Down Expand Up @@ -527,34 +535,62 @@ export class LineInputModel implements EditableModel {
*/
edit(edits: ModelEdit<ModelEditFunction>[], options: ModelEditOptions): Thenable<boolean> {
return new Promise((resolve, reject) => {
for (const edit of edits) {
switch (edit.editFn) {
case 'insertString': {
const fn = this.insertString;
this.insertString(...(edit.args.slice(0, 4) as Parameters<typeof fn>));
break;
}
case 'changeRange': {
const fn = this.changeRange;
this.changeRange(...(edit.args.slice(0, 5) as Parameters<typeof fn>));
break;
}
case 'deleteRange': {
const fn = this.deleteRange;
this.deleteRange(...(edit.args.slice(0, 5) as Parameters<typeof fn>));
break;
}
default:
break;
}
}
this.editTextNow(edits, options);
if (this.document && options.selections) {
this.document.selections = options.selections;
}
resolve(true);
});
}

editNow(edits: ModelEdit<ModelEditFunction>[], options: ModelEditOptions): void {
const ultimateSelections = this.editTextNow(edits, options);
if (this.document && options.selections) {
this.document.selections = options.selections;
} else {
// Mimic TextEditorEdit, which leaves the selection at the end of the insertion or start of deletion:
if (this.document && ultimateSelections) {
this.document.selections = ultimateSelections;
}
}
}

// Returns the selection that would mimic TextEditorEdit
editTextNow(
edits: ModelEdit<ModelEditFunction>[],
options: ModelEditOptions
): ModelEditSelection[] {
let ultimateSelections = undefined;
for (const edit of edits) {
switch (edit.editFn) {
case 'insertString': {
const fn = this.insertString;
ultimateSelections = this.insertString(
...(edit.args.slice(0, 4) as Parameters<typeof fn>)
);
break;
}
case 'changeRange': {
const fn = this.changeRange;
ultimateSelections = this.changeRange(
...(edit.args.slice(0, 5) as Parameters<typeof fn>)
);
break;
}
case 'deleteRange': {
const fn = this.deleteRange;
ultimateSelections = this.deleteRange(
...(edit.args.slice(0, 5) as Parameters<typeof fn>)
);
break;
}
default:
break;
}
}
return ultimateSelections;
}

/**
* Changes the model. Deletes any text between `start` and `end`, and the inserts `text`.
*
Expand All @@ -572,7 +608,7 @@ export class LineInputModel implements EditableModel {
text: string,
oldSelection?: ModelEditRange,
newSelection?: ModelEditRange
) {
): ModelEditSelection[] {
const t1 = new Date();

const startPos = Math.min(start, end);
Expand Down Expand Up @@ -626,6 +662,9 @@ export class LineInputModel implements EditableModel {
}

// console.log("Parsing took: ", new Date().valueOf() - t1.valueOf());

// To mimic TextEditorEdit: No change to selection by default:
return undefined;
}

/**
Expand All @@ -643,9 +682,10 @@ export class LineInputModel implements EditableModel {
text: string,
oldSelection?: ModelEditRange,
newSelection?: ModelEditRange
): number {
this.changeRange(offset, offset, text, oldSelection, newSelection);
return text.length;
): ModelEditSelection[] {
this.changeRange(offset, offset, text);
// To mimic TextEditorEdit: selection moves to end of insertion, by default
return [new ModelEditSelection(offset + text.length)];
}

/**
Expand All @@ -662,8 +702,10 @@ export class LineInputModel implements EditableModel {
count: number,
oldSelection?: ModelEditRange,
newSelection?: ModelEditRange
) {
this.changeRange(offset, offset + count, '', oldSelection, newSelection);
): ModelEditSelection[] {
this.changeRange(offset, offset + count, '');
// To mimic TextEditorEdit: selection moves to start of deletion, by default
return [new ModelEditSelection(offset)];
}

/** Return the offset of the last character in this model. */
Expand Down Expand Up @@ -755,23 +797,4 @@ export class StringDocument implements EditableDocument {
}

getSelectionText: () => string;

delete() {
const p = this.selections[0].anchor;
return this.model.edit([new ModelEdit('deleteRange', [p, 1])], {
selections: [new ModelEditSelection(p)],
});
}

backspace() {
const anchor = this.selections[0].anchor;
const active = this.selections[0].active;
const [left, right] =
anchor == active
? [Math.max(0, anchor - 1), anchor]
: [Math.min(anchor, active), Math.max(anchor, active)];
return this.model.edit([new ModelEdit('deleteRange', [left, right - left])], {
selections: [new ModelEditSelection(left)],
});
}
}
Loading

0 comments on commit 5a0707c

Please sign in to comment.