diff --git a/src/renderer/plugins.ts b/src/renderer/plugins.ts index c9532dc96..4b9a9b2e1 100644 --- a/src/renderer/plugins.ts +++ b/src/renderer/plugins.ts @@ -17,6 +17,7 @@ import statusBarGet from '@fe/plugins/status-bar-get' import editorPaste from '@fe/plugins/editor-paste' import editorAttachment from '@fe/plugins/editor-attachment' import editorMarkdown from '@fe/plugins/editor-markdown' +import editorMdSyntax from '@fe/plugins/editor-md-syntax' import editorWords from '@fe/plugins/editor-words' import editorEmoji from '@fe/plugins/editor-emoji' import copyText from '@fe/plugins/copy-text' @@ -71,6 +72,7 @@ export default [ editorPaste, editorAttachment, editorMarkdown, + editorMdSyntax, editorEmoji, editorWords, copyText, diff --git a/src/renderer/plugins/editor-markdown.ts b/src/renderer/plugins/editor-markdown.ts index a1433f5b6..2c0a30eba 100644 --- a/src/renderer/plugins/editor-markdown.ts +++ b/src/renderer/plugins/editor-markdown.ts @@ -1,74 +1,12 @@ /* eslint-disable no-template-curly-in-string */ import dayjs from 'dayjs' import type * as Monaco from 'monaco-editor' -import { deleteLine, getEditor, getLineContent, getMonaco, getOneIndent, insert, replaceLine, whenEditorReady } from '@fe/services/editor' +import { deleteLine, getEditor, getLineContent, getOneIndent, insert, replaceLine, whenEditorReady } from '@fe/services/editor' import type { Plugin } from '@fe/context' import { t } from '@fe/services/i18n' import { getSetting } from '@fe/services/setting' import { isKeydown } from '@fe/core/command' -function createDependencyProposals (range: Monaco.IRange): Monaco.languages.CompletionItem[] { - const monaco = getMonaco() - - const replaceRange = { ...range, endColumn: range.startColumn + 1 } - - const result: Monaco.languages.CompletionItem[] = [] - - ;[ - { name: '/ ![]() Image', insertText: '![${2:Img}]($1)' }, - { name: '/ []() Link', insertText: '[${2:Link}]($1)' }, - { name: '/ # Head', insertText: '# $1' }, - { name: '/ ## Head', insertText: '## $1' }, - { name: '/ ### Head', insertText: '### $1' }, - { name: '/ #### Head', insertText: '#### $1' }, - { name: '/ ##### Head', insertText: '##### $1' }, - { name: '/ ###### Head', insertText: '###### $1' }, - { name: '/ + List', insertText: '+ ' }, - { name: '/ - List', insertText: '- ' }, - { name: '/ ` Code', insertText: '`$1`' }, - { name: '/ * Italic', insertText: '*$1*' }, - { name: '/ _ Italic', insertText: '_$1_' }, - { name: '/ ~ Sub', insertText: '~$1~' }, - { name: '/ ^ Sup', insertText: '^$1^' }, - { name: '/ ** Bold', insertText: '**$1**' }, - { name: '/ __ Bold', insertText: '__$1__' }, - { name: '/ ~~ Delete', insertText: '~~$1~~' }, - { name: '/ == Mark', insertText: '==$1==' }, - { name: '/ + [ ] TODO List', insertText: '+ [ ] ' }, - { name: '/ - [ ] TODO List', insertText: '- [ ] ' }, - { name: '/ ```', insertText: '```$1\n```\n' }, - { name: '/ [toc]', insertText: '[toc]{type: "${1|ul,ol|}", level: [2,3]}' }, - { name: '/ + MindMap', insertText: '+ ${1:Subject}{.mindmap}\n + ${2:Topic}' }, - { name: '/ $ Inline KaTeX', insertText: '$$1$' }, - { name: '/ $$ Block KaTeX', insertText: '$$$1$$\n' }, - { name: '/ ``` ECharts', insertText: '```js\n// --echarts-- \nchart => chart.setOption({\n xAxis: {\n type: "category",\n data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]\n },\n yAxis: {\n type: "value"\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: "line"\n }\n ]\n}, true)\n```\n' }, - { name: '/ ``` Run Code', insertText: '```js\n// --run--\n${1:await new Promise(r => setTimeout(r, 500))\nctx.ui.useToast().show("info", "HELLOWORLD!")\nconsole.log("hello world!")}\n```\n' }, - { name: '/ ``` Applet', insertText: '```html\n\n\n```\n' }, - { name: '/ ``` Mermaid', insertText: '```mermaid\ngraph LR\n${1:A[Hard] --> |Text| B(Round)}\n```\n' }, - { name: '/ @startuml Plantuml', insertText: '@startuml\n${1:a -> b}\n@enduml\n' }, - { name: '/ []() Drawio Link', insertText: '[${2:Drawio}]($1){link-type="drawio"}' }, - { name: '/ []() Luckysheet Link', insertText: '[${2:Luckysheet}]($1){link-type="luckysheet"}' }, - { name: '/ ||| Table', insertText: '| ${1:--} | ${2:--} | ${3:--} |\n| -- | -- | -- |\n| -- | -- | -- |' }, - { name: '/ ||| Small Table', insertText: '| ${1:--} | ${2:--} | ${3:--} |\n| -- | -- | -- |\n| -- | -- | -- |\n{.small}' }, - { name: '/ [= Macro', insertText: '[= ${1:1+1} =]' }, - { name: '/ --- Horizontal Line', insertText: '---\n' }, - { name: '/ --- Front Matter', insertText: '---\nheadingNumber: true\nenableMacro: true\nmdOptions: { linkify: true, breaks: true }\ndefine:\n APP_NAME: Yank Note\n---\n' }, - { name: '/ ::: Container', insertText: '${1|:::,::::,:::::|} ${2|tip,warning,danger,details,group,group-item|} ${3:Title}\n${4:Content}\n${1|:::,::::,:::::|}\n' }, - { name: '/ ::: Group Container', insertText: ':::: group ${1:Title}\n::: group-item Tab 1\ntest 1\n:::\n::: group-item *Tab 2\ntest 2\n:::\n::: group-item Tab 3\ntest 3\n:::\n::::\n' }, - ].forEach((item, i) => { - result.push({ - label: { label: item.name }, - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: item.insertText, - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range: replaceRange, - sortText: (20000 + i).toString() - }) - }) - - return result -} - function processCursorChange (source: string, position: Monaco.Position) { const isEnter = source === 'keyboard' && isKeydown('ENTER') const isTab = source === 'tab' @@ -206,71 +144,6 @@ export default { editor.onDidCompositionEnd(() => { ctx.store.commit('setInComposition', false) }) - - monaco.languages.setLanguageConfiguration('markdown', { - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '<', close: '>' }, - { open: '`', close: '`' }, - { open: "'", close: "'" }, - { open: '"', close: '"' }, - { open: '*', close: '*' }, - { open: '_', close: '_' }, - { open: '=', close: '=' }, - { open: '~', close: '~' }, - { open: '^', close: '^' }, - { open: '#', close: '#' }, - { open: '$', close: '$' }, - { open: '《', close: '》' }, - { open: '【', close: '】' }, - { open: '「', close: '」' }, - { open: '(', close: ')' }, - { open: '“', close: '”' }, - ], - onEnterRules: [ - { beforeText: /^\s*> .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '> ' } }, - { beforeText: /^\s*\+ \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ [ ] ' } }, - { beforeText: /^\s*- \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- [ ] ' } }, - { beforeText: /^\s*\* \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* [ ] ' } }, - { beforeText: /^\s*\+ \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ [ ] ' } }, - { beforeText: /^\s*- \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- [ ] ' } }, - { beforeText: /^\s*\* \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* [ ] ' } }, - { beforeText: /^\s*\+ .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ ' } }, - { beforeText: /^\s*- .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- ' } }, - { beforeText: /^\s*\* .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* ' } }, - { beforeText: /^\s*\d+\. .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '1. ' } }, - { beforeText: /^\s*\d+\) .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '1) ' } }, - ] - }) - - monaco.languages.registerCompletionItemProvider('markdown', { - triggerCharacters: Array(93).fill(undefined).map((_, i) => String.fromCharCode(i + 33)).concat(['~']), - provideCompletionItems: (model, position) => { - const lineContent = model.getLineContent(position.lineNumber) - let startColumn = lineContent.substring(0, position.column).lastIndexOf(' ') + 1 - if (startColumn === position.column) { - startColumn = 0 - } - - let endColumn = lineContent.substring(position.column - 1).indexOf(' ') + position.column - if (endColumn < position.column) { - endColumn = lineContent.length + 1 - } - - const range: Monaco.IRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: startColumn + 1, - endColumn: endColumn - } - - return { - suggestions: createDependencyProposals(range, model, position) - } - } - }) }) ctx.statusBar.tapMenus(menus => { diff --git a/src/renderer/plugins/editor-md-syntax.ts b/src/renderer/plugins/editor-md-syntax.ts new file mode 100644 index 000000000..2f45d5af1 --- /dev/null +++ b/src/renderer/plugins/editor-md-syntax.ts @@ -0,0 +1,146 @@ +/* eslint-disable no-template-curly-in-string */ +import type * as Monaco from 'monaco-editor' +import type { Ctx, Plugin } from '@fe/context' + +const surroundingPairs = [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '<', close: '>' }, + { open: '`', close: '`' }, + { open: "'", close: "'" }, + { open: '"', close: '"' }, + { open: '*', close: '*' }, + { open: '_', close: '_' }, + { open: '=', close: '=' }, + { open: '~', close: '~' }, + { open: '^', close: '^' }, + { open: '#', close: '#' }, + { open: '$', close: '$' }, + { open: '《', close: '》' }, + { open: '【', close: '】' }, + { open: '「', close: '」' }, + { open: '(', close: ')' }, + { open: '“', close: '”' }, +] + +class MdSyntaxCompletionProvider implements Monaco.languages.CompletionItemProvider { + triggerCharacters = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('') + + private readonly monaco: typeof Monaco + private readonly ctx: Ctx + + private readonly list = [ + { name: '/ ![]() Image', insertText: '![${2:Img}]($1)' }, + { name: '/ []() Link', insertText: '[${2:Link}]($1)' }, + { name: '/ # Head 1', insertText: '# $1' }, + { name: '/ ## Head 2', insertText: '## $1' }, + { name: '/ ### Head 3', insertText: '### $1' }, + { name: '/ #### Head 4', insertText: '#### $1' }, + { name: '/ ##### Head 5', insertText: '##### $1' }, + { name: '/ ###### Head 6', insertText: '###### $1' }, + { name: '/ + List', insertText: '+ ' }, + { name: '/ - List', insertText: '- ' }, + { name: '/ ` Code', insertText: '`$1`' }, + { name: '/ * Italic', insertText: '*$1*' }, + { name: '/ _ Italic', insertText: '_$1_' }, + { name: '/ ~ Sub', insertText: '~$1~' }, + { name: '/ ^ Sup', insertText: '^$1^' }, + { name: '/ ** Bold', insertText: '**$1**' }, + { name: '/ __ Bold', insertText: '__$1__' }, + { name: '/ ~~ Delete', insertText: '~~$1~~' }, + { name: '/ == Mark', insertText: '==$1==' }, + { name: '/ + [ ] TODO List', insertText: '+ [ ] ' }, + { name: '/ - [ ] TODO List', insertText: '- [ ] ' }, + { name: '/ ```', insertText: '```$1\n```\n' }, + { name: '/ [toc]', insertText: '[toc]{type: "${1|ul,ol|}", level: [2,3]}' }, + { name: '/ + MindMap', insertText: '+ ${1:Subject}{.mindmap}\n + ${2:Topic}' }, + { name: '/ $ Inline KaTeX', insertText: '$$1$' }, + { name: '/ $$ Block KaTeX', insertText: '$$$1$$\n' }, + { name: '/ ``` ECharts', insertText: '```js\n// --echarts-- \nchart => chart.setOption({\n xAxis: {\n type: "category",\n data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]\n },\n yAxis: {\n type: "value"\n },\n series: [\n {\n data: [150, 230, 224, 218, 135, 147, 260],\n type: "line"\n }\n ]\n}, true)\n```\n' }, + { name: '/ ``` Run Code', insertText: '```js\n// --run--\n${1:await new Promise(r => setTimeout(r, 500))\nctx.ui.useToast().show("info", "HELLOWORLD!")\nconsole.log("hello world!")}\n```\n' }, + { name: '/ ``` Applet', insertText: '```html\n\n\n```\n' }, + { name: '/ ``` Mermaid', insertText: '```mermaid\ngraph LR\n${1:A[Hard] --> |Text| B(Round)}\n```\n' }, + { name: '/ @startuml Plantuml', insertText: '@startuml\n${1:a -> b}\n@enduml\n' }, + { name: '/ []() Drawio Link', insertText: '[${2:Drawio}]($1){link-type="drawio"}' }, + { name: '/ []() Luckysheet Link', insertText: '[${2:Luckysheet}]($1){link-type="luckysheet"}' }, + { name: '/ ||| Table', insertText: '| ${1:--} | ${2:--} | ${3:--} |\n| -- | -- | -- |\n| -- | -- | -- |' }, + { name: '/ ||| Small Table', insertText: '| ${1:--} | ${2:--} | ${3:--} |\n| -- | -- | -- |\n| -- | -- | -- |\n{.small}' }, + { name: '/ [= Macro', insertText: '[= ${1:1+1} =]' }, + { name: '/ --- Horizontal Line', insertText: '---\n' }, + { name: '/ --- Front Matter', insertText: '---\nheadingNumber: true\nenableMacro: true\nmdOptions: { linkify: true, breaks: true }\ndefine:\n APP_NAME: Yank Note\n---\n' }, + { name: '/ ::: Container', insertText: '${1|:::,::::,:::::|} ${2|tip,warning,danger,details,group,group-item|} ${3:Title}\n${4:Content}\n${1|:::,::::,:::::|}\n' }, + { name: '/ ::: Group Container', insertText: ':::: group ${1:Title}\n::: group-item Tab 1\ntest 1\n:::\n::: group-item *Tab 2\ntest 2\n:::\n::: group-item Tab 3\ntest 3\n:::\n::::\n' }, + ] + + private readonly pairsMap = new Map(surroundingPairs.map(x => [x.open, x.close])) + + constructor (monaco: typeof Monaco, ctx: Ctx) { + this.monaco = monaco + this.ctx = ctx + } + + public async provideCompletionItems (model: Monaco.editor.IModel, position: Monaco.Position): Promise { + const line = model.getLineContent(position.lineNumber) + const cursor = position.column - 1 + const linePrefixText = line.slice(0, cursor) + + let startColumn = linePrefixText.lastIndexOf(' ') + 1 + if (startColumn === position.column) { + startColumn = 0 + } + + const range = new this.monaco.Range( + position.lineNumber, + startColumn, + position.lineNumber, + // remove auto surrounding pairs + this.pairsMap.get(line.charAt(cursor - 1)) === line.charAt(cursor) + ? position.column + 1 + : position.column, + ) + + const result: Monaco.languages.CompletionItem[] = this.list.map((item, i) => ( + { + label: { label: item.name }, + kind: this.monaco.languages.CompletionItemKind.Keyword, + insertText: item.insertText, + insertTextRules: this.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + sortText: i.toString().padStart(7), + } + )) + + return { suggestions: result } + } +} + +export default { + name: 'editor-md-syntax', + register: (ctx) => { + ctx.editor.whenEditorReady().then(({ monaco }) => { + monaco.languages.registerCompletionItemProvider( + 'markdown', + new MdSyntaxCompletionProvider(monaco, ctx) + ) + + monaco.languages.setLanguageConfiguration('markdown', { + surroundingPairs, + onEnterRules: [ + { beforeText: /^\s*> .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '> ' } }, + { beforeText: /^\s*\+ \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ [ ] ' } }, + { beforeText: /^\s*- \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- [ ] ' } }, + { beforeText: /^\s*\* \[ \] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* [ ] ' } }, + { beforeText: /^\s*\+ \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ [ ] ' } }, + { beforeText: /^\s*- \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- [ ] ' } }, + { beforeText: /^\s*\* \[x\] .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* [ ] ' } }, + { beforeText: /^\s*\+ .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '+ ' } }, + { beforeText: /^\s*- .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '- ' } }, + { beforeText: /^\s*\* .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '* ' } }, + { beforeText: /^\s*\d+\. .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '1. ' } }, + { beforeText: /^\s*\d+\) .*$/, action: { indentAction: monaco.languages.IndentAction.None, appendText: '1) ' } }, + ] + }) + }) + } +} as Plugin