Skip to content

Commit

Permalink
Merge pull request #1227 from BetterThanTomorrow/1224-comment-trail-b…
Browse files Browse the repository at this point in the history
…racket

Special formatting for comment form trailing bracket
  • Loading branch information
PEZ authored Jul 11, 2021
2 parents 42f390c + 8c2753e commit 146b3fa
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 51 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changes to Calva.
## [Unreleased]
- Fix: [Calva formatting defaults do not get applied when including any kind of .cljfmt.edn config](https://github.com/BetterThanTomorrow/calva/issues/1228)
- Workaround: [Paredit commands don't propagate to multiple cursors](https://github.com/BetterThanTomorrow/calva/issues/610)
- [Put closing paren of rich comments on a separate line](https://github.com/BetterThanTomorrow/calva/issues/1224)

## [2.0.203] - 2021-07-04
- Fix: [Custom repl commands show error if run from non-clojure file](https://github.com/BetterThanTomorrow/calva/issues/1203)
Expand Down
69 changes: 31 additions & 38 deletions docs/site/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ NB: _The below assumes you have read about [Finding Calva Commands and Shortcuts

## Evaluation in a File Editor

Calva has commands for evaluating the **current form** and the **current top-level form**.
Calva has many commands for evaluating forms, including the **current form** and the **current top-level form**.

You can also choose what should happen with the results:
Some of the commands also let you choose what should happen with the results:

1. **Inline.** This will display the results (or some of it, if it is long) inline in the editor. _You find the full results in the [output window](output.md)_, from where it is easy to copy it to the clipboard.
1. **To comments.** This will add the results as comment lines below the current line.
1. **Inline.** This will display the results (or some of it, if it is long) inline in the editor.
* This also creates a hover pane including the full results and a button which will copy the results to the clipboard.
* There is also a command for copying the last result to the clipboard.
* The full results are always available in the [output window](output.md).
* There is a command for showing the output window, allowing for a workflow where you either generally have it closed, or have it as one of the tabs in the same editor group as the files you are working with.
1. **To comments.** This will add the results as line comments below the current line.
1. **Replace the evaluated code.** This will do what it says, the evaluated code will be replaced with its results.

## Wait, Current Form? Top-level Form?
Expand All @@ -22,47 +26,36 @@ These are important concepts in Calva in order for you to create your most effec

Default shortcut for evaluating the current form: `ctrl+enter`.

The current form either means the current selection, or otherwise is based on the cursor position. Play some with the command **Calva: Select current form**, `ctrl+alt+c s`, to figure out what Calva thinks is the current form for some different situations. Try it inside a symbol, adjacent to a symbol (both sides) and adjacent to an opening or closing bracket (again, both sides). Generally the current form is determined like so:

If text is selected, then that text

If the cursor is ”in” a symbol, then that symbol
```clojure
foob|ar ; foobar
```

If the cursor is adjacent to a form (a symbol or a list of some kind), then that form
```clojure
(foo bar |(baz)) ; (baz)
```

If the cursor is between to forms, then the left side form
```clojure
(foo bar | (baz)) ; bar
```

If the cursor is before the first form of a line, then that form
```clojure
(foo
| bar (baz)) ; bar
```
The **current form** either means the current selection, or otherwise is based on the cursor position. Play some with the command **Calva: Select current form**, `ctrl+alt+c s`, to figure out what Calva thinks is the current form for some different situations. Try it inside a symbol, adjacent to a symbol (both sides) and adjacent to an opening or closing bracket (again, both sides). Generally the current form is determined like so:

1. If text is selected, then that text
1. If the cursor is ”in” a symbol, then that symbol
```clojure
foob|ar ; foobar
```
1. If the cursor is adjacent to a form (a symbol or a list of some kind), then that form
```clojure
(foo bar |(baz)) ; (baz)
```
1. If the cursor is between to forms, then the left side form
```clojure
(foo bar | (baz)) ; bar
```
1. If the cursor is before the first form of a line, then that form
```clojure
(foo
| bar (baz)) ; bar
```

### Current Top-level Form

Default shortcut for evaluating the current top level form: `alt+enter`.

The current top-level form means top-level in a structural sense. It is _not_ the topmost form in the file. Typically in a Clojure file you will find `def` and `defn` (and `defwhatever`) forms at the top level, but it can be any form not enclosed in any other form.

An exception is the `comment` form. It will create a new top level context, so that any forms immediately inside a `(commment ...)` form will be considered top-level by Calva. This is to support a workflow where you

1. Iterate on your functions.
2. Evaluate the function (top level).
3. Put them to test with expressions inside a `comment` form.
4. Repeat from *1.*, until the function does what you want it to do.
The **current top-level form** means top-level in a structural sense. It is _not_ the topmost form in the file. Typically in a Clojure file you will find `def` and `defn` (and `defwhatever`) forms at the top level, which also is one major intended use for evaluating top level form: _to define and redefine variables_. However, Calva does not check the contents of the form in order to determine it as a top-level forms: _all forms not enclosed in any other form are top level forms_.

Here's a demo of the last repetition of such a workflow, for a simple implementation of the `abs` function:
An ”exception” is introduced by the `comment` form. It will create a new top level context, so that any forms immediately inside a `(commment ...)` form will be considered top-level by Calva. This is to support a workflow with what is often referred to the [Rich Comments](rich-comments.md).

![top-level-eval](images/howto/top-level-eval.gif)
At the top level the selection of which form is the current top level form follows the same rules as those for [the current form](#current-form).

### Evaluate to Cursor

Expand Down
7 changes: 4 additions & 3 deletions docs/site/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ With the default settings, Calva's formatting behaves like so:
* formats the current enclosing form when you hit `tab`
* formats pasted code
* formats according to community standards (see above link)
* formats the current form, _aligning map keys and values_, when you press `ctrl+alt+l`.
* formats the current form, _aligning map keys and values_, when you press `ctrl+alt+l`
* formats `(comment ..)` forms special, see [rich comments](#rich-comments)

!!! Tips
Calva has a command that will ”heal” the bracket structure if it is correctly indented. Yes, it is Parinfer behind the scenes. This command is default bound to `shift+tab` to form a nicely balanced pair with the `tab` formatting.
Expand Down Expand Up @@ -89,6 +90,6 @@ Save, then hit `tab`, and the code should get formatted like so:

That's somewhat similar to Nikita Prokopov's [Better Clojure Formatting](https://tonsky.me/blog/clojurefmt/) suggestion. (Please be aware that this setting might not be sufficient to get complete **Tonsky Formatting**, please share any settings you use to get full compliance.)

## Under Construction
## Rich Comments

Much of this formatting configurability is recent work. There might be dragons. And also, we probably should make Calva pick the `:cljfmt` config up from Leiningen project files. If you agree, and there isn't an issue about that already, please file one.
To encourage use of `(comment ...)` forms for development, these forms get a special treatment when formatting. See [Rich Comments](rich-comments.md).
129 changes: 129 additions & 0 deletions docs/site/rich-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Rich Comments Support

Why bother with **Rich comments**? Read on. Consider watching [this Youtube video](https://www.youtube.com/watch?v=d0K1oaFGvuQ) for a demo of the workflow using the (in?)famous FizzBuzz problem as an example.

<iframe width="560" height="315" src="https://www.youtube.com/embed/d0K1oaFGvuQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## Things in `comment` is not evaluated

The Clojure `comment` macro is defined like so:

```clojure
(defmacro comment
"Ignores body, yields nil"
{:added "1.0"}
[& body])
```

It has no forms in its body and will therefore always (as long as the Clojure Reader can read it) evaluate to `nil`. That is: _nothing in the `(comment ...)` form will get evaluated when the file is loaded_.

This makes it a very good ”place” where you can develop code, experiment with code, and keep example code. Since you will be able to load/evaluate the current file without worrying about that the code in the `comment` form will get evaluated. This also holds true for when using tools that hot-reloads the code on save, such as [Figwheel](https://figwheel.org), [shadow-cljs](https://github.com/thheller/shadow-cljs) and [Krell](https://calva.io/krell/).

To develop or refine a function you might:

1. Open up a `(comment ...)` form
1. Inside this form, type a first, super simple, version (or refinement) of your function and evaluate it
1. Inside the same `comment` form, type some code to test your function and evaluate that
* Or type and evaluate some code you might need for your function
1. Repeat from *2.*, until the function does what you want it to do
1. Move the function definition out of the `comment` form
1. Clean up the `comment` form to keep some of the test code as example use, or ”design decision log” for the function.

!!! Note
Using `(comment ...)` forms for developing code is very common among Clojure coders. Rich Hickey is known for using it, which is why they are called **Rich comments** to begin with (even if it also is a very rich experience).

## Calva encourages Rich comments

Calva has several features to facilitate the Rich comments workflow, e.g.

1. Secial [Syntax highlight](customizing.md#calva-highlight). By default `comment` forms are rendered in _italics_
1. Special [top-level form](evaluation.md#current-top-level-form) context
1. Special formatting

### `comment` is top-level

To make it easy to evaluate forms in `(commment ...)` forms, they create a new top-level context. Instead of you having to place the cursor with precision before evaluating the **current form**, you can have the cursor anywhere within a `comment` enclosed form and [**Evaluate Top-Level Form**](evaluation.md#current-top-level-form).

This carries over to all commands in Calva which deal with the top level form. Including [custom command snippets](custom-commands.md).

### Special formatting

To invite a **Rich comments** workflow, the Calva command **Format Current Form** will not fold the closing bracket of the `(comment ...)` form. Instead it will place this bracket on a line of its own (or keep it there).

```clojure
(comment
)
```

With the cursor somewhere directly inside the comment form (denoted with a `|`):

```clojure
(comment
(def foo
:foo)|)
```

<kbd>tab</kbd>

```clojure
(comment
(def foo
:foo)
|)
```

#### Thinking space is kept

The formatter will not remove newlines between the cursor and the closing bracket. So if you have entered a few lines to get ”thinking” room:

```clojure
(comment
(def foo
:foo)

|

)
```

<kbd>tab</kbd>

```clojure
(comment
(def foo
:foo)

|

)
```

#### Fold when done

To fold the trailing paren automatically, place the cursor immediately outside (before or after) the form:

```clojure
(comment
(def foo
:foo))|
```

<kbd>tab</kbd>

```clojure
(comment
(def foo
:foo))|
```

#### Enabled by default

You can disable this behavior with the setting: `calva.fmt.keepCommentTrailParenOnOwnLine`.

But why would you? It is awesome! 😄


#### Only for the Current Form

!!! Note
This treatment only applies to formatting of [the current form](evaluation.md#current-form). With [fold when done](#fold-when-done) as an exception.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nav:
- Features:
- commands-top10.md
- evaluation.md
- rich-comments.md
- output.md
- formatting.md
- paredit.md
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,11 @@
"type": "boolean",
"default": true,
"markdownDescription": "Use the structural editor for indentation (instead of `cljfmt`)."
},
"calva.fmt.keepCommentTrailParenOnOwnLine": {
"type": "boolean",
"default": true,
"markdownDescription": "Treat `(comment...)` forms special and keep its closing paren on a line of its own."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions src/calva-fmt/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const defaultCljfmtContent = "\
function configuration(workspaceConfig: vscode.WorkspaceConfiguration, cljfmtString: string) {
return {
"format-as-you-type": workspaceConfig.get("formatAsYouType") as boolean,
"keep-comment-forms-trail-paren-on-own-line?": workspaceConfig.get("keepCommentTrailParenOnOwnLine") as boolean,
"cljfmt-string": cljfmtString,
"cljfmt-options": cljfmtOptions(cljfmtString)
};
Expand Down
4 changes: 3 additions & 1 deletion src/calva-fmt/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export function formatPositionInfo(editor: vscode.TextEditor, onType: boolean =
const mirroredDoc: MirroredDocument = getDocument(doc);
const cursor = mirroredDoc.getTokenCursor(index);
const formatDepth = extraConfig["format-depth"] ? extraConfig["format-depth"] : 1;
const isComment = cursor.getFunctionName() === 'comment';
const config = {...extraConfig, "comment-form?": isComment};
let formatRange = cursor.rangeForList(formatDepth);
if (!formatRange) {
formatRange = cursor.rangeForCurrentForm(index);
Expand All @@ -60,7 +62,7 @@ export function formatPositionInfo(editor: vscode.TextEditor, onType: boolean =
"range-text": string,
"range": number[],
"new-index": number
} = _formatIndex(doc.getText(), formatRange, index, doc.eol == 2 ? "\r\n" : "\n", onType, extraConfig);
} = _formatIndex(doc.getText(), formatRange, index, doc.eol == 2 ? "\r\n" : "\n", onType, config);
const range: vscode.Range = new vscode.Range(doc.positionAt(formatted.range[0]), doc.positionAt(formatted.range[1]));
const newIndex: number = doc.offsetAt(range.start) + formatted["new-index"];
const previousText: string = doc.getText(range);
Expand Down
Loading

0 comments on commit 146b3fa

Please sign in to comment.