From 6d0c3e23372bbf915015e7c5d8fa46bffc3354c4 Mon Sep 17 00:00:00 2001 From: Macdara Date: Wed, 25 Mar 2026 13:33:06 +0000 Subject: [PATCH] feat: add code autocomplete to CodeMirror editor Adds Ctrl+Space autocomplete using CodeMirror show-hint addon. Provides completions from user-defined snippets and document words. Fixes #2567 IssueHunt: https://oss.issuehunt.io/r/BoostIo/Boostnote/issues/2567 --- browser/components/CodeEditor.js | 75 ++++++++++++++++++++++++++++++ browser/components/CodeEditor.styl | 31 ++++++++++++ 2 files changed, 106 insertions(+) diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 27ee4271e..488e86a86 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,6 +2,9 @@ import PropTypes from 'prop-types' import React from 'react' import _ from 'lodash' import CodeMirror from 'codemirror' +import 'codemirror/addon/hint/show-hint' +import 'codemirror/addon/hint/show-hint.css' +import 'codemirror/addon/hint/anyword-hint' import hljs from 'highlight.js' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' @@ -233,6 +236,17 @@ export default class CodeEditor extends React.Component { 'Cmd-T': function(cm) { // Do nothing }, + 'Ctrl-Space': function(cm) { + const hints = self.getSnippetHints(cm) + if (hints) { + cm.showHint({ + hint: function() { + return self.getSnippetHints(cm) + }, + completeSingle: false + }) + } + }, [translateHotkey(hotkey.insertDate)]: function(cm) { const dateNow = new Date() if (self.props.dateFormatISO8601) { @@ -569,6 +583,67 @@ export default class CodeEditor extends React.Component { } } + getSnippetHints(cm) { + const cursor = cm.getCursor() + const line = cm.getLine(cursor.line) + const end = cursor.ch + let start = end + + while (start > 0 && /\w/.test(line.charAt(start - 1))) { + start-- + } + + const currentWord = line.slice(start, end) + if (currentWord.length === 0) { + return null + } + + const lowerWord = currentWord.toLowerCase() + const list = [] + const seen = new Set() + + const snippets = snippetManager.snippets || [] + for (let i = 0; i < snippets.length; i++) { + const snippet = snippets[i] + const prefixes = snippet.prefix || [] + for (let j = 0; j < prefixes.length; j++) { + const prefix = prefixes[j] + if ( + prefix.toLowerCase().indexOf(lowerWord) === 0 && + !seen.has(prefix) + ) { + seen.add(prefix) + list.push({ + text: snippet.content.replace(/:\{\}/g, ''), + displayText: prefix + ' (snippet: ' + snippet.name + ')', + className: 'snippet-hint' + }) + } + } + } + + const anywordResult = CodeMirror.hint.anyword(cm) + if (anywordResult && anywordResult.list) { + for (let i = 0; i < anywordResult.list.length; i++) { + const word = anywordResult.list[i] + if (!seen.has(word)) { + seen.add(word) + list.push(word) + } + } + } + + if (list.length === 0) { + return null + } + + return { + list: list, + from: CodeMirror.Pos(cursor.line, start), + to: CodeMirror.Pos(cursor.line, end) + } + } + quitEditor() { document.querySelector('textarea').blur() } diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl index 1aa0e466b..bb4f0fc09 100644 --- a/browser/components/CodeEditor.styl +++ b/browser/components/CodeEditor.styl @@ -3,3 +3,34 @@ .spellcheck-select border: none + +.CodeMirror-hints + position absolute + z-index 100 + overflow hidden + list-style none + margin 0 + padding 2px + box-shadow 2px 3px 5px rgba(0, 0, 0, 0.2) + border-radius 3px + border 1px solid silver + background white + font-size 90% + font-family monospace + max-height 20em + overflow-y auto + +.CodeMirror-hint + margin 0 + padding 0 4px + border-radius 2px + white-space pre + color black + cursor pointer + +li.CodeMirror-hint-active + background #08f + color white + +.snippet-hint + font-style italic