From 3b9b0fd204444b9c0868c7da7b55aa3c53fbc3d3 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 16 Feb 2022 14:26:15 +0100 Subject: [PATCH] VSC: Run rewrap on saving (#165) --- vscode/package.json | 6 ++++++ vscode/src/AutoWrap.ts | 4 ++-- vscode/src/Common.ts | 35 ++++++++++++++++++++--------------- vscode/src/Extension.ts | 29 +++++++++++++++++++++++------ vscode/src/Settings.ts | 5 +++++ 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index dbafa85..56302f9 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -91,6 +91,12 @@ "default": false, "description": "(EXPERIMENTAL) When wrapping lines, reformat paragraph indents." }, + "rewrap.onSave": { + "scope": "language-overridable", + "type": "boolean", + "default": false, + "description": "Do a rewrap on the whole document when saving." + }, "rewrap.autoWrap.enabled": { "scope": "language-overridable", "type": "boolean", diff --git a/vscode/src/AutoWrap.ts b/vscode/src/AutoWrap.ts index 9ffe302..d5171e4 100644 --- a/vscode/src/AutoWrap.ts +++ b/vscode/src/AutoWrap.ts @@ -1,6 +1,6 @@ import {getWrappingColumn, maybeAutoWrap} from './Core' import {Memento, ThemeColor, workspace, window, TextDocumentChangeEvent, TextEditor, ConfigurationChangeEvent} from 'vscode' -import {buildEdit, catchErr, docLine, docType} from './Common' +import {buildEdits, catchErr, docLine, docType} from './Common' import {EditorSettings, getCoreSettings, getEditorSettings} from './Settings' /** Handler that's called if the text in the active editor changes */ @@ -30,7 +30,7 @@ const checkChange = async (e: TextDocumentChangeEvent) => { // maybeAutoWrap does more checks: that newText isn't empty, but is only whitespace. // Don't call this in a promise: it causes timing issues. const edit = maybeAutoWrap(file, settings, newText, range.start, docLine(doc)) - if (!edit.isEmpty) return editor.edit (builder => buildEdit(editor, builder, edit, false)) + if (!edit.isEmpty) return editor.edit (builder => buildEdits(doc, edit, builder)) } catch (err) { catchErr(err) } } diff --git a/vscode/src/Common.ts b/vscode/src/Common.ts index adf07a2..aa2298a 100644 --- a/vscode/src/Common.ts +++ b/vscode/src/Common.ts @@ -1,5 +1,5 @@ import {DocState, DocType, Edit, saveDocState} from './Core' -import vscode, {Position, Range, Selection, TextDocument, TextEditor, TextEditorEdit} from 'vscode' +import vscode, {Position, Range, TextDocument, TextEdit, TextEditor} from 'vscode' import fd from 'fast-diff' import GetCustomMarkers from './CustomLanguage' const getCustomMarkers = GetCustomMarkers() @@ -14,19 +14,20 @@ export const getDocState = (editor: TextEditor) : DocState => { return {filePath: docType(doc).path, version: doc.version, selections} } -/** Builds the vscode edits that apply an Edit to the document. Also calculates where the - * post-wrap selections will be and saves the state of the document. If the edit is empty - * this is a no-op */ -export function buildEdit - (editor: TextEditor, editBuilder: TextEditorEdit, edit: Edit, saveState: boolean) : void +/** Creates and returns the vscode edits to be applied to the the document. If a + * TextEditorEdit is supplied, the edits are applied to it. If saveState is true, also + * calculates where the post-wrap selections will be and saves the state of the document. + * If the edit is empty this is a no-op */ +export function buildEdits + (doc: TextDocument, edit: Edit, eb?: vscode.TextEditorEdit, saveState = false) : TextEdit[] { - if (edit.isEmpty) return + const edits: TextEdit[] = [] + if (edit.isEmpty) return edits - const doc = editor.document - , oldLines = Array(edit.endLine - edit.startLine + 1).fill(null) - .map((_, i) => doc.lineAt(edit.startLine + i).text) + const oldLines = Array(edit.endLine - edit.startLine + 1).fill(null) + .map((_, i) => doc.lineAt(edit.startLine + i).text) , oldSelections = [...edit.selections].reverse() - , selections: Selection[] = [] + , selections: vscode.Selection[] = [] let sel = oldSelections.pop() const eol = doc.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n" @@ -51,7 +52,7 @@ export function buildEdit for (let [op, str] of diffs) { if (op === fd.INSERT) { offsetDiff += str.length - editBuilder.insert (startPos, str) + edits.push (TextEdit.insert (startPos, str)) continue } @@ -63,7 +64,7 @@ export function buildEdit if (!newAnchorPos) newAnchorPos = checkSelPos(sel.anchor, newOffset) if (!newActivePos) newActivePos = checkSelPos(sel.active, newOffset) if (newAnchorPos && newActivePos) { - selections.push (new Selection (newAnchorPos, newActivePos)) + selections.push (new vscode.Selection (newAnchorPos, newActivePos)) newAnchorPos = newActivePos = undefined sel = oldSelections.pop () } @@ -72,7 +73,7 @@ export function buildEdit if (op === fd.DELETE) { offsetDiff -= str.length - editBuilder.delete (new Range (startPos, endPos)) + edits.push (TextEdit.delete (new Range (startPos, endPos))) } startPos = endPos } @@ -90,7 +91,7 @@ export function buildEdit // Finish off selections after the edit const lineDelta = edit.lines.length - (edit.endLine - edit.startLine + 1) while (sel) { - selections.push (new Selection ( + selections.push (new vscode.Selection ( newAnchorPos || sel.anchor.translate (lineDelta), newActivePos || sel.active.translate (lineDelta) )) @@ -98,8 +99,12 @@ export function buildEdit sel = oldSelections.pop () } + if (eb) + edits.forEach(e => e.newText ? eb.insert (e.range.start, e.newText) : eb.delete (e.range)) if (saveState) saveDocState ({filePath: doc.fileName, version: doc.version + 1, selections}) + + return edits } /** Catches any error and displays a friendly message to the user. */ diff --git a/vscode/src/Extension.ts b/vscode/src/Extension.ts index e084e94..21fbb8a 100644 --- a/vscode/src/Extension.ts +++ b/vscode/src/Extension.ts @@ -1,13 +1,13 @@ -import {maybeChangeWrappingColumn, rewrap} from './Core' -import {buildEdit, catchErr, docType, docLine, getDocState} from './Common' -import {ExtensionContext, TextEditor, TextEditorEdit, commands, window} from 'vscode' -import {getCoreSettings, getEditorSettings} from './Settings' +import {getWrappingColumn, maybeChangeWrappingColumn, rewrap} from './Core' +import {buildEdits, catchErr, docType, docLine, getDocState} from './Common' +import vscode, {TextEditor, TextEditorEdit, commands, window, workspace} from 'vscode' +import {getCoreSettings, getEditorSettings, getOnSaveSetting} from './Settings' import AutoWrap from './AutoWrap' export {activate, getCoreSettings, getEditorSettings} /** Function to activate the extension. */ -async function activate (context: ExtensionContext) { +async function activate (context: vscode.ExtensionContext) { const autoWrap = AutoWrap(context.workspaceState, context.subscriptions) // Register the commands @@ -15,6 +15,7 @@ async function activate (context: ExtensionContext) { ( commands.registerTextEditorCommand('rewrap.rewrapComment', rewrapCommentCommand) , commands.registerTextEditorCommand('rewrap.rewrapCommentAt', rewrapCommentAtCommand) , commands.registerTextEditorCommand('rewrap.toggleAutoWrap', autoWrap.editorToggle) + , workspace.onWillSaveTextDocument(onSaveDocument) ) } @@ -44,6 +45,22 @@ async function rewrapCommentAtCommand (editor: TextEditor, editBuilder: TextEdit } +function onSaveDocument (e: vscode.TextDocumentWillSaveEvent) { + if (e.reason !== vscode.TextDocumentSaveReason.Manual) return + if (! getOnSaveSetting (e.document)) return + // We need an editor for the tab size. So for now we have to look for it. + const editor = window.visibleTextEditors.find(ed => ed.document === e.document) + if (!editor) return + + const file = docType (e.document) + , settings = getCoreSettings (editor, cs => getWrappingColumn(file.path, cs)) + , edit = rewrap (file, settings, [], docLine(e.document)) + , edits = buildEdits (e.document, edit) + + e.waitUntil(Promise.resolve(edits)) +} + + /** Collects the information for a wrap from the editor, passes it to the wrapping code, * and then applies the result to the document. If an edit is applied, returns an updated * DocState object, else returns null. Takes an optional customColumn to wrap at. @@ -58,7 +75,7 @@ const doWrap = (editor: TextEditor, editBuilder: TextEditorEdit, customColumn?) const selections = editor.selections const edit = rewrap(docType(doc), settings, selections, docLine(doc)) - buildEdit (editor, editBuilder, edit, isNaN(customColumn)) + buildEdits (doc, edit, editBuilder, isNaN(customColumn)) } catch (err) { catchErr(err) } } diff --git a/vscode/src/Settings.ts b/vscode/src/Settings.ts index 07cf186..2c43dc9 100644 --- a/vscode/src/Settings.ts +++ b/vscode/src/Settings.ts @@ -47,6 +47,11 @@ export function getEditorSettings (editor: TextEditor) : EditorSettings { } } +/** Gets the onSave setting for the wrap-on-save feature */ +export function getOnSaveSetting (document: TextDocument) : boolean { + return workspace.getConfiguration('', document).get('rewrap.onSave', false) +} + const getAutoWrapSettings = (config: WorkspaceConfiguration, lang: string) : AutoWrapSettings => ({