2023-07-14 19:58:16 +08:00
import customMarkdownStyle from "./style.ts" ;
2024-07-05 03:22:37 +08:00
import { history , indentWithTab , standardKeymap } from "@codemirror/commands" ;
2023-07-14 19:58:16 +08:00
import {
autocompletion ,
closeBrackets ,
closeBracketsKeymap ,
completionKeymap ,
2024-03-16 22:29:24 +08:00
} from "@codemirror/autocomplete" ;
import {
codeFolding ,
indentOnInput ,
LanguageDescription ,
LanguageSupport ,
syntaxHighlighting ,
} from "@codemirror/language" ;
2024-07-04 13:05:58 +08:00
import { EditorState } from "@codemirror/state" ;
2024-03-16 22:29:24 +08:00
import {
2024-07-04 13:05:58 +08:00
drawSelection ,
2023-07-14 19:58:16 +08:00
dropCursor ,
EditorView ,
highlightSpecialChars ,
2024-07-30 23:33:33 +08:00
type KeyBinding ,
2023-07-14 19:58:16 +08:00
keymap ,
ViewPlugin ,
2024-07-30 23:33:33 +08:00
type ViewUpdate ,
2024-03-16 22:29:24 +08:00
} from "@codemirror/view" ;
import { vim } from "@replit/codemirror-vim" ;
import { markdown } from "@codemirror/lang-markdown" ;
2024-07-30 23:33:33 +08:00
import type { Client } from "./client.ts" ;
2024-07-09 15:22:08 +08:00
import { inlineImagesPlugin } from "./cm_plugins/inline_content.ts" ;
2023-07-14 19:58:16 +08:00
import { cleanModePlugins } from "./cm_plugins/clean.ts" ;
import { lineWrapper } from "./cm_plugins/line_wrapper.ts" ;
2024-07-20 01:08:49 +08:00
import { createSmartQuoteKeyBindings } from "./cm_plugins/smart_quotes.ts" ;
2024-07-30 23:33:33 +08:00
import type { ClickEvent } from "../plug-api/types.ts" ;
2023-07-14 19:58:16 +08:00
import {
attachmentExtension ,
pasteLinkExtension ,
} from "./cm_plugins/editor_paste.ts" ;
2024-07-30 23:33:33 +08:00
import type { TextChange } from "./change.ts" ;
2023-11-27 23:29:19 +08:00
import { postScriptPrefacePlugin } from "./cm_plugins/top_bottom_panels.ts" ;
2024-02-09 04:00:45 +08:00
import { languageFor } from "$common/languages.ts" ;
2023-11-21 23:24:20 +08:00
import { plugLinter } from "./cm_plugins/lint.ts" ;
2024-07-30 23:33:33 +08:00
import { Compartment , type Extension } from "@codemirror/state" ;
2024-02-09 04:00:45 +08:00
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts" ;
import { parseCommand } from "$common/command.ts" ;
import { safeRun } from "$lib/async.ts" ;
2024-02-23 17:12:48 +08:00
import { codeCopyPlugin } from "./cm_plugins/code_copy.ts" ;
2023-07-14 19:58:16 +08:00
export function createEditorState (
2023-11-21 23:24:20 +08:00
client : Client ,
2023-07-14 19:58:16 +08:00
pageName : string ,
text : string ,
readOnly : boolean ,
) : EditorState {
let touchCount = 0 ;
2024-01-21 02:16:07 +08:00
// Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded
client . keyHandlerCompartment = new Compartment ( ) ;
const keyBindings = client . keyHandlerCompartment . of (
createKeyBindings ( client ) ,
) ;
2023-07-14 19:58:16 +08:00
return EditorState . create ( {
doc : text ,
extensions : [
// Not using CM theming right now, but some extensions depend on the "dark" thing
2023-07-14 20:22:26 +08:00
EditorView . theme ( { } , {
2023-11-21 23:24:20 +08:00
dark : client.ui.viewState.uiOptions.darkMode ,
2023-07-14 20:22:26 +08:00
} ) ,
2024-07-05 03:22:37 +08:00
2023-07-14 19:58:16 +08:00
// Enable vim mode, or not
[
2023-11-21 23:24:20 +08:00
. . . client . ui . viewState . uiOptions . vimMode ? [ vim ( { status : true } ) ] : [ ] ,
2023-07-14 20:22:26 +08:00
] ,
[
2023-11-21 23:24:20 +08:00
. . . readOnly || client . ui . viewState . uiOptions . forcedROMode
2024-07-05 03:22:37 +08:00
? [ EditorView . editable . of ( false ) ]
2023-07-14 19:58:16 +08:00
: [ ] ,
] ,
2023-11-21 23:24:20 +08:00
2023-07-14 19:58:16 +08:00
// The uber markdown mode
markdown ( {
2024-01-24 20:34:12 +08:00
base : extendedMarkdownLanguage ,
2023-10-03 20:16:33 +08:00
codeLanguages : ( info ) = > {
const lang = languageFor ( info ) ;
if ( lang ) {
return LanguageDescription . of ( {
name : info ,
support : new LanguageSupport ( lang ) ,
} ) ;
}
return null ;
} ,
2023-07-14 19:58:16 +08:00
addKeymap : true ,
} ) ,
2024-01-24 20:34:12 +08:00
extendedMarkdownLanguage . data . of ( {
2023-07-14 19:58:16 +08:00
closeBrackets : { brackets : [ "(" , "{" , "[" , "`" ] } ,
} ) ,
2024-01-24 20:34:12 +08:00
syntaxHighlighting ( customMarkdownStyle ( ) ) ,
2023-07-14 19:58:16 +08:00
autocompletion ( {
override : [
2023-11-21 23:24:20 +08:00
client . editorComplete . bind ( client ) ,
2024-02-07 21:50:01 +08:00
client . clientSystem . slashCommandHook . slashCommandCompleter . bind (
client . clientSystem . slashCommandHook ,
2023-07-14 19:58:16 +08:00
) ,
] ,
2024-07-31 17:28:31 +08:00
optionClass ( completion : any ) {
console . log ( "Calling on" , completion ) ;
if ( completion . cssClass ) {
return "sb-decorated-object " + completion . cssClass ;
} else {
return "" ;
}
} ,
2023-07-14 19:58:16 +08:00
} ) ,
2023-11-21 23:24:20 +08:00
inlineImagesPlugin ( client ) ,
2024-02-23 17:12:48 +08:00
codeCopyPlugin ( client ) ,
2023-07-14 19:58:16 +08:00
highlightSpecialChars ( ) ,
history ( ) ,
dropCursor ( ) ,
codeFolding ( {
placeholderText : "…" ,
} ) ,
indentOnInput ( ) ,
2023-11-21 23:24:20 +08:00
. . . cleanModePlugins ( client ) ,
2023-07-14 19:58:16 +08:00
EditorView . lineWrapping ,
2023-11-21 23:24:20 +08:00
plugLinter ( client ) ,
2024-07-04 13:05:58 +08:00
drawSelection ( ) ,
2023-11-25 20:40:56 +08:00
postScriptPrefacePlugin ( client ) ,
2023-07-14 19:58:16 +08:00
lineWrapper ( [
{ selector : "ATXHeading1" , class : "sb-line-h1" } ,
{ selector : "ATXHeading2" , class : "sb-line-h2" } ,
{ selector : "ATXHeading3" , class : "sb-line-h3" } ,
{ selector : "ATXHeading4" , class : "sb-line-h4" } ,
2024-05-14 19:24:33 +08:00
{ selector : "ATXHeading5" , class : "sb-line-h5" } ,
{ selector : "ATXHeading6" , class : "sb-line-h6" } ,
2023-07-14 19:58:16 +08:00
{ selector : "ListItem" , class : "sb-line-li" , nesting : true } ,
{ selector : "Blockquote" , class : "sb-line-blockquote" } ,
{ selector : "Task" , class : "sb-line-task" } ,
{ selector : "CodeBlock" , class : "sb-line-code" } ,
{
selector : "FencedCode" ,
class : "sb-line-fenced-code" ,
disableSpellCheck : true ,
} ,
{ selector : "Comment" , class : "sb-line-comment" } ,
{ selector : "BulletList" , class : "sb-line-ul" } ,
{ selector : "OrderedList" , class : "sb-line-ol" } ,
{ selector : "TableHeader" , class : "sb-line-tbl-header" } ,
{
2024-01-21 02:16:07 +08:00
selector : "FrontMatter" ,
class : "sb-frontmatter" ,
disableSpellCheck : true ,
2023-07-14 19:58:16 +08:00
} ,
] ) ,
2024-01-21 02:16:07 +08:00
keyBindings ,
2023-07-14 19:58:16 +08:00
EditorView . domEventHandlers ( {
// This may result in duplicated touch events on mobile devices
touchmove : ( ) = > {
touchCount ++ ;
} ,
touchend : ( event : TouchEvent , view : EditorView ) = > {
if ( touchCount === 0 ) {
safeRun ( async ( ) = > {
const touch = event . changedTouches . item ( 0 ) ! ;
if ( ! event . altKey && event . target instanceof Element ) {
// prevent the browser from opening the link twice
const parentA = event . target . closest ( "a" ) ;
if ( parentA ) {
event . preventDefault ( ) ;
}
}
2023-11-29 23:50:53 +08:00
const pos = view . posAtCoords ( {
x : touch.clientX ,
y : touch.clientY ,
} ) ! ;
const potentialClickEvent : ClickEvent = {
2023-07-14 19:58:16 +08:00
page : pageName ,
ctrlKey : event.ctrlKey ,
metaKey : event.metaKey ,
altKey : event.altKey ,
2023-11-29 23:50:53 +08:00
pos : pos ,
2023-07-14 19:58:16 +08:00
} ;
2023-11-29 23:50:53 +08:00
const distanceX = touch . clientX - view . coordsAtPos ( pos ) ! . left ;
// What we're trying to determine here is if the tap occured anywhere near the looked up position
// this may not be the case with locations that expand signifcantly based on live preview (such as links), we don't want any accidental clicks
// Fixes #585
//
if ( distanceX <= view . defaultCharacterWidth ) {
await client . dispatchAppEvent (
"page:click" ,
potentialClickEvent ,
) ;
}
2023-07-14 19:58:16 +08:00
} ) ;
}
touchCount = 0 ;
} ,
2024-07-04 01:13:54 +08:00
click : ( event : MouseEvent , view : EditorView ) = > {
2024-01-14 01:42:40 +08:00
const pos = view . posAtCoords ( event ) ;
if ( event . button !== 0 ) {
return ;
}
if ( ! pos ) {
return ;
}
2023-07-14 19:58:16 +08:00
safeRun ( async ( ) = > {
const potentialClickEvent : ClickEvent = {
page : pageName ,
ctrlKey : event.ctrlKey ,
metaKey : event.metaKey ,
altKey : event.altKey ,
pos : view.posAtCoords ( {
x : event.x ,
y : event.y ,
} ) ! ,
} ;
// Make sure <a> tags are clicked without moving the cursor there
if ( ! event . altKey && event . target instanceof Element ) {
const parentA = event . target . closest ( "a" ) ;
if ( parentA ) {
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
2023-11-21 23:24:20 +08:00
await client . dispatchAppEvent (
2023-07-14 19:58:16 +08:00
"page:click" ,
potentialClickEvent ,
) ;
return ;
}
}
const distanceX = event . x - view . coordsAtPos ( pos ) ! . left ;
// What we're trying to determine here is if the click occured anywhere near the looked up position
// this may not be the case with locations that expand signifcantly based on live preview (such as links), we don't want any accidental clicks
// Fixes #357
if ( distanceX <= view . defaultCharacterWidth ) {
2023-11-21 23:24:20 +08:00
await client . dispatchAppEvent ( "page:click" , potentialClickEvent ) ;
2023-07-14 19:58:16 +08:00
}
} ) ;
} ,
} ) ,
ViewPlugin . fromClass (
class {
update ( update : ViewUpdate ) : void {
if ( update . docChanged ) {
2023-08-16 21:15:19 +08:00
const changes : TextChange [ ] = [ ] ;
update . changes . iterChanges ( ( fromA , toA , fromB , toB , inserted ) = >
changes . push ( {
inserted : inserted.toString ( ) ,
oldRange : { from : fromA , to : toA } ,
newRange : { from : fromB , to : toB } ,
} )
) ;
2023-11-21 23:24:20 +08:00
client . dispatchAppEvent ( "editor:pageModified" , { changes } ) ;
client . ui . viewDispatch ( { type : "page-changed" } ) ;
client . debouncedUpdateEvent ( ) ;
client . save ( ) . catch ( ( e ) = > console . error ( "Error saving" , e ) ) ;
2023-07-14 19:58:16 +08:00
}
}
} ,
) ,
pasteLinkExtension ,
2023-11-21 23:24:20 +08:00
attachmentExtension ( client ) ,
2023-07-14 19:58:16 +08:00
closeBrackets ( ) ,
] ,
} ) ;
}
2024-01-21 02:16:07 +08:00
2024-01-24 21:03:14 +08:00
export function createCommandKeyBindings ( client : Client ) : KeyBinding [ ] {
2024-01-21 02:16:07 +08:00
const commandKeyBindings : KeyBinding [ ] = [ ] ;
// Track which keyboard shortcuts for which commands we've overridden, so we can skip them later
const overriddenCommands = new Set < string > ( ) ;
// Keyboard shortcuts from SETTINGS take precedense
if ( client . settings ? . shortcuts ) {
for ( const shortcut of client . settings . shortcuts ) {
// Figure out if we're using the command link syntax here, if so: parse it out
2024-01-25 18:42:36 +08:00
const parsedCommand = parseCommand ( shortcut . command ) ;
if ( parsedCommand . args . length === 0 ) {
2024-01-21 02:16:07 +08:00
// If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set:
2024-01-25 18:42:36 +08:00
overriddenCommands . add ( parsedCommand . name ) ;
2024-01-21 02:16:07 +08:00
}
commandKeyBindings . push ( {
key : shortcut.key ,
mac : shortcut.mac ,
run : ( ) : boolean = > {
2024-01-25 18:42:36 +08:00
client . runCommandByName ( parsedCommand . name , parsedCommand . args ) . catch (
( e : any ) = > {
console . error ( e ) ;
client . flashNotification (
` Error running command: ${ e . message } ` ,
"error" ,
) ;
} ,
2024-01-26 02:46:08 +08:00
) . then ( ( returnValue : any ) = > {
2024-01-21 02:16:07 +08:00
// Always be focusing the editor after running a command
2024-01-26 02:46:08 +08:00
if ( returnValue !== false ) {
client . focus ( ) ;
}
2024-01-21 02:16:07 +08:00
} ) ;
return true ;
} ,
} ) ;
}
}
// Then add bindings for plug commands
2024-02-07 21:50:01 +08:00
for ( const def of client . clientSystem . commandHook . editorCommands . values ( ) ) {
2024-01-21 02:16:07 +08:00
if ( def . command . key ) {
// If we've already overridden this command, skip it
2024-02-23 06:26:31 +08:00
if ( overriddenCommands . has ( def . command . name ) ) {
2024-01-21 02:16:07 +08:00
continue ;
}
commandKeyBindings . push ( {
key : def.command.key ,
mac : def.command.mac ,
run : ( ) : boolean = > {
if ( def . command . contexts ) {
const context = client . getContext ( ) ;
if ( ! context || ! def . command . contexts . includes ( context ) ) {
return false ;
}
}
Promise . resolve ( [ ] )
. then ( def . run )
. catch ( ( e : any ) = > {
console . error ( e ) ;
client . flashNotification (
` Error running command: ${ e . message } ` ,
"error" ,
) ;
2024-01-26 02:46:08 +08:00
} ) . then ( ( returnValue : any ) = > {
2024-01-21 02:16:07 +08:00
// Always be focusing the editor after running a command
2024-01-26 02:46:08 +08:00
if ( returnValue !== false ) {
client . focus ( ) ;
}
2024-01-21 02:16:07 +08:00
} ) ;
2024-01-26 02:46:08 +08:00
2024-01-21 02:16:07 +08:00
return true ;
} ,
} ) ;
}
}
2024-01-24 21:03:14 +08:00
return commandKeyBindings ;
}
export function createKeyBindings ( client : Client ) : Extension {
2024-01-21 02:16:07 +08:00
return keymap . of ( [
2024-01-24 21:03:14 +08:00
. . . createCommandKeyBindings ( client ) ,
2024-07-20 01:08:49 +08:00
. . . createSmartQuoteKeyBindings ( client ) ,
2024-01-21 02:16:07 +08:00
. . . closeBracketsKeymap ,
. . . standardKeymap ,
. . . completionKeymap ,
indentWithTab ,
] ) ;
}