diff --git a/docs/PREFERENCES.md b/docs/PREFERENCES.md index ae812759..8028bbd0 100644 --- a/docs/PREFERENCES.md +++ b/docs/PREFERENCES.md @@ -19,21 +19,23 @@ Preferences can be controlled and modified in the settings window or via the `pr #### Editor -| Key | Type | Defaut | Description | -| ------------------------ | ------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| fontSize | Number | 16 | Font size in pixels. 12 ~ 32 | -| editorFontFamily | String | Open Sans | Font Family | -| lineHeight | Number | 1.6 | Line Height | -| autoPairBracket | Boolean | true | Automatically brackets when editing | -| autoPairMarkdownSyntax | Boolean | true | Autocomplete markdown syntax | -| autoPairQuote | Boolean | true | Automatic completion of quotes | -| endOfLine | String | default | The newline character used at the end of each line. The default value is default, which will be selected according to your system intelligence. `lf` `crlf` `default` | -| textDirection | String | ltr | The writing text direction, optional value: `ltr` or `rtl` | -| codeFontSize | Number | 14 | Font size on code block, the range is 12 ~ 28 | -| codeFontFamily | String | `DejaVu Sans Mono` | Code font family | -| trimUnnecessaryCodeBlockEmptyLines | Boolean | true | Whether to trim the beginning and end empty line in Code block | -| hideQuickInsertHint | Boolean | false | Hide hint for quickly creating paragraphs | -| imageDropAction | String | folder | The default behavior after paste or drag the image to Mark Text, upload it to the image cloud (if configured), move to the specified folder, insert the path | +| Key | Type | Defaut | Description | +| ---------------------- | ------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fontSize | Number | 16 | Font size in pixels. 12 ~ 32 | +| editorFontFamily | String | Open Sans | Font Family | +| lineHeight | Number | 1.6 | Line Height | +| autoPairBracket | Boolean | true | Automatically brackets when editing | +| autoPairMarkdownSyntax | Boolean | true | Autocomplete markdown syntax | +| autoPairQuote | Boolean | true | Automatic completion of quotes | +| endOfLine | String | default | The newline character used at the end of each line. The default value is default, which will be selected according to your system intelligence. `lf` `crlf` `default` | +| textDirection | String | ltr | The writing text direction, optional value: `ltr` or `rtl` | +| codeFontSize | Number | 14 | Font size on code block, the range is 12 ~ 28 | +| codeFontFamily | String | `DejaVu Sans Mono` | Code font family | +| trimUnnecessaryCodeBlockEmptyLines | Boolean | true | Whether to trim the beginning and end empty line in Code block | +| hideQuickInsertHint | Boolean | false | Hide hint for quickly creating paragraphs | +| imageDropAction | String | folder | The default behavior after paste or drag the image to Mark Text, upload it to the image cloud (if configured), move to the specified folder, insert the path | +| defaultEncoding | String | `utf8` | The default file encoding | +| autoGuessEncoding | Boolean | true | Try to automatically guess the file encoding when opening files | #### Markdown @@ -46,9 +48,17 @@ Preferences can be controlled and modified in the settings window or via the `pr | tabSize | Number | 4 | The number of spaces a tab is equal to | | listIndentation | String | 1 | The list indentation of sub list items or paragraphs, optional value `dfm`, `tab` or number 1~4 | | frontmatterType | String | `-` | The frontmatter type: `-` (YAML), `+` (TOML), `;` (JSON) or `{` (JSON) | +#### Theme +| Key | Type | Default | Description | +| ----- | ------ | ------- | --------------------------------------------------------------------- | +| theme | String | light | `dark`, `graphite`, `material-dark`, `one-dark`, `light` or `ulysses` | -#### View +#### Editable via file + +These entires don't have a settings option and need to be changed manually. + +##### View | Key | Type | Default | Description | | ----------------------------- | ------- | ------- | -------------------------------------------------- | @@ -58,8 +68,13 @@ Preferences can be controlled and modified in the settings window or via the `pr \*: These options are default/fallback values that are used if not session is loaded and are overwritten by the menu entries. -#### Theme +##### File system -| Key | Type | Default | Description | -| ----- | ------ | ------- | --------------------------------------------------------------------- | -| theme | String | light | `dark`, `graphite`, `material-dark`, `one-dark`, `light` or `ulysses` | +| Key | Type | Default | Description | +| -------------------- | ---------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| searchExclusions | Array of Strings | `[]` | The filename exclusions for the file searcher. Default: `'*.markdown', '*.mdown', '*.mkdn', '*.md', '*.mkd', '*.mdwn', '*.mdtxt', '*.mdtext', '*.text', '*.txt'` | +| searchMaxFileSize | String | `""` | The maximum file size to search in (e.g. 50K or 10MB). Default: unlimited | +| searchIncludeHidden | Boolean | false | Search hidden files and directories | +| searchNoIgnore | Boolean | false | Don't respect ignore files such as `.gitignore`. | +| searchFollowSymlinks | Boolean | true | Whether to follow symbolic links. | +| watcherUsePolling | Boolean | false | Whether to use polling to receive file changes. Polling may leads to high CPU utilization. | diff --git a/package.json b/package.json index f3e09cc5..95eebb2e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@octokit/rest": "^16.30.1", "arg": "^4.1.1", "axios": "^0.19.0", + "ced": "^1.0.0", "chokidar": "^3.2.1", "codemirror": "^5.49.0", "command-exists": "^1.2.8", @@ -58,6 +59,7 @@ "fuzzaldrin": "^2.1.0", "github-markdown-css": "^3.0.1", "html-tags": "^3.0.0", + "iconv-lite": "^0.5.0", "joplin-turndown-plugin-gfm": "^1.0.9", "katex": "^0.11.1", "keyboard-layout": "^2.0.16", diff --git a/src/common/encoding.js b/src/common/encoding.js new file mode 100644 index 00000000..0d29b6da --- /dev/null +++ b/src/common/encoding.js @@ -0,0 +1,52 @@ +export const ENCODING_NAME_MAP = { + utf8: 'UTF-8', + utf16be: 'UTF-16 BE', + utf16le: 'UTF-16 LE', + utf32be: 'UTF-32 BE', + utf32le: 'UTF-32 LE', + ascii: 'Western (ISO 8859-1)', + latin3: 'Western (ISO 8859-3)', + iso885915: 'Western (ISO 8859-15)', + cp1252: 'Western (Windows 1252)', + arabic: 'Arabic (ISO 8859-6)', + cp1256: 'Arabic (Windows 1256)', + latin4: 'Baltic (ISO 8859-4)', + cp1257: 'Baltic (Windows 1257)', + iso88592: 'Central European (ISO 8859-2)', + windows1250: 'Central European (Windows 1250)', + cp866: 'Cyrillic (CP 866)', + iso88595: 'Cyrillic (ISO 8859-5)', + koi8r: 'Cyrillic (KOI8-R)', + koi8u: 'Cyrillic (KOI8-U)', + cp1251: 'Cyrillic (Windows 1251)', + iso885913: 'Estonian (ISO 8859-13)', + greek: 'Greek (ISO 8859-7)', + cp1253: 'Greek (Windows 1253)', + hebrew: 'Hebrew (ISO 8859-8)', + cp1255: 'Hebrew (Windows 1255)', + latin5: 'Turkish (ISO 8859-9)', + cp1254: 'Turkish (Windows 1254)', + gb2312: 'Simplified Chinese (GB2312)', + gb18030: 'Simplified Chinese (GB18030)', + gbk: 'Simplified Chinese (GBK)', + big5: 'Traditional Chinese (Big5)', + big5hkscs: 'Traditional Chinese (Big5-HKSCS)', + shiftjis: 'Japanese (Shift JIS)', + eucjp: 'Japanese (EUC-JP)', + euckr: 'Korean (EUC-KR)', + latin6: 'Nordic (ISO 8859-10)' +} + +/** + * Try to translate the encoding. + * + * @param {Encoding} enc The encoding object. + */ +export const getEncodingName = enc => { + const { encoding, isBom } = enc + let str = ENCODING_NAME_MAP[encoding] || encoding + if (isBom) { + str += ' with BOM' + } + return str +} diff --git a/src/main/config.js b/src/main/config.js index ac8990a6..f1f28771 100644 --- a/src/main/config.js +++ b/src/main/config.js @@ -39,6 +39,7 @@ export const defaultPreferenceWinOptions = { export const PANDOC_EXTENSIONS = [ 'html', 'docx', + 'odt', 'latex', 'tex', 'ltx', diff --git a/src/main/filesystem/encoding.js b/src/main/filesystem/encoding.js new file mode 100644 index 00000000..cdec2195 --- /dev/null +++ b/src/main/filesystem/encoding.js @@ -0,0 +1,74 @@ +import ced from 'ced' + +const CED_ICONV_ENCODINGS = { + 'BIG5-CP950': 'big5', + KSC: 'euckr', + 'ISO-2022-KR': 'euckr', + GB: 'gb2312', + ISO_2022_CN: 'gb2312', + JIS: 'shiftjis', + SJS: 'shiftjis', + Unicode: 'utf8', + + // Map ASCII to UTF-8 + 'ASCII-7-bit': 'utf8', + ASCII: 'utf8', + MACINTOSH: 'utf8' +} + +// Byte Order Mark's to detect endianness and encoding. +const BOM_ENCODINGS = { + utf8: [0xEF, 0xBB, 0xBF], + utf16be: [0xFE, 0xFF], + utf16le: [0xFF, 0xFE] +} + +const checkSequence = (buffer, sequence) => { + if (buffer.length < sequence.length) { + return false + } + return sequence.every((v, i) => v === buffer[i]) +} + +/** + * Guess the encoding from the buffer. + * + * @param {Buffer} buffer + * @param {boolean} autoGuessEncoding + * @returns {Encoding} + */ +export const guessEncoding = (buffer, autoGuessEncoding) => { + let isBom = false + let encoding = 'utf8' + + // Detect UTF8- and UTF16-BOM encodings. + for (const [key, value] of Object.entries(BOM_ENCODINGS)) { + if (checkSequence(buffer, value)) { + return { encoding: key, isBom: true } + } + } + + // // Try to detect binary files. Text files should not containt four 0x00 characters. + // let zeroSeenCounter = 0 + // for (let i = 0; i < Math.min(buffer.byteLength, 256); ++i) { + // if (buffer[i] === 0x00) { + // if (zeroSeenCounter >= 3) { + // return { encoding: 'binary', isBom: false } + // } + // zeroSeenCounter++ + // } else { + // zeroSeenCounter = 0 + // } + // } + + // Auto guess encoding, otherwise use UTF8. + if (autoGuessEncoding) { + encoding = ced(buffer) + if (CED_ICONV_ENCODINGS[encoding]) { + encoding = CED_ICONV_ENCODINGS[encoding] + } else { + encoding = encoding.toLowerCase().replace(/-_/g, '') + } + } + return { encoding, isBom } +} diff --git a/src/main/filesystem/index.js b/src/main/filesystem/index.js index cbaa6f60..870738af 100644 --- a/src/main/filesystem/index.js +++ b/src/main/filesystem/index.js @@ -22,11 +22,11 @@ export const normalizeAndResolvePath = pathname => { return path.resolve(pathname) } -export const writeFile = (pathname, content, extension) => { +export const writeFile = (pathname, content, extension, options = 'utf-8') => { if (!pathname) { return Promise.reject(new Error('[ERROR] Cannot save file without path.')) } pathname = !extension || pathname.endsWith(extension) ? pathname : `${pathname}${extension}` - return fs.outputFile(pathname, content, 'utf-8') + return fs.outputFile(pathname, content, options) } diff --git a/src/main/filesystem/markdown.js b/src/main/filesystem/markdown.js index 0cdc3a0f..250cda0c 100644 --- a/src/main/filesystem/markdown.js +++ b/src/main/filesystem/markdown.js @@ -1,10 +1,12 @@ import fs from 'fs-extra' import path from 'path' import log from 'electron-log' +import iconv from 'iconv-lite' import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config' import { isDirectory } from 'common/filesystem' import { isMarkdownFileOrLink } from 'common/filesystem/paths' import { normalizeAndResolvePath, writeFile } from '../filesystem' +import { guessEncoding } from './encoding' const getLineEnding = lineEnding => { if (lineEnding === 'lf') { @@ -51,46 +53,47 @@ export const normalizeMarkdownPath = pathname => { * @param {IMarkdownDocumentOptions} options The markdown document options */ export const writeMarkdownFile = (pathname, content, options) => { - const { adjustLineEndingOnSave, encoding, lineEnding } = options + const { adjustLineEndingOnSave, lineEnding } = options + const { encoding, isBom } = options.encoding const extension = path.extname(pathname) || '.md' - if (encoding === 'utf8bom') { - content = '\uFEFF' + content - } - if (adjustLineEndingOnSave) { content = convertLineEndings(content, lineEnding) } + const buffer = iconv.encode(content, encoding, { addBOM: isBom }) + // TODO(@fxha): "safeSaveDocuments" using temporary file and rename syscall. - return writeFile(pathname, content, extension) + return writeFile(pathname, buffer, extension, undefined) } /** * Reads the contents of a markdown file. * * @param {string} pathname The path to the markdown file. - * @param {string} preferedEOL The prefered EOL. + * @param {string} preferedEol The prefered EOL. * @returns {IMarkdownDocumentRaw} Returns a raw markdown document. */ -export const loadMarkdownFile = async (pathname, preferedEOL) => { - let markdown = await fs.readFile(path.resolve(pathname), 'utf-8') +export const loadMarkdownFile = async (pathname, preferedEol, autoGuessEncoding = true) => { + // TODO: Use streams to not buffer the file multiple times and only guess + // encoding on the first 256/512 bytes. - // Check UTF-8 BOM (EF BB BF) encoding - const isUtf8BomEncoded = markdown.length >= 1 && markdown.charCodeAt(0) === 0xFEFF - if (isUtf8BomEncoded) { - markdown.splice(0, 1) + let buffer = await fs.readFile(path.resolve(pathname)) + + const encoding = guessEncoding(buffer, autoGuessEncoding) + const supported = iconv.encodingExists(encoding.encoding) + if (!supported) { + throw new Error(`"${encoding.encoding}" encoding is not supported.`) } - // TODO(@fxha): Check for more file encodings and whether the file is binary but for now expect UTF-8. - const encoding = isUtf8BomEncoded ? 'utf8bom' : 'utf8' + let markdown = iconv.decode(buffer, encoding.encoding) // Detect line ending const isLf = LF_LINE_ENDING_REG.test(markdown) const isCrlf = CRLF_LINE_ENDING_REG.test(markdown) const isMixedLineEndings = isLf && isCrlf const isUnknownEnding = !isLf && !isCrlf - let lineEnding = preferedEOL + let lineEnding = preferedEol if (isLf && !isCrlf) { lineEnding = 'lf' } else if (isCrlf && !isLf) { diff --git a/src/main/filesystem/watcher.js b/src/main/filesystem/watcher.js index 154a5f83..fa5a3696 100644 --- a/src/main/filesystem/watcher.js +++ b/src/main/filesystem/watcher.js @@ -18,7 +18,7 @@ const EVENT_NAME = { file: 'AGANI::update-file' } -const add = async (win, pathname, type, endOfLine) => { +const add = async (win, pathname, type, endOfLine, autoGuessEncoding) => { const stats = await fs.stat(pathname) const birthTime = stats.birthtime const isMarkdown = hasMarkdownExtension(pathname) @@ -33,7 +33,7 @@ const add = async (win, pathname, type, endOfLine) => { if (isMarkdown) { // HACK: But this should be removed completely in #1034/#1035. try { - const data = await loadMarkdownFile(pathname, endOfLine) + const data = await loadMarkdownFile(pathname, endOfLine, autoGuessEncoding) file.data = data } catch (err) { // Only notify user about opened files. @@ -61,7 +61,7 @@ const unlink = (win, pathname, type) => { }) } -const change = async (win, pathname, type, endOfLine) => { +const change = async (win, pathname, type, endOfLine, autoGuessEncoding) => { // No need to update the tree view if the file content has changed. if (type === 'dir') return @@ -70,7 +70,7 @@ const change = async (win, pathname, type, endOfLine) => { // HACK: Markdown data should be removed completely in #1034/#1035 and // should be only loaded after user interaction. try { - const data = await loadMarkdownFile(pathname, endOfLine) + const data = await loadMarkdownFile(pathname, endOfLine, autoGuessEncoding) const file = { pathname, data @@ -179,12 +179,16 @@ class Watcher { watcher .on('add', pathname => { if (!this._shouldIgnoreEvent(win.id, pathname, type)) { - add(win, pathname, type, this._preferences.getPreferedEOL()) + const eol = this._preferences.getPreferedEol() + const autoGuessEncoding = this._preferences.getItem('autoGuessEncoding') + add(win, pathname, type, eol, autoGuessEncoding) } }) .on('change', pathname => { if (!this._shouldIgnoreEvent(win.id, pathname, type)) { - change(win, pathname, type, this._preferences.getPreferedEOL()) + const eol = this._preferences.getPreferedEol() + const autoGuessEncoding = this._preferences.getItem('autoGuessEncoding') + change(win, pathname, type, eol, autoGuessEncoding) } }) .on('unlink', pathname => unlink(win, pathname, type)) @@ -204,7 +208,9 @@ class Watcher { } renameTimer = setTimeout(async () => { renameTimer = null - if (disposed) return + if (disposed) { + return + } const fileExists = await exists(watchPath) if (fileExists) { diff --git a/src/main/preferences/index.js b/src/main/preferences/index.js index cbc6dbab..48e4fa81 100644 --- a/src/main/preferences/index.js +++ b/src/main/preferences/index.js @@ -122,7 +122,7 @@ class Preference extends EventEmitter { }) } - getPreferedEOL () { + getPreferedEol () { const endOfLine = this.getItem('endOfLine') if (endOfLine === 'lf') { return 'lf' diff --git a/src/main/preferences/schema.json b/src/main/preferences/schema.json index 5946c2bc..4ab4a757 100644 --- a/src/main/preferences/schema.json +++ b/src/main/preferences/schema.json @@ -1,7 +1,8 @@ { "autoSave": { "description": "General--Automatically save the content being edited.", - "type": "boolean" + "type": "boolean", + "default": false }, "autoSaveDelay": { "description": "General--The time in ms after a change that the file is saved.", @@ -56,6 +57,7 @@ "description": "General--The language Mark Text use.", "type": "string" }, + "editorFontFamily": { "description": "Editor--editor font family", "type": "string", @@ -114,8 +116,55 @@ "default", "lf", "crlf" + ], + "default": "default" + }, + "defaultEncoding": { + "description": "Editor--The default file encoding.", + "default": "utf8", + "enum": [ + "utf8", + "utf16be", + "utf16le", + "utf32be", + "utf32le", + "latin3", + "iso885915", + "cp1252", + "arabic", + "cp1256", + "latin4", + "cp1257", + "iso88592", + "windows1250", + "cp866", + "iso88595", + "koi8r", + "koi8u", + "cp1251", + "iso885913", + "greek", + "cp1253", + "hebrew", + "cp1255", + "latin5", + "cp1254", + "gb2312", + "gb18030", + "gbk", + "big5", + "big5hkscs", + "shiftjis", + "eucjp", + "euckr", + "latin6" ] }, + "autoGuessEncoding": { + "description": "Editor--Try to automatically guess the file encoding when opening files.", + "type": "boolean", + "default": true + }, "textDirection": { "description": "Editor--The writing text direction", "enum": [ @@ -127,6 +176,7 @@ "description": "Editor--Hide hint for quickly creating paragraphs", "type": "boolean" }, + "preferLooseListItem": { "description": "Markdown--The preferred list type", "type": "boolean" @@ -177,10 +227,12 @@ "{" ] }, + "theme": { "description": "Theme--Select the theme used in Mark Text", "type": "string" }, + "imageInsertAction": { "description": "Image--The default behavior after insert image from local folder", "enum": [ @@ -189,6 +241,7 @@ "path" ] }, + "sideBarVisibility": { "description": "View--Whether the side bar is visible.", "type": "boolean" @@ -201,6 +254,7 @@ "description": "View--Whether the source-code mode is enabled by default.", "type": "boolean" }, + "searchExclusions": { "description": "Searcher--List of glob patterns to exclude from search.", "type": "array", diff --git a/src/main/windows/base.js b/src/main/windows/base.js index 07a77163..2e878f55 100644 --- a/src/main/windows/base.js +++ b/src/main/windows/base.js @@ -102,6 +102,27 @@ class BaseWindow extends EventEmitter { _buildUrlString (windowId, env, userPreference) { return this._buildUrlWithSettings(windowId, env, userPreference).toString() } + + _getPreferredBackgroundColor (theme) { + // Hardcode the theme background color and show the window direct for the fastet window ready time. + // Later with custom themes we need the background color (e.g. from meta information) and wait + // that the window is loaded and then pass theme data to the renderer. + switch (theme) { + case 'dark': + return '#282828' + case 'material-dark': + return '#34393f' + case 'ulysses': + return '#f3f3f3' + case 'graphite': + return '#f7f7f7' + case 'one-dark': + return '#282c34' + case 'light': + default: + return '#ffffff' + } + } } export default BaseWindow diff --git a/src/main/windows/editor.js b/src/main/windows/editor.js index d44eccc3..6a7f0de6 100644 --- a/src/main/windows/editor.js +++ b/src/main/windows/editor.js @@ -80,7 +80,7 @@ class EditorWindow extends BaseWindow { // Restore and focus window this.bringToFront() - const lineEnding = preferences.getPreferedEOL() + const lineEnding = preferences.getPreferedEol() appMenu.updateLineEndingMenu(this.id, lineEnding) win.webContents.send('mt::bootstrap-editor', { @@ -211,10 +211,11 @@ class EditorWindow extends BaseWindow { const { browserWindow } = this const { preferences } = this._accessor - const eol = preferences.getPreferedEOL() + const eol = preferences.getPreferedEol() + const autoGuessEncoding = preferences.getItem('autoGuessEncoding') for (const { filePath, options, selected } of fileList) { - loadMarkdownFile(filePath, eol).then(rawDocument => { + loadMarkdownFile(filePath, eol, autoGuessEncoding).then(rawDocument => { if (this.lifecycle === WindowLifecycle.READY) { this._doOpenTab(rawDocument, options, selected) } else { @@ -363,7 +364,7 @@ class EditorWindow extends BaseWindow { this.lifecycle = WindowLifecycle.READY const { preferences } = this._accessor const { sideBarVisibility, tabBarVisibility, sourceCodeModeEnabled } = preferences.getAll() - const lineEnding = preferences.getPreferedEOL() + const lineEnding = preferences.getPreferedEol() browserWindow.webContents.send('mt::bootstrap-editor', { addBlankTab: true, markdownList: [], @@ -396,27 +397,6 @@ class EditorWindow extends BaseWindow { // --- private --------------------------------- - _getPreferredBackgroundColor (theme) { - // Hardcode the theme background color and show the window direct for the fastet window ready time. - // Later with custom themes we need the background color (e.g. from meta information) and wait - // that the window is loaded and then pass theme data to the renderer. - switch (theme) { - case 'dark': - return '#282828' - case 'material-dark': - return '#34393f' - case 'ulysses': - return '#f3f3f3' - case 'graphite': - return '#f7f7f7' - case 'one-dark': - return '#282c34' - case 'light': - default: - return '#ffffff' - } - } - /** * Open a new new tab from the markdown document. * diff --git a/src/main/windows/setting.js b/src/main/windows/setting.js index 7b0db7b2..f0a27751 100644 --- a/src/main/windows/setting.js +++ b/src/main/windows/setting.js @@ -28,7 +28,7 @@ class SettingWindow extends BaseWindow { } // Enable native or custom/frameless window and titlebar - const { titleBarStyle } = preferences.getAll() + const { titleBarStyle, theme } = preferences.getAll() if (!isOsx) { winOptions.titleBarStyle = 'default' if (titleBarStyle === 'native') { @@ -36,6 +36,8 @@ class SettingWindow extends BaseWindow { } } + winOptions.backgroundColor = this._getPreferredBackgroundColor(theme) + let win = this.browserWindow = new BrowserWindow(winOptions) this.id = win.id diff --git a/src/renderer/prefComponents/editor/config.js b/src/renderer/prefComponents/editor/config.js index b3236ad4..0ccb6814 100644 --- a/src/renderer/prefComponents/editor/config.js +++ b/src/renderer/prefComponents/editor/config.js @@ -1,3 +1,5 @@ +import { ENCODING_NAME_MAP } from 'common/encoding' + export const endOfLineOptions = [{ label: 'Default', value: 'default' @@ -16,3 +18,16 @@ export const textDirectionOptions = [{ label: 'Right to Left', value: 'rtl' }] + +let defaultEncodingOptions = null +export const getDefaultEncodingOptions = () => { + if (defaultEncodingOptions) { + return defaultEncodingOptions + } + + defaultEncodingOptions = [] + for (const [value, label] of Object.entries(ENCODING_NAME_MAP)) { + defaultEncodingOptions.push({ label, value }) + } + return defaultEncodingOptions +} diff --git a/src/renderer/prefComponents/editor/index.vue b/src/renderer/prefComponents/editor/index.vue index 7e439bb4..8a34c084 100644 --- a/src/renderer/prefComponents/editor/index.vue +++ b/src/renderer/prefComponents/editor/index.vue @@ -62,29 +62,41 @@ > + + + + - @@ -100,7 +112,8 @@ import Separator from '../common/separator' import TextBox from '../common/textBox' import { endOfLineOptions, - textDirectionOptions + textDirectionOptions, + getDefaultEncodingOptions } from './config' export default { @@ -115,6 +128,7 @@ export default { data () { this.endOfLineOptions = endOfLineOptions this.textDirectionOptions = textDirectionOptions + this.defaultEncodingOptions = getDefaultEncodingOptions() return {} }, computed: { @@ -131,7 +145,9 @@ export default { codeFontFamily: state => state.preferences.codeFontFamily, trimUnnecessaryCodeBlockEmptyLines: state => state.preferences.trimUnnecessaryCodeBlockEmptyLines, hideQuickInsertHint: state => state.preferences.hideQuickInsertHint, - editorLineWidth: state => state.preferences.editorLineWidth + editorLineWidth: state => state.preferences.editorLineWidth, + defaultEncoding: state => state.preferences.defaultEncoding, + autoGuessEncoding: state => state.preferences.autoGuessEncoding }) }, methods: { diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index f9b5c54d..01069bac 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -11,7 +11,6 @@ import notice from '../services/notification' const autoSaveTimers = new Map() const state = { - lineEnding: 'lf', currentFile: {}, tabs: [], listToc: [], // Just use for deep equal check. and replace with new toc if needed. @@ -270,11 +269,6 @@ const mutations = { }) }, - // TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used. - SET_GLOBAL_LINE_ENDING (state, ending) { - state.lineEnding = ending - }, - // Push a tab specific notification on stack that never disappears. PUSH_TAB_NOTIFICATION (state, data) { const defaultAction = () => {} @@ -606,8 +600,8 @@ const actions = { sourceCodeModeEnabled } = config - commit('SET_GLOBAL_LINE_ENDING', lineEnding) dispatch('SEND_INITIALIZED') + commit('SET_USER_PREFERENCE', { endOfLine: lineEnding }) commit('SET_LAYOUT', { rightColumn: 'files', showSideBar: !!sideBarVisibility, @@ -714,15 +708,17 @@ const actions = { * @param {{markdown?: string, selected?: boolean}} obj Optional markdown string * and whether the tab should become the selected tab (true if not set). */ - NEW_UNTITLED_TAB ({ commit, state, dispatch }, { markdown: markdownString, selected }) { + NEW_UNTITLED_TAB ({ commit, state, dispatch, rootState }, { markdown: markdownString, selected }) { // If not set select the tab. if (selected == null) { selected = true } dispatch('SHOW_TAB_VIEW', false) - const { tabs, lineEnding } = state - const fileState = getBlankFileState(tabs, lineEnding, markdownString) + + const { defaultEncoding, endOfLine } = rootState.preferences + const { tabs } = state + const fileState = getBlankFileState(tabs, defaultEncoding, endOfLine, markdownString) if (selected) { const { id, markdown } = fileState diff --git a/src/renderer/store/help.js b/src/renderer/store/help.js index 87cfea96..05bbc16f 100644 --- a/src/renderer/store/help.js +++ b/src/renderer/store/help.js @@ -12,7 +12,10 @@ export const defaultFileState = { pathname: '', filename: 'Untitled-1', markdown: '', - encoding: 'utf8', // Currently just "utf8" or "utf8bom" + encoding: { + encoding: 'utf8', + isBom: false + }, lineEnding: 'lf', // lf or crlf adjustLineEndingOnSave: false, // convert editor buffer (LF) to CRLF when saving history: { @@ -65,8 +68,8 @@ export const getFileStateFromData = data => { }) } -export const getBlankFileState = (tabs, lineEnding = 'lf', markdown = '') => { - const fileState = JSON.parse(JSON.stringify(defaultFileState)) +export const getBlankFileState = (tabs, defaultEncoding = 'utf8', lineEnding = 'lf', markdown = '') => { + const fileState = cloneObj(defaultFileState, true) let untitleId = Math.max(...tabs.map(f => { if (f.pathname === '') { return +f.filename.split('-')[1] @@ -77,11 +80,12 @@ export const getBlankFileState = (tabs, lineEnding = 'lf', markdown = '') => { const id = getUniqueId() - // We may pass muarkdown=null as parameter. + // We may pass markdown=null as parameter. if (markdown == null) { markdown = '' } + fileState.encoding.encoding = defaultEncoding return Object.assign(fileState, { lineEnding, adjustLineEndingOnSave: lineEnding.toLowerCase() === 'crlf', @@ -94,7 +98,7 @@ export const getBlankFileState = (tabs, lineEnding = 'lf', markdown = '') => { export const getSingleFileState = ({ id = getUniqueId(), markdown, filename, pathname, options }) => { // TODO(refactor:renderer/editor): Replace this function with `createDocumentState`. - const fileState = JSON.parse(JSON.stringify(defaultFileState)) + const fileState = cloneObj(defaultFileState, true) const { encoding, lineEnding, adjustLineEndingOnSave = 'ltr' } = options assertLineEnding(adjustLineEndingOnSave, lineEnding) diff --git a/src/renderer/store/preferences.js b/src/renderer/store/preferences.js index aefbc9af..521b3db4 100644 --- a/src/renderer/store/preferences.js +++ b/src/renderer/store/preferences.js @@ -26,6 +26,8 @@ const state = { autoPairMarkdownSyntax: true, autoPairQuote: true, endOfLine: 'default', + defaultEncoding: 'utf8', + autoGuessEncoding: true, textDirection: 'ltr', hideQuickInsertHint: false, imageInsertAction: 'folder', diff --git a/static/preference.json b/static/preference.json index 5095e07e..5df2b175 100644 --- a/static/preference.json +++ b/static/preference.json @@ -23,6 +23,8 @@ "autoPairMarkdownSyntax": true, "autoPairQuote": true, "endOfLine": "default", + "defaultEncoding": "utf8", + "autoGuessEncoding": true, "textDirection": "ltr", "hideQuickInsertHint": false, "imageInsertAction": "path", diff --git a/yarn.lock b/yarn.lock index d9bd08a9..26b80fb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,6 +1795,13 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bl@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -2239,6 +2246,13 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +ced@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ced/-/ced-1.0.0.tgz#09899dbcda52083b80a65f165c65f9055d03ab03" + integrity sha512-Ud3ltdMCO3XMaclGtBGAuV+rNTx/lOwXukEf2pjtmk8CjGRhgACUtbHSQnty/wDn3ZPSz+gv1FS5jiiXH5g8Bw== + dependencies: + bindings "^1.3.0" + center-align@^0.1.1: version "0.1.3" resolved "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" @@ -5049,6 +5063,11 @@ file-loader@^4.1.0: loader-utils "^1.2.3" schema-utils "^2.0.0" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filesize@^3.6.1: version "3.6.1" resolved "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -5998,7 +6017,7 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv iconv-lite@^0.5.0: version "0.5.0" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.0.tgz#59cdde0a2a297cc2aeb0c6445a195ee89f127550" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.0.tgz#59cdde0a2a297cc2aeb0c6445a195ee89f127550" integrity sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw== dependencies: safer-buffer ">= 2.1.2 < 3"