From 3e3d70e886dc25549166b9e2f10539a37ec5e2be Mon Sep 17 00:00:00 2001 From: Caleb Figgers Date: Fri, 1 Sep 2023 19:38:12 -0500 Subject: [PATCH] 0.0.1 Release - List handling features --- CHANGELOG.md | 8 +- LICENSE | 22 + README.md | 70 +-- icons/typst-small.png | Bin 0 -> 482 bytes icons/typst.png | Bin 0 -> 38326 bytes language-configuration.json | 46 ++ package.json | 137 ++++- src/IDisposable.ts | 39 ++ .../context-service-in-list.ts | 61 ++ .../i-context-service.ts | 74 +++ src/editor-context-service/manager.ts | 94 +++ src/extension.ts | 28 +- src/listEditing.ts | 567 ++++++++++++++++++ tsconfig.json | 25 +- vsc-extension-quickstart.md | 47 -- 15 files changed, 1072 insertions(+), 146 deletions(-) create mode 100644 LICENSE create mode 100644 icons/typst-small.png create mode 100644 icons/typst.png create mode 100644 language-configuration.json create mode 100644 src/IDisposable.ts create mode 100644 src/editor-context-service/context-service-in-list.ts create mode 100644 src/editor-context-service/i-context-service.ts create mode 100644 src/editor-context-service/manager.ts create mode 100644 src/listEditing.ts delete mode 100644 vsc-extension-quickstart.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c41a53..8f087ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,7 @@ # Change Log -All notable changes to the "typst-companion" extension will be documented in this file. +All notable changes to Typst Companion will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## 0.0.1 -## [Unreleased] - -- Initial release \ No newline at end of file +- Initial release of Typst Companion. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..486ff93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2023 Caleb Figgers + +With attributed inclusions Copyright (c) 2017 张宇 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 68b5dd6..69bec17 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,27 @@ -# typst-companion README +# Typst Companion -This is the README for your extension "typst-companion". After writing up a brief description, we recommend including the following sections. +A VS Code extension that adds Markdown-like editing niceties **on top of and in addition to** to Nathan Varner's [Typst LSP](https://github.com/nvarner/typst-lsp). ## Features -Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - -For example if there is an image subfolder under your extension project workspace: - -\!\[feature X\]\(images/feature-x.png\) - -> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. +- Intuitive handling of Ordered and Unordered lists in `.typ` files. + - `Enter` while in a list context (either ordered or unordered) continues the existing list at the current level of indentation (with correct numbering, if ordered). + - `Tab` and `Shift+Tab` while in a list context (either ordered or unordered) indents and out-dents bullets intuitively (and re-numbers ordered lists if appropriate). + - Reordering lines inside an ordered list automatically updates the list numbers accordingly. ## Requirements -If you have any requirements or dependencies, add a section describing those and how to install and configure them. - -## Extension Settings - -Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. - -For example: - -This extension contributes the following settings: - -* `myExtension.enable`: Enable/disable this extension. -* `myExtension.thing`: Set to `blah` to do something. - -## Known Issues - -Calling out known issues can help limit users opening duplicate issues against your extension. +I *strongly* encourage installing Nathan Varner's [Typst LSP](https://github.com/nvarner/typst-lsp) in addition to this extension for syntax highlighting, error reporting, code completion, and all of Typst LSP's other features. + This extension just adds some small additional features that I missed when using Typst LSP. ## Release Notes -Users appreciate release notes as you update your extension. - -### 1.0.0 - -Initial release of ... - -### 1.0.1 - -Fixed issue #. - -### 1.1.0 - -Added features X, Y, and Z. - ---- - -## Following extension guidelines - -Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. - -* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) - -## Working with Markdown - -You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: +### 0.0.1 -* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). -* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). -* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. +Initial release of Typst Companion. -## For more information +## Prior Art -* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) -* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) +The core logic of this extension is adapted, with attribution and gratitude, from [Markdown All-in-One](https://github.com/yzhang-gh/vscode-markdown/), under the following license: -**Enjoy!** +MIT License, Copyright (c) 2017 张宇 \ No newline at end of file diff --git a/icons/typst-small.png b/icons/typst-small.png new file mode 100644 index 0000000000000000000000000000000000000000..24edfbab1972aed13c2235c549c8391eb46e8119 GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaMW0(?STeKHG`=dOh#y+w=QY#`(R|Nk{d&uzH#5XgS@?T6X2Em{jVeE#*@ zW8LnegQxF*{54nJFiV3lqvV_JML@xMo-U3d7N_q{KFN1jfyYI?)kH@$x*_lX z|M{jnO_`Lx-q;yoJz2eA9i?ZFkcMfa1)ZWSp#+5UF)HCcpD)9Z9klH(-OT()| zB1(c1%M}WW^3yVNQWZ)n3sMy_3rdn17%JvG{=~yk7^b0d%K!8k&!<5Q%*xz)$=t%q z!rqfbn1vNw8cYtSFe`5kQ8<0$%84Uqj>sHgKi%N5z)O$emAGKZCnwXXKr0wLUHx3v IIVCg!0Ic@THUIzs literal 0 HcmV?d00001 diff --git a/icons/typst.png b/icons/typst.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e3ae9d09f80668b2cb35b1910c7bd05b009882 GIT binary patch literal 38326 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 W;f6o>Wr-F50000 + * @see + * @see + */ +export default interface IDisposable { + + /** + * Performs application-defined tasks associated with freeing, releasing, or resetting resources. + */ + dispose(): any; +} \ No newline at end of file diff --git a/src/editor-context-service/context-service-in-list.ts b/src/editor-context-service/context-service-in-list.ts new file mode 100644 index 0000000..834bf79 --- /dev/null +++ b/src/editor-context-service/context-service-in-list.ts @@ -0,0 +1,61 @@ +// From https://github.com/yzhang-gh/vscode-markdown/ , under the following license: +// +// MIT License + +// Copyright (c) 2017 张宇 + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +'use strict' + +import { ExtensionContext, Position, TextDocument, window } from 'vscode'; +import { AbsContextService } from "./i-context-service"; + +export class ContextServiceEditorInList extends AbsContextService { + public contextName: string = "typst-companion.extension.editor.cursor.inList"; + + public onActivate(_context: ExtensionContext) { + // set initial state of context + this.setState(false); + } + + public dispose(): void { } + + public onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position) { + this.updateContextState(document, cursorPos); + } + + public onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position) { + this.updateContextState(document, cursorPos); + } + + private updateContextState(document: TextDocument, cursorPos: Position) { + let lineText = document.lineAt(cursorPos.line).text; + + let inList = /^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] +)?/.test(lineText); + if (inList) { + this.setState(true); + } + else { + this.setState(false); + } + return; + } +} \ No newline at end of file diff --git a/src/editor-context-service/i-context-service.ts b/src/editor-context-service/i-context-service.ts new file mode 100644 index 0000000..f8bba83 --- /dev/null +++ b/src/editor-context-service/i-context-service.ts @@ -0,0 +1,74 @@ +// From https://github.com/yzhang-gh/vscode-markdown/ , under the following license: +// +// MIT License + +// Copyright (c) 2017 张宇 + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +'use strict' + +import { commands, ExtensionContext, Position, TextDocument } from 'vscode'; +import type IDisposable from "../IDisposable"; + +interface IContextService extends IDisposable { + onActivate(context: ExtensionContext): void; + + /** + * handler of onDidChangeActiveTextEditor + * implement this method to handle that event to update context state + */ + onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position): void; + /** + * handler of onDidChangeTextEditorSelection + * implement this method to handle that event to update context state + */ + onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position): void; +} + +export abstract class AbsContextService implements IContextService { + public abstract readonly contextName: string; + + /** + * activate context service + * @param context ExtensionContext + */ + public abstract onActivate(context: ExtensionContext): void; + public abstract dispose(): void; + + /** + * default handler of onDidChangeActiveTextEditor, do nothing. + * override this method to handle that event to update context state. + */ + public abstract onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position): void; + + /** + * default handler of onDidChangeTextEditorSelection, do nothing. + * override this method to handle that event to update context state. + */ + public abstract onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position): void; + + /** + * set state of context + */ + protected setState(state: any) { + commands.executeCommand('setContext', this.contextName, state); + } +} \ No newline at end of file diff --git a/src/editor-context-service/manager.ts b/src/editor-context-service/manager.ts new file mode 100644 index 0000000..f0fb229 --- /dev/null +++ b/src/editor-context-service/manager.ts @@ -0,0 +1,94 @@ +// From https://github.com/yzhang-gh/vscode-markdown/ , under the following license: +// +// MIT License + +// Copyright (c) 2017 张宇 + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +'use strict' + +import type IDisposable from "../IDisposable"; +import { ExtensionContext, window } from 'vscode'; +import { AbsContextService } from "./i-context-service"; +import { ContextServiceEditorInList } from "./context-service-in-list"; +// import { ContextServiceEditorInFencedCodeBlock } from "./context-service-in-fenced-code-block"; +// import { ContextServiceEditorInMathEn } from "./context-service-in-math-env"; + +export class ContextServiceManager implements IDisposable { + private readonly contextServices: Array = []; + + public constructor() { + // push context services + this.contextServices.push(new ContextServiceEditorInList()); + // this.contextServices.push(new ContextServiceEditorInFencedCodeBlock()); + // this.contextServices.push(new ContextServiceEditorInMathEn()); + } + + public activate(context: ExtensionContext) { + for (const service of this.contextServices) { + service.onActivate(context); + } + // subscribe update handler for context + context.subscriptions.push( + window.onDidChangeActiveTextEditor(() => this.onDidChangeActiveTextEditor()), + window.onDidChangeTextEditorSelection(() => this.onDidChangeTextEditorSelection()) + ); + // initialize context state + this.onDidChangeActiveTextEditor(); + } + + public dispose(): void { + while (this.contextServices.length > 0) { + const service = this.contextServices.pop(); + service!.dispose(); + } + } + + private onDidChangeActiveTextEditor() { + const editor = window.activeTextEditor; + if (editor === undefined) { + return; + } + + const cursorPos = editor.selection.start; + const document = editor.document; + + for (const service of this.contextServices) { + service.onDidChangeActiveTextEditor(document, cursorPos); + } + } + + private onDidChangeTextEditorSelection() { + const editor = window.activeTextEditor; + if (editor === undefined) { + return; + } + + const cursorPos = editor.selection.start; + const document = editor.document; + + for (const service of this.contextServices) { + service.onDidChangeTextEditorSelection(document, cursorPos); + } + } +} + +export const contextServiceManager = new ContextServiceManager(); \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index f21f7b4..890e96f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,26 +1,18 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; +import { contextServiceManager } from "./editor-context-service/manager" +import * as listEditing from './listEditing'; -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push( + contextServiceManager + ); - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "typst-companion" is now active!'); + // Context services + contextServiceManager.activate(context); + + // Override `Enter`, `Tab` and `Backspace` keys + listEditing.activate(context); - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = vscode.commands.registerCommand('typst-companion.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from Typst Companion!'); - }); - - context.subscriptions.push(disposable); } -// This method is called when your extension is deactivated export function deactivate() {} diff --git a/src/listEditing.ts b/src/listEditing.ts new file mode 100644 index 0000000..d16b113 --- /dev/null +++ b/src/listEditing.ts @@ -0,0 +1,567 @@ +// From https://github.com/yzhang-gh/vscode-markdown/ , under the following license: +// +// MIT License + +// Copyright (c) 2017 张宇 + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + + +import { commands, ExtensionContext, Position, Range, Selection, TextEditor, window, workspace, WorkspaceEdit } from 'vscode'; +// import { isInFencedCodeBlock, mathEnvCheck } from "./util/contextCheck"; + +type IModifier = "ctrl" | "shift"; + +export function activate(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand('typst-companion.extension.onEnterKey', onEnterKey), + commands.registerCommand('typst-companion.extension.onCtrlEnterKey', () => { return onEnterKey('ctrl'); }), + commands.registerCommand('typst-companion.extension.onShiftEnterKey', () => { return onEnterKey('shift'); }), + commands.registerCommand('typst-companion.extension.onTabKey', onTabKey), + commands.registerCommand('typst-companion.extension.onShiftTabKey', () => { return onTabKey('shift'); }), + commands.registerCommand('typst-companion.extension.onBackspaceKey', onBackspaceKey), + commands.registerCommand('typst-companion.extension.checkTaskList', checkTaskList), + commands.registerCommand('typst-companion.extension.onMoveLineDown', onMoveLineDown), + commands.registerCommand('typst-companion.extension.onMoveLineUp', onMoveLineUp), + commands.registerCommand('typst-companion.extension.onCopyLineDown', onCopyLineDown), + commands.registerCommand('typst-companion.extension.onCopyLineUp', onCopyLineUp), + commands.registerCommand('typst-companion.extension.onIndentLines', onIndentLines), + commands.registerCommand('typst-companion.extension.onOutdentLines', onOutdentLines) + ); +} + +// The commands here are only bound to keys with `when` clause containing `editorTextFocus && !editorReadonly`. (package.json) +// So we don't need to check whether `activeTextEditor` returns `undefined` in most cases. + +function onEnterKey(modifiers?: IModifier) { + const editor = window.activeTextEditor!; + let cursorPos: Position = editor.selection.active; + let line = editor.document.lineAt(cursorPos.line); + let textBeforeCursor = line.text.substr(0, cursorPos.character); + let textAfterCursor = line.text.substr(cursorPos.character); + + let lineBreakPos = cursorPos; + if (modifiers == 'ctrl') { + lineBreakPos = line.range.end; + } + + if (modifiers == 'shift') { + return asNormal(editor, 'enter', modifiers); + } + + //// This is a possibility that the current line is a thematic break `
` (GitHub #785) + const lineTextNoSpace = line.text.replace(/\s/g, ''); + if (lineTextNoSpace.length > 2 + && ( + lineTextNoSpace.replace(/\-/g, '').length === 0 + || lineTextNoSpace.replace(/\*/g, '').length === 0 + ) + ) { + return asNormal(editor, 'enter', modifiers); + } + + //// If it's an empty list item, remove it + if (/^([-+*]|[0-9]+[.)])( +\[[ x]\])?$/.test(textBeforeCursor.trim()) && textAfterCursor.trim().length == 0) { + return editor.edit(editBuilder => { + editBuilder.delete(line.range); + editBuilder.insert(line.range.end, '\n'); + }).then(() => { + editor.revealRange(editor.selection); + }).then(() => fixMarker(editor)); + } + + let matches: RegExpExecArray | null; + if (/^> /.test(textBeforeCursor)) { + // Block quotes + + // Case 1: ending a blockquote if: + const isEmptyArrowLine = line.text.replace(/[ \t]+$/, '') === '>'; + if (isEmptyArrowLine) { + if (cursorPos.line === 0) { + // it is an empty '>' line and also the first line of the document + return editor.edit(editorBuilder => { + editorBuilder.replace(new Range(new Position(0, 0), new Position(cursorPos.line, cursorPos.character)), ''); + }).then(() => { editor.revealRange(editor.selection) }); + } else { + // there have been 2 consecutive empty `>` lines + const prevLineText = editor.document.lineAt(cursorPos.line - 1).text; + if (prevLineText.replace(/[ \t]+$/, '') === '>') { + return editor.edit(editorBuilder => { + editorBuilder.replace(new Range(new Position(cursorPos.line - 1, 0), new Position(cursorPos.line, cursorPos.character)), '\n'); + }).then(() => { editor.revealRange(editor.selection) }); + } + } + } + + // Case 2: `>` continuation + return editor.edit(editBuilder => { + if (isEmptyArrowLine) { + const startPos = new Position(cursorPos.line, line.text.trim().length); + editBuilder.delete(new Range(startPos, line.range.end)); + lineBreakPos = startPos; + } + editBuilder.insert(lineBreakPos, `\n> `); + }).then(() => { + // Fix cursor position + if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { + let newCursorPos = cursorPos.with(line.lineNumber + 1, 2); + editor.selection = new Selection(newCursorPos, newCursorPos); + } + }).then(() => { editor.revealRange(editor.selection) }); + } else if ((matches = /^((\s*[-+*] +)(\[[ x]\] +)?)/.exec(textBeforeCursor)) !== null) { + // satisfy compiler's null check + const match0 = matches[0]; + const match1 = matches[1]; + const match2 = matches[2]; + const match3 = matches[3]; + + // Unordered list + return editor.edit(editBuilder => { + if ( + match3 && // If it is a task list item and + match0 === textBeforeCursor && // the cursor is right after the checkbox "- [x] |item1" + modifiers !== 'ctrl' + ) { + // Move the task list item to the next line + // - [x] |item1 + // ↓ + // - [ ] + // - [x] |item1 + editBuilder.replace(new Range(cursorPos.line, match2.length + 1, cursorPos.line, match2.length + 2), " "); + editBuilder.insert(lineBreakPos, `\n${match1}`); + } else { + // Insert "- [ ]" + // - [ ] item1| + // ↓ + // - [ ] item1 + // - [ ] | + editBuilder.insert(lineBreakPos, `\n${match1.replace('[x]', '[ ]')}`); + } + }).then(() => { + // Fix cursor position + if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { + let newCursorPos = cursorPos.with(line.lineNumber + 1, matches![1].length); + editor.selection = new Selection(newCursorPos, newCursorPos); + } + }).then(() => { editor.revealRange(editor.selection) }); + } else if ((matches = /^(\s*)([0-9]+)([.)])( +)((\[[ x]\] +)?)/.exec(textBeforeCursor)) !== null) { + // Ordered list + let config = workspace.getConfiguration('typst-companion.extension.orderedList').get('marker'); + let marker = '1'; + let leadingSpace = matches[1]; + let previousMarker = matches[2]; + let delimiter = matches[3]; + let trailingSpace = matches[4]; + let gfmCheckbox = matches[5].replace('[x]', '[ ]'); + let textIndent = (previousMarker + delimiter + trailingSpace).length; + if (config == 'ordered') { + marker = String(Number(previousMarker) + 1); + } + // Add enough trailing spaces so that the text is aligned with the previous list item, but always keep at least one space + trailingSpace = " ".repeat(Math.max(1, textIndent - (marker + delimiter).length)); + + const toBeAdded = leadingSpace + marker + delimiter + trailingSpace + gfmCheckbox; + return editor.edit( + editBuilder => { + editBuilder.insert(lineBreakPos, `\n${toBeAdded}`); + }, + { undoStopBefore: true, undoStopAfter: false } + ).then(() => { + // Fix cursor position + if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { + let newCursorPos = cursorPos.with(line.lineNumber + 1, toBeAdded.length); + editor.selection = new Selection(newCursorPos, newCursorPos); + } + }).then(() => fixMarker(editor)).then(() => { editor.revealRange(editor.selection); }); + } else { + return asNormal(editor, 'enter', modifiers); + } +} + +function onTabKey(modifiers?: IModifier) { + const editor = window.activeTextEditor!; + let cursorPos = editor.selection.start; + let lineText = editor.document.lineAt(cursorPos.line).text; + + let match = /^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] +)?/.exec(lineText); + if ( + match + && ( + modifiers === 'shift' + || !editor.selection.isEmpty + || editor.selection.isEmpty && cursorPos.character <= match[0].length + ) + ) { + if (modifiers === 'shift') { + return outdent(editor).then(() => fixMarker(editor)); + } else { + return indent(editor).then(() => fixMarker(editor)); + } + } else { + return asNormal(editor, 'tab', modifiers); + } +} + +function onBackspaceKey() { + const editor = window.activeTextEditor!; + let cursor = editor.selection.active; + let document = editor.document; + let textBeforeCursor = document.lineAt(cursor.line).text.substr(0, cursor.character); + + if (!editor.selection.isEmpty) { + return asNormal(editor, 'backspace').then(() => fixMarker(editor)); + } else if (/^\s+([-+*]|[0-9]+[.)]) $/.test(textBeforeCursor)) { + // e.g. textBeforeCursor === ` - `, ` 1. ` + return outdent(editor).then(() => fixMarker(editor)); + } else if (/^([-+*]|[0-9]+[.)]) $/.test(textBeforeCursor)) { + // e.g. textBeforeCursor === `- `, `1. ` + return editor.edit(editBuilder => { + editBuilder.replace(new Range(cursor.with({ character: 0 }), cursor), ' '.repeat(textBeforeCursor.length)) + }).then(() => fixMarker(editor)); + } else if (/^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] )$/.test(textBeforeCursor)) { + // e.g. textBeforeCursor === `- [ ]`, `1. [x]`, ` - [x]` + return deleteRange(editor, new Range(cursor.with({ character: textBeforeCursor.length - 4 }), cursor)).then(() => fixMarker(editor)); + } else { + return asNormal(editor, 'backspace'); + } +} + +function asNormal(editor: TextEditor, key: "backspace" | "enter" | "tab", modifiers?: IModifier) { + switch (key) { + case 'enter': + if (modifiers === 'ctrl') { + return commands.executeCommand('editor.action.insertLineAfter'); + } else { + return commands.executeCommand('type', { source: 'keyboard', text: '\n' }); + } + case 'tab': + if (modifiers === 'shift') { + return commands.executeCommand('editor.action.outdentLines'); + } else if ( + editor.selection.isEmpty + && workspace.getConfiguration('emmet').get('triggerExpansionOnTab') + ) { + return commands.executeCommand('editor.emmet.action.expandAbbreviation'); + } else { + return commands.executeCommand('tab'); + } + case 'backspace': + return commands.executeCommand('deleteLeft'); + } +} + +/** + * If + * + * 1. it is not the first line + * 2. there is a Markdown list item before this line + * + * then indent the current line to align with the previous list item. + */ +function indent(editor: TextEditor) { + if (workspace.getConfiguration("typst-companion.extension.list", editor.document.uri).get("indentationSize") === "adaptive") { + try { + const selection = editor.selection; + const indentationSize = tryDetermineIndentationSize(editor, selection.start.line, editor.document.lineAt(selection.start.line).firstNonWhitespaceCharacterIndex); + let edit = new WorkspaceEdit() + for (let i = selection.start.line; i <= selection.end.line; i++) { + if (i === selection.end.line && !selection.isEmpty && selection.end.character === 0) { + break; + } + if (editor.document.lineAt(i).text.length !== 0) { + edit.insert(editor.document.uri, new Position(i, 0), ' '.repeat(indentationSize)); + } + } + return workspace.applyEdit(edit); + } catch (error) { } + } + + return commands.executeCommand('editor.action.indentLines'); +} + +/** + * Similar to `indent`-function + */ +function outdent(editor: TextEditor) { + if (workspace.getConfiguration("typst-companion.extension.list", editor.document.uri).get("indentationSize") === "adaptive") { + try { + const selection = editor.selection; + const indentationSize = tryDetermineIndentationSize(editor, selection.start.line, editor.document.lineAt(selection.start.line).firstNonWhitespaceCharacterIndex); + let edit = new WorkspaceEdit() + for (let i = selection.start.line; i <= selection.end.line; i++) { + if (i === selection.end.line && !selection.isEmpty && selection.end.character === 0) { + break; + } + const lineText = editor.document.lineAt(i).text; + let maxOutdentSize: number; + if (lineText.trim().length === 0) { + maxOutdentSize = lineText.length; + } else { + maxOutdentSize = editor.document.lineAt(i).firstNonWhitespaceCharacterIndex; + } + if (maxOutdentSize > 0) { + edit.delete(editor.document.uri, new Range(i, 0, i, Math.min(indentationSize, maxOutdentSize))); + } + } + return workspace.applyEdit(edit); + } catch (error) { } + } + + return commands.executeCommand('editor.action.outdentLines'); +} + +function tryDetermineIndentationSize(editor: TextEditor, line: number, currentIndentation: number) { + while (--line >= 0) { + const lineText = editor.document.lineAt(line).text; + let matches; + if ((matches = /^(\s*)(([-+*]|[0-9]+[.)]) +)(\[[ x]\] +)?/.exec(lineText)) !== null) { + if (matches[1].length <= currentIndentation) { + return matches[2].length; + } + } + } + throw "No previous Markdown list item"; +} + +/** + * Returns the line index of the next ordered list item starting from the specified line. + * + * @param line + * Defaults to the beginning of the current primary selection (`editor.selection.start.line`) + * in order to find the first marker following either the cursor or the entire selected range. + */ +function findNextMarkerLineNumber(editor: TextEditor, line = editor.selection.start.line): number { + while (line < editor.document.lineCount) { + const lineText = editor.document.lineAt(line).text; + + if (lineText.startsWith('#')) { + // Don't go searching past any headings + return -1; + } + + if (/^\s*[0-9]+[.)] +/.exec(lineText) !== null) { + return line; + } + line++; + } + return -1; +} + +/** + * Looks for the previous ordered list marker at the same indentation level + * and returns the marker number that should follow it. + * + * @param currentIndentation treat tabs as if they were replaced by spaces with a tab stop of 4 characters + * + * @returns the fixed marker number + */ +function lookUpwardForMarker(editor: TextEditor, line: number, currentIndentation: number): number { + let prevLine = line; + while (--prevLine >= 0) { + const prevLineText = editor.document.lineAt(prevLine).text.replace(/\t/g, ' '); + let matches; + if ((matches = /^(\s*)(([0-9]+)[.)] +)/.exec(prevLineText)) !== null) { + // The previous line has an ordered list marker + const prevLeadingSpace: string = matches[1]; + const prevMarker = matches[3]; + if (currentIndentation < prevLeadingSpace.length) { + // yet to find a sibling item + continue; + } else if ( + currentIndentation >= prevLeadingSpace.length + && currentIndentation <= (prevLeadingSpace + prevMarker).length + ) { + // found a sibling item + return Number(prevMarker) + 1; + } else if (currentIndentation > (prevLeadingSpace + prevMarker).length) { + // found a parent item + return 1; + } else { + // not possible + } + } else if ((matches = /^(\s*)([-+*] +)/.exec(prevLineText)) !== null) { + // The previous line has an unordered list marker + const prevLeadingSpace: string = matches[1]; + if (currentIndentation >= prevLeadingSpace.length) { + // stop finding + break; + } + } else if ((matches = /^(\s*)\S/.exec(prevLineText)) !== null) { + // The previous line doesn't have a list marker + if (matches[1].length < 3) { + // no enough indentation for a list item + break; + } + } + } + return 1; +} + +/** + * Fix ordered list marker *iteratively* starting from current line + */ +export function fixMarker(editor: TextEditor, line?: number): Thenable | void { + if (!workspace.getConfiguration('typst-companion.extension.orderedList').get('autoRenumber')) return; + if (workspace.getConfiguration('typst-companion.extension.orderedList').get('marker') == 'one') return; + + if (line === undefined) { + line = findNextMarkerLineNumber(editor); + } + if (line < 0 || line >= editor.document.lineCount) { + return; + } + + let currentLineText = editor.document.lineAt(line).text; + let matches; + if ((matches = /^(\s*)([0-9]+)([.)])( +)/.exec(currentLineText)) !== null) { // ordered list + let leadingSpace = matches[1]; + let marker = matches[2]; + let delimiter = matches[3]; + let trailingSpace = matches[4]; + let fixedMarker = lookUpwardForMarker(editor, line, leadingSpace.replace(/\t/g, ' ').length); + let listIndent = marker.length + delimiter.length + trailingSpace.length; + let fixedMarkerString = String(fixedMarker); + + return editor.edit( + // fix the marker (current line) + editBuilder => { + if (marker === fixedMarkerString) { + return; + } + // Add enough trailing spaces so that the text is still aligned at the same indentation level as it was previously, but always keep at least one space + fixedMarkerString += delimiter + " ".repeat(Math.max(1, listIndent - (fixedMarkerString + delimiter).length)); + + editBuilder.replace(new Range(line!, leadingSpace.length, line!, leadingSpace.length + listIndent), fixedMarkerString); + }, + { undoStopBefore: false, undoStopAfter: false } + ).then(() => { + let nextLine = line! + 1; + while (editor.document.lineCount > nextLine) { + const nextLineText = editor.document.lineAt(nextLine).text; + if (/^\s*[0-9]+[.)] +/.test(nextLineText)) { + return fixMarker(editor, nextLine); + } else if ( + editor.document.lineAt(nextLine - 1).isEmptyOrWhitespace // This line is a block + && !nextLineText.startsWith(" ".repeat(3)) // and doesn't have enough indentation + && !nextLineText.startsWith("\t") // so terminates the current list. + ) { + return; + } else { + nextLine++; + } + } + }); + } +} + +function deleteRange(editor: TextEditor, range: Range): Thenable { + return editor.edit( + editBuilder => { + editBuilder.delete(range); + }, + // We will enable undoStop after fixing markers + { undoStopBefore: true, undoStopAfter: false } + ); +} + +function checkTaskList(): Thenable | void { + // - Look into selections for lines that could be checked/unchecked. + // - The first matching line dictates the new state for all further lines. + // - I.e. if the first line is unchecked, only other unchecked lines will + // be considered, and vice versa. + const editor = window.activeTextEditor!; + const uncheckedRegex = /^(\s*([-+*]|[0-9]+[.)]) +\[) \]/ + const checkedRegex = /^(\s*([-+*]|[0-9]+[.)]) +\[)x\]/ + let toBeToggled: Position[] = [] // all spots that have an "[x]" resp. "[ ]" which should be toggled + let newState: boolean | undefined = undefined // true = "x", false = " ", undefined = no matching lines + + // go through all touched lines of all selections. + for (const selection of editor.selections) { + for (let i = selection.start.line; i <= selection.end.line; i++) { + const line = editor.document.lineAt(i); + const lineStart = line.range.start; + + if (!selection.isSingleLine && (selection.start.isEqual(line.range.end) || selection.end.isEqual(line.range.start))) { + continue; + } + + let matches: RegExpExecArray | null; + if ( + (matches = uncheckedRegex.exec(line.text)) + && newState !== false + ) { + toBeToggled.push(lineStart.with({ character: matches[1].length })); + newState = true; + } else if ( + (matches = checkedRegex.exec(line.text)) + && newState !== true + ) { + toBeToggled.push(lineStart.with({ character: matches[1].length })); + newState = false; + } + } + } + + if (newState !== undefined) { + const newChar = newState ? 'x' : ' '; + return editor.edit(editBuilder => { + for (const pos of toBeToggled) { + let range = new Range(pos, pos.with({ character: pos.character + 1 })); + editBuilder.replace(range, newChar); + } + }); + } +} + +function onMoveLineUp() { + const editor = window.activeTextEditor!; + return commands.executeCommand('editor.action.moveLinesUpAction') + .then(() => fixMarker(editor)); +} + +function onMoveLineDown() { + const editor = window.activeTextEditor!; + return commands.executeCommand('editor.action.moveLinesDownAction') + .then(() => fixMarker(editor, findNextMarkerLineNumber(editor, editor.selection.start.line - 1))); +} + +function onCopyLineUp() { + const editor = window.activeTextEditor!; + return commands.executeCommand('editor.action.copyLinesUpAction') + .then(() => fixMarker(editor)); +} + +function onCopyLineDown() { + const editor = window.activeTextEditor!; + return commands.executeCommand('editor.action.copyLinesDownAction') + .then(() => fixMarker(editor)); +} + +function onIndentLines() { + const editor = window.activeTextEditor!; + return indent(editor).then(() => fixMarker(editor)); +} + +function onOutdentLines() { + const editor = window.activeTextEditor!; + return outdent(editor).then(() => fixMarker(editor)); +} + +export function deactivate() { } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 965a7b4..9fd6eeb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,13 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "lib": [ - "ES2020" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "commonjs", + "target": "es2020", + "lib": ["es2020"], + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "strict": true + }, + "include": ["src"], + "exclude": ["node_modules"] } diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md deleted file mode 100644 index b2eb4a4..0000000 --- a/vsc-extension-quickstart.md +++ /dev/null @@ -1,47 +0,0 @@ -# Welcome to your VS Code Extension - -## What's in the folder - -* This folder contains all of the files necessary for your extension. -* `package.json` - this is the manifest file in which you declare your extension and command. - * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. -* `src/extension.ts` - this is the main file where you will provide the implementation of your command. - * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. - * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. - -## Setup - -* install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) - - -## Get up and running straight away - -* Press `F5` to open a new window with your extension loaded. -* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. -* Set breakpoints in your code inside `src/extension.ts` to debug your extension. -* Find output from your extension in the debug console. - -## Make changes - -* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. -* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - - -## Explore the API - -* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. - -## Run tests - -* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. -* Press `F5` to run the tests in a new window with your extension loaded. -* See the output of the test result in the debug console. -* Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. - * The provided test runner will only consider files matching the name pattern `**.test.ts`. - * You can create folders inside the `test` folder to structure your tests any way you want. - -## Go further - -* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). -* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. -* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).