diff --git a/README.md b/README.md index ed96fd6..3db3111 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ The usual editing shortcuts of Ace (multiple cursors, moving and duplicating lin ## Supported languages -Syntax highlighting is chosen from the file name or extension, and can be changed from the Language menu: +Syntax highlighting is chosen from the file name or extension, and can be changed from the Language menu, which keeps Text at the top and lists the other languages alphabetically, with languages that share a first letter grouped under that letter: Plain text, JavaScript, TypeScript, JSON, XML, HTML, CSS, YAML, Markdown, Java, Kotlin, C/C++, C#, Go, Rust, Python, Ruby, PHP, SQL, PL/SQL, Shell, Batch, PowerShell, INI, Properties, TOML, Dockerfile and Makefile. diff --git a/package-lock.json b/package-lock.json index 3cde504..7db6a2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "note-manager", - "version": "1.7.1", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "note-manager", - "version": "1.7.1", + "version": "1.8.0", "license": "MIT", "workspaces": [ "tools/shims/glob", diff --git a/package.json b/package.json index e84b3f7..090fbfa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "note-manager", "productName": "Note Manager", - "version": "1.7.1", + "version": "1.8.0", "description": "A lightweight, fast and multi-functional desktop text editor", "homepage": "https://github.com/Note-Manager/note-manager#readme", "repository": { diff --git a/src/domain/SupportedLanguage.ts b/src/domain/SupportedLanguage.ts index 516c3f6..e7c8bc9 100644 --- a/src/domain/SupportedLanguage.ts +++ b/src/domain/SupportedLanguage.ts @@ -197,4 +197,26 @@ export function findLanguageByFileName(fileName: string | undefined): SupportedL const extension = baseName.substring(dotIndex).toLowerCase(); return languages.find(l => l.extensions.includes(extension)) || SupportedLanguages[LanguageName.TEXT]; +} + +export interface LanguageGroup { + initial: string, + languages: Array +} + +function getInitial(language: SupportedLanguage) { + return language.label.charAt(0).toUpperCase(); +} + +function compareLabels(a: SupportedLanguage, b: SupportedLanguage) { + return a.label.localeCompare(b.label, "en", {sensitivity: "base"}); +} + +export function groupLanguagesByInitial(languages: Array): Array { + return [...languages].sort(compareLabels).reduce>((groups, language) => { + const lastGroup = groups[groups.length - 1]; + if (lastGroup?.initial === getInitial(language)) lastGroup.languages.push(language); + else groups.push({initial: getInitial(language), languages: [language]}); + return groups; + }, []); } \ No newline at end of file diff --git a/src/domain/__tests__/SupportedLanguage.test.ts b/src/domain/__tests__/SupportedLanguage.test.ts index f4e8246..08a79ac 100644 --- a/src/domain/__tests__/SupportedLanguage.test.ts +++ b/src/domain/__tests__/SupportedLanguage.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest"; -import {findLanguageByFileName, LanguageName} from "../SupportedLanguage"; +import {findLanguageByFileName, groupLanguagesByInitial, LanguageName, SupportedLanguage, SupportedLanguages} from "../SupportedLanguage"; describe("findLanguageByFileName", () => { it("finds the language by extension", () => { @@ -32,3 +32,27 @@ describe("findLanguageByFileName", () => { expect(findLanguageByFileName("Main.class").name).toBe(LanguageName.TEXT); }); }); + +describe("groupLanguagesByInitial", () => { + const languageNamed = (label: string): SupportedLanguage => ({name: label.toLowerCase(), label: label, extensions: []}); + + it("sorts languages by label and groups them by their first letter", () => { + const groups = groupLanguagesByInitial(["Go", "CSS", "batch", "C#", "Java", "JSON", "Rust"].map(languageNamed)); + + expect(groups.map(g => [g.initial, g.languages.map(l => l.label)])).toEqual([ + ["B", ["batch"]], + ["C", ["C#", "CSS"]], + ["G", ["Go"]], + ["J", ["Java", "JSON"]], + ["R", ["Rust"]] + ]); + }); + + it("puts every supported language in exactly one group", () => { + const languages = Object.values(SupportedLanguages); + const grouped = groupLanguagesByInitial(languages).flatMap(g => g.languages); + + expect(grouped).toHaveLength(languages.length); + expect(new Set(grouped)).toEqual(new Set(languages)); + }); +}); diff --git a/src/utils/ApplicationMenu.ts b/src/utils/ApplicationMenu.ts index d4897ec..950f642 100644 --- a/src/utils/ApplicationMenu.ts +++ b/src/utils/ApplicationMenu.ts @@ -1,7 +1,6 @@ import {app, BrowserWindow, dialog, Menu, MenuItemConstructorOptions, nativeImage, shell} from "electron"; import path from "node:path"; import {EventType, SystemPaths, TabAction} from "../enums"; -import {SupportedLanguages} from "../domain/SupportedLanguage"; import {Settings} from "../domain/Settings"; import {getSystemPath, isFileExists} from "./FileUtils"; import {openInWindow} from "./EditorUtils"; @@ -15,6 +14,7 @@ import {ShortcutId} from "./Shortcuts"; import {getAccelerator} from "./Accelerators"; import {getUserPluginManifests, getUserPluginPath, loadUserPluginManifests} from "./UserPlugins"; import {createEncodingMenu} from "./EncodingMenu"; +import {createLanguageMenu} from "./LanguageMenu"; import {createSearchMenu} from "./SearchMenu"; import {missingRecentFileNotice} from "./DialogUtils"; import {broadcast, notify, sendPluginAction, sendToFocusedWindow} from "./WindowUtils"; @@ -74,10 +74,7 @@ function createApplicationMenu() { }, {label: "Search", submenu: createSearchMenu(command => sendToFocusedWindow(EventType.EDITOR_COMMAND, {command: command}))}, {label: "Encoding", submenu: createEncodingMenu(sendToFocusedWindow)}, - { - label: "Language", - submenu: Object.values(SupportedLanguages).map(language => ({label: language.label, click: sendAction(EventType.SET_TAB_LANGUAGE, language)})) - }, + {label: "Language", submenu: createLanguageMenu(sendToFocusedWindow)}, { label: "Preferences", submenu: [ diff --git a/src/utils/LanguageMenu.ts b/src/utils/LanguageMenu.ts new file mode 100644 index 0000000..0dcfc3d --- /dev/null +++ b/src/utils/LanguageMenu.ts @@ -0,0 +1,26 @@ +import {groupLanguagesByInitial, LanguageGroup, LanguageName, SupportedLanguage, SupportedLanguages} from "../domain/SupportedLanguage"; +import {EventType} from "../enums"; +import type {Sender} from "./WindowUtils"; + +type MenuItem = Electron.MenuItemConstructorOptions; + +export function createLanguageMenu(send: Sender): Array { + const plainText = SupportedLanguages[LanguageName.TEXT]; + const otherLanguages = Object.values(SupportedLanguages).filter(language => language !== plainText); + + return [ + createLanguageItem(send, plainText), + {type: "separator"}, + ...groupLanguagesByInitial(otherLanguages).map(group => createGroupItem(send, group)) + ]; +} + +function createGroupItem(send: Sender, group: LanguageGroup): MenuItem { + if (group.languages.length === 1) return createLanguageItem(send, group.languages[0]); + + return {label: group.initial, submenu: group.languages.map(language => createLanguageItem(send, language))}; +} + +function createLanguageItem(send: Sender, language: SupportedLanguage): MenuItem { + return {label: language.label, click: () => send(EventType.SET_TAB_LANGUAGE, language)}; +}