Skip to content

Commit

Permalink
feat: Insert heading at current, higher and deeper level (#10)
Browse files Browse the repository at this point in the history
* Add v0 implementation of 'insert heading' feature

* Fix some little index bugs

* Fix conditional line assignment

* Add additional test

* doc: add document of insert headings

Co-authored-by: kasahala <[email protected]>
  • Loading branch information
VFUC and k4a-l authored Jan 6, 2023
1 parent 828ebf7 commit d7188d7
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 6 deletions.
46 changes: 40 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@

Easily Shift and Change markdown headings.

## Demo

![Demo](https://raw.githubusercontent.com/k4a-dev/obsidian-heading-shifter/main/doc/attachment/shiftHeadings.gif)

## Why use this plugin

Obsidian links numerous markdown files to form knowledge. Daily rearrangement of links is important to create a good knowledge base.
Expand Down Expand Up @@ -41,7 +37,7 @@ Download directory(includes `main.js, manifest.json, styles.css`) from the lates

## Features

### Applying Heading
### Apply Headings

![Applying Heading Demo](https://raw.githubusercontent.com/k4a-dev/obsidian-heading-shifter/main/doc/attachment/applyingHeading.gif)

Expand All @@ -54,7 +50,7 @@ Download directory(includes `main.js, manifest.json, styles.css`) from the lates

> It is useful to assign a hotkey such as `Ctrl + 0 ~ 6`
### Headings Shift
### Shift Headings

![Headings Shift Demo](https://raw.githubusercontent.com/k4a-dev/obsidian-heading-shifter/main/doc/attachment/shiftHeadings.gif)

Expand All @@ -79,6 +75,44 @@ Download directory(includes `main.js, manifest.json, styles.css`) from the lates
- `Increase Headings` is ineffective if selected lines contains less than `Lower limit of Heading`.
- `Decrease Headings` is ineffective if selected lines contains more than heading 6.

### Insert Headings

#### Commands

| Command | Description | Hotkey |
| ---------------------------------- | ------------------------------------------------- | ------ |
| Insert Heading at current level | Change current line headings to current level | |
| Insert Heading at one level deeper | Change current line headings to current level + 1 | |
| Insert Heading at one level higher | Change current line headings to current level - 1 | |

#### Use Case

Operate headings like an outliner like the following,

```markdown
# The Festival Myster -> hit "Apply 1"

This is a great document.

## Chapter One -> hit "Insert deeper"

### Prologue -> hit "Insert deeper"

The sun was setting over the horizon...

### The Summer Festival -> hit "Insert current"

As the townspeople gathered in the town square...

## Chapter Two -> hit "Insert higher"

### The Mystery of the Missing Prize -> hit "Insert deeper"

As the summer festival came to a close...
```

If you want to make headings deeper or higher than 2, use "shift" or "apply".

## Loadmap

Nothing specific at this time.
Expand Down
1 change: 1 addition & 0 deletions src/features/insertHeading/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createInsertHeadingAtCurrentLevelCommand, createInsertHeadingAtDeeperLevelCommand, createInsertHeadingAtHigherLevelCommand } from "./operation";
103 changes: 103 additions & 0 deletions src/features/insertHeading/operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { applyHeading } from "features/applyHeading";
import { Command, Editor, Notice } from "obsidian";
import { composeLineChanges } from "utils/editorChange";
import { checkHeading, getPreviousHeading } from "utils/markdown";

export const createInsertHeadingAtCurrentLevelCommand = (): Command => {
const createEditorCallback = () => {
return (editor: Editor) => {

const cursorLine = editor.getCursor("from").line
const lastHeadingLine = getPreviousHeading(editor, cursorLine)

// current heading level == most recently added heading
// 0 if no heading exists yet
const headingLevel = lastHeadingLine != undefined ? checkHeading(editor.getLine(lastHeadingLine)) : 0

editor.transaction({
changes: composeLineChanges(editor, [cursorLine], (chunk: string) =>
applyHeading(chunk, headingLevel)
),
});

editor.setCursor(editor.getCursor().line)
return
};
};

// return command object
return {
id: `insert-heading-current`,
name: `Insert Heading at current level`,
icon: `headingShifter_heading`,
editorCallback: createEditorCallback(),
};
};


export const createInsertHeadingAtDeeperLevelCommand = (): Command => {
const createEditorCallback = () => {
return (editor: Editor) => {

const cursorLine = editor.getCursor("from").line
const lastHeadingLine = getPreviousHeading(editor, cursorLine)

// current heading level == most recently added heading
// 0 if no heading exists yet
const headingLevel = lastHeadingLine ? checkHeading(editor.getLine(lastHeadingLine)) : 0

if (headingLevel+1 > 6) {
new Notice("Cannot Increase (contains more than Heading 6)");
return true;
}

editor.transaction({
changes: composeLineChanges(editor, [cursorLine], (chunk: string) =>
applyHeading(chunk, headingLevel + 1)
),
});

editor.setCursor(editor.getCursor().line)
return
};
};

// return command object
return {
id: `insert-heading-deeper`,
name: `Insert Heading at one level deeper`,
icon: `headingShifter_heading`,
editorCallback: createEditorCallback(),
};
};

export const createInsertHeadingAtHigherLevelCommand = (): Command => {
const createEditorCallback = () => {
return (editor: Editor) => {

const cursorLine = editor.getCursor("from").line
const lastHeadingLine = getPreviousHeading(editor, cursorLine)

// current heading level == most recently added heading
// 0 if no heading exists yet
const headingLevel = lastHeadingLine ? checkHeading(editor.getLine(lastHeadingLine)) : 0

editor.transaction({
changes: composeLineChanges(editor, [cursorLine], (chunk: string) =>
applyHeading(chunk, headingLevel - 1)
),
});

editor.setCursor(editor.getCursor().line)
return
};
};

// return command object
return {
id: `insert-heading-higher`,
name: `Insert Heading at one level higher`,
icon: `headingShifter_heading`,
editorCallback: createEditorCallback(),
};
};
4 changes: 4 additions & 0 deletions src/services/registerService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createApplyHeadingCommand } from "features/applyHeading";
import { IncreaseHeading, DecreaseHeading } from "features/shiftHeading";
import { createInsertHeadingAtCurrentLevelCommand, createInsertHeadingAtDeeperLevelCommand, createInsertHeadingAtHigherLevelCommand } from "features/insertHeading";
import HeadingShifter from "main";
import { HEADINGS } from "types/type";

Expand Down Expand Up @@ -28,6 +29,9 @@ export class RegisterService {
);
this.plugin.addCommand(increaseHeading.createCommand());
this.plugin.addCommand(decreaseHeading.createCommand());
this.plugin.addCommand(createInsertHeadingAtCurrentLevelCommand())
this.plugin.addCommand(createInsertHeadingAtDeeperLevelCommand())
this.plugin.addCommand(createInsertHeadingAtHigherLevelCommand())

this.plugin.registerEditorExtension(
Prec.highest(
Expand Down
24 changes: 24 additions & 0 deletions src/utils/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,27 @@ export const getHeadingLines = (
}
return { headingLines, minHeading, maxHeading };
};

// goes backwards from the `from` line, returns line number of first line containing any heading
export const getPreviousHeading = (
editor: {
getLine: (number: number) => string;
},
from: number
) => {

let fence: FenceType = null;
let start = from > 0 ? from - 1 : 0

for (let line = start; line >= 0; line--) {
fence = getFenceStatus(fence, checkFence(editor.getLine(line)));
if (fence) continue;

if (checkHeading(editor.getLine(line)) > 0) {
return line
}
}

// no heading found
return undefined
};
57 changes: 57 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FenceType,
getFenceStatus,
getHeadingLines,
getPreviousHeading
} from "utils/markdown";

import { createRange } from "utils/range";
Expand Down Expand Up @@ -123,6 +124,62 @@ Normal
});
});

describe("getPreviousHeading", () => {
test("normal", () => {
const input = `# Heading1
## Heading2
Normal
`;

const editor = new Editor(input);
expect(getPreviousHeading(editor, 4)).toEqual(2);
});

test("edge", () => {
const input = `# Heading1
## Heading2
Normal
`;
const editor = new Editor(input);
expect(getPreviousHeading(editor, 1)).toEqual(0);
});

test("no heading", () => {
const input = `Normal
Normal
~~~~
addAbortSignal
~~~
Normal
`;

const editor = new Editor(input);
expect(getPreviousHeading(editor, 10)).toEqual(undefined);
});

test("'from line' contains heading", () => {
const input = `# Heading1
## Heading2
Normal
`;
const editor = new Editor(input);
expect(getPreviousHeading(editor, 2)).toEqual(0);
});
});

describe("compose editorChange", () => {
test("", () => {
const input = `a
Expand Down

0 comments on commit d7188d7

Please sign in to comment.