Skip to content

Commit 1d95073

Browse files
authored
Support comment link (#97)
* Support comment link * Stop refreshing when press cmd key * Fix conflicts * Optimize CSS * Fix style changing * Ignore click events * Optimize interaction * Update README
1 parent e9f6c84 commit 1d95073

File tree

7 files changed

+154
-8
lines changed

7 files changed

+154
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- [Editor](https://recho.dev/) 📝 - The quickest way to get started with Recho.
88
- [Documentation](https://recho.dev/docs/introduction) 📚 - Learn how to use Recho with our comprehensive guides.
99
- [Examples](https://recho.dev/examples) 🖼️ - See what you can create and draw some inspiration!
10-
- [Sharing](/CONTRIBUTING.md#contributing-to-recho) 🎨 - Follow the instructions to open a [pull request](https://github.com/recho-dev/recho/new/main/app/examples) to share your sketches!
10+
- [Sharing](/CONTRIBUTING.md#sharing-examples) 🎨 - Follow the instructions to open a [pull request](https://github.com/recho-dev/recho/new/main/app/examples) to share your sketches!
1111
- [Contributing](/CONTRIBUTING.md) 🙏 - We have [a bunch of things](https://github.com/recho-dev/recho/issues) that we would like you to help us build together!
1212

1313
## Why Recho 💡

editor/commentLink.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {syntaxTree} from "@codemirror/language";
2+
import {Decoration, ViewPlugin, WidgetType} from "@codemirror/view";
3+
4+
const isWindows = navigator.userAgent.includes("Windows");
5+
6+
class LinkWidget extends WidgetType {
7+
constructor(url) {
8+
super();
9+
this.url = url;
10+
}
11+
12+
toDOM() {
13+
const link = document.createElement("a");
14+
link.href = this.url;
15+
link.textContent = this.url;
16+
link.target = "_blank";
17+
link.rel = "noopener noreferrer";
18+
link.className = "cm-comment-link";
19+
link.title = `Open in a new tab (${isWindows ? "Ctrl" : "Cmd"} + Click)`;
20+
21+
// Prevent default click behavior - only open on Cmd+Click
22+
const preventDefault = (e) => !e.metaKey && !e.ctrlKey && e.preventDefault();
23+
link.addEventListener("click", preventDefault);
24+
25+
// Change cursor to pointer when Ctrl/Cmd is held
26+
const add = (e) => (e.metaKey || e.ctrlKey) && link.classList.add("cmd-clickable");
27+
const remove = (e) => !e.metaKey && !e.ctrlKey && link.classList.remove("cmd-clickable");
28+
link.addEventListener("mouseenter", add);
29+
link.addEventListener("mousemove", add);
30+
link.addEventListener("mouseleave", remove);
31+
return link;
32+
}
33+
34+
ignoreEvent(event) {
35+
// Ignore click events when Cmd/Ctrl is held (for opening links)
36+
if (event.type === "mousedown" || event.type === "click") {
37+
return event.metaKey || event.ctrlKey;
38+
}
39+
return false;
40+
}
41+
}
42+
43+
export const commentLink = ViewPlugin.fromClass(
44+
class {
45+
constructor(view) {
46+
this.decorations = this.buildDeco(view);
47+
}
48+
49+
update(update) {
50+
if (update.docChanged || update.viewportChanged) {
51+
this.decorations = this.buildDeco(update.view);
52+
}
53+
}
54+
55+
buildDeco(view) {
56+
let widgets = [];
57+
let tree = syntaxTree(view.state);
58+
59+
// Only iterate tree nodes that intersect with the viewport.
60+
for (const {from, to} of view.visibleRanges) {
61+
tree.iterate({
62+
from,
63+
to,
64+
enter: ({type, from, to}) => {
65+
if (type.name === "BlockComment" || type.name === "LineComment") {
66+
const text = view.state.doc.sliceString(from, to);
67+
let httpRegex = /https?:\/\/[^\s\)]+/g;
68+
let m;
69+
while ((m = httpRegex.exec(text))) {
70+
const tagStart = from + m.index;
71+
const tagEnd = tagStart + m[0].length;
72+
if (tagEnd >= from && tagStart <= to) {
73+
const url = m[0];
74+
const widget = new LinkWidget(url);
75+
widgets.push(
76+
Decoration.replace({
77+
widget,
78+
inclusive: true,
79+
}).range(tagStart, tagEnd),
80+
);
81+
}
82+
}
83+
}
84+
},
85+
});
86+
}
87+
88+
return Decoration.set(widgets, true);
89+
}
90+
},
91+
{
92+
decorations: (v) => v.decorations,
93+
},
94+
);

editor/index.css

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@
6464
}
6565

6666
.cm-doc-tag span {
67-
color: #cf222e !important;
67+
color: #cf222e;
68+
}
69+
70+
.cm-comment-link {
71+
color: #6a737d;
72+
text-decoration: underline;
73+
cursor: text;
74+
}
75+
76+
.cm-comment-link.cmd-clickable {
77+
cursor: pointer;
78+
}
79+
80+
.cm-comment-link.cmd-clickable:hover {
81+
color: #0550ae;
6882
}
6983
}

editor/index.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import {outputProtection} from "./protection.js";
1212
import {dispatch as d3Dispatch} from "d3-dispatch";
1313
import {rechoCompletion} from "./completion.js";
1414
import {docStringTag} from "./docStringTag.js";
15+
import {commentLink} from "./commentLink.js";
1516

1617
export function createEditor(container, options) {
1718
const {code} = options;
1819
const dispatcher = d3Dispatch("userInput");
19-
2020
const runtimeRef = {current: null};
2121

2222
const state = EditorState.create({
@@ -51,17 +51,19 @@ export function createEditor(container, options) {
5151
// Disable this for now, because it prevents copying/pasting the code.
5252
// outputProtection(),
5353
docStringTag,
54+
commentLink,
5455
],
5556
});
5657

57-
const view = new EditorView({
58-
state,
59-
parent: container,
60-
});
58+
const view = new EditorView({state, parent: container});
59+
60+
let isStopByMetaKey = false;
6161

6262
function initRuntime() {
6363
runtimeRef.current = createRuntime(view.state.doc.toString());
6464
runtimeRef.current.onChanges(dispatch);
65+
window.addEventListener("keydown", onKeyDown);
66+
window.addEventListener("keyup", onKeyUp);
6567
}
6668

6769
function dispatch(changes) {
@@ -84,6 +86,25 @@ export function createEditor(container, options) {
8486
}
8587
}
8688

89+
// Stop running when press cmd key. This is useful when we want to open a link
90+
// in the comment by cmd + click. If we don't stop running, the links consistently
91+
// create and destroy, and there is no way to click them. This also makes sense
92+
// when we want to copy/paste/select code using the shortcut with cmd key.
93+
function onKeyDown(e) {
94+
if (e.metaKey || e.ctrlKey) {
95+
if (runtimeRef.current?.isRunning()) isStopByMetaKey = true;
96+
runtimeRef.current?.setIsRunning(false);
97+
}
98+
}
99+
100+
function onKeyUp(e) {
101+
const key = e.key;
102+
if ((key === "Meta" || key === "Control") && isStopByMetaKey) {
103+
isStopByMetaKey = false;
104+
runtimeRef.current?.setIsRunning(true);
105+
}
106+
}
107+
87108
return {
88109
run: () => {
89110
if (!runtimeRef.current) initRuntime();
@@ -92,10 +113,14 @@ export function createEditor(container, options) {
92113
stop: () => {
93114
runtimeRef.current?.destroy();
94115
runtimeRef.current = null;
116+
window.removeEventListener("keydown", onKeyDown);
117+
window.removeEventListener("keyup", onKeyUp);
95118
},
96119
on: (event, callback) => dispatcher.on(event, callback),
97120
destroy: () => {
98121
runtimeRef.current?.destroy();
122+
window.removeEventListener("keydown", onKeyDown);
123+
window.removeEventListener("keyup", onKeyUp);
99124
view.destroy();
100125
},
101126
};

runtime/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,5 +305,5 @@ export function createRuntime(initialCode) {
305305
rerun(code);
306306
}
307307

308-
return {setCode, setIsRunning, run, onChanges, destroy};
308+
return {setCode, setIsRunning, run, onChanges, destroy, isRunning: () => isRunning};
309309
}

test/js/comment-link.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const commentLink = `// https://recho.dev
2+
// http://recho.dev
3+
4+
/**
5+
* [Recho](https://recho.dev)
6+
* [Recho](http://recho.dev)
7+
*/
8+
9+
const now = recho.now();
10+
11+
echo(now);
12+
`;

test/js/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {randomHistogram} from "./random-histogram.js";
1313
export {mandelbrotSet} from "./mandelbrot-set.js";
1414
export {matrixRain} from "./matrix-rain.js";
1515
export {jsDocString} from "./js-doc-string.js";
16+
export {commentLink} from "./comment-link.js";

0 commit comments

Comments
 (0)