diff --git a/src-node/package-lock.json b/src-node/package-lock.json index 6177376876..6183efc95f 100644 --- a/src-node/package-lock.json +++ b/src-node/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "license": "GNU-AGPL3.0", "dependencies": { "@phcode/fs": "^3.0.1", diff --git a/src/extensionsIntegrated/Bookmarks/main.js b/src/extensionsIntegrated/Bookmarks/main.js new file mode 100644 index 0000000000..a9724f6978 --- /dev/null +++ b/src/extensionsIntegrated/Bookmarks/main.js @@ -0,0 +1,48 @@ +define(function (require, exports, module) { + const AppInit = require("utils/AppInit"); + const CommandManager = require("command/CommandManager"); + const Menus = require("command/Menus"); + const Strings = require("strings"); + + const Bookmarks = require("./src/bookmarks"); + + // command ids + const CMD_TOGGLE_BOOKMARK = "bookmarks.toggleBookmark"; + const CMD_NEXT_BOOKMARK = "bookmarks.nextBookmark"; + const CMD_PREV_BOOKMARK = "bookmarks.prevBookmark"; + + // default keyboard shortcuts + const TOGGLE_BOOKMARK_KB_SHORTCUT = "Ctrl-Alt-B"; + const NEXT_BOOKMARK_KB_SHORTCUT = "Ctrl-Alt-N"; + const PREV_BOOKMARK_KB_SHORTCUT = "Ctrl-Alt-P"; + + /** + * This function is responsible for registering all the required commands + */ + function _registerCommands() { + CommandManager.register(Strings.TOGGLE_BOOKMARK, CMD_TOGGLE_BOOKMARK, Bookmarks.toggleBookmark); + CommandManager.register(Strings.GOTO_NEXT_BOOKMARK, CMD_NEXT_BOOKMARK, Bookmarks.goToNextBookmark); + CommandManager.register(Strings.GOTO_PREV_BOOKMARK, CMD_PREV_BOOKMARK, Bookmarks.goToPrevBookmark); + } + + /** + * This function is responsible to add the bookmarks menu items to the navigate menu + */ + function _addItemsToMenu() { + const navigateMenu = Menus.getMenu(Menus.AppMenuBar.NAVIGATE_MENU); + navigateMenu.addMenuDivider(); // add a line to separate the other items from the bookmark ones + + navigateMenu.addMenuItem(CMD_TOGGLE_BOOKMARK, TOGGLE_BOOKMARK_KB_SHORTCUT); + navigateMenu.addMenuItem(CMD_NEXT_BOOKMARK, NEXT_BOOKMARK_KB_SHORTCUT); + navigateMenu.addMenuItem(CMD_PREV_BOOKMARK, PREV_BOOKMARK_KB_SHORTCUT); + } + + function init() { + _registerCommands(); + _addItemsToMenu(); + } + + AppInit.appReady(function () { + init(); + }); +}); diff --git a/src/extensionsIntegrated/Bookmarks/src/bookmarks.js b/src/extensionsIntegrated/Bookmarks/src/bookmarks.js new file mode 100644 index 0000000000..8bcefcf350 --- /dev/null +++ b/src/extensionsIntegrated/Bookmarks/src/bookmarks.js @@ -0,0 +1,179 @@ +define(function (require, exports, module) { + const EditorManager = require("editor/EditorManager"); + const Editor = require("editor/Editor").Editor; + + const Helper = require("./helper"); + + const GUTTER_NAME = "CodeMirror-bookmarkGutter", + BOOKMARK_PRIORITY = 100; + + /** + * This is where all the bookmarks will be stored + * it is an array of objects where each object will be stored firstly based on the file and then as per line no. + * the sorting is done to make sure we need not access the whole complete list when trying to move back and forth + * + * @type {[{file: {String}, line: {Number}}]} + */ + const BookmarksList = []; + + // initialize the bookmark gutter + Editor.registerGutter(GUTTER_NAME, BOOKMARK_PRIORITY); + + /** + * This function is responsible to remove the bookmark from the bookmarks list + * + * @private + * @param {String} file - the file path + * @param {Number} line - the line number + */ + function _removeFromBookmarksList(file, line) { + for (let i = 0; i < BookmarksList.length; i++) { + if (BookmarksList[i].file === file && BookmarksList[i].line === line) { + BookmarksList.splice(i, 1); + break; + } + } + } + + /** + * This function is responsible to add the bookmark to the bookmarks list + * after adding that we also sort that first by file path and then by line number to make accessing efficient + * + * @private + * @param {String} file - the file path + * @param {Number} line - the line number + */ + function _addToBookmarksList(file, line) { + BookmarksList.push({ file: file, line: line }); + + BookmarksList.sort((a, b) => { + if (a.file === b.file) { + return a.line - b.line; + } + return a.file.localeCompare(b.file); + }); + } + + /** + * This function toggles a bookmark on a specific line + * + * @private + * @param {Editor} editor - The current editor instance + * @param {number} line - The line number to toggle bookmark on + */ + function _toggleLineBookmark(editor, line) { + const file = editor.document.file.fullPath; // this file path will be used when storing in the bookmarks list + + // remove bookmark + if (Helper.hasBookmark(editor, line, GUTTER_NAME)) { + editor.setGutterMarker(line, GUTTER_NAME, ""); + _removeFromBookmarksList(file, line); + } else { + // add bookmark + editor.setGutterMarker(line, GUTTER_NAME, Helper.createBookmarkMarker()); + _addToBookmarksList(file, line); + } + } + + /** + * This function is responsible to toggle bookmarks at the current cursor position(s) + */ + function toggleBookmark() { + const editor = EditorManager.getFocusedEditor(); + if (!editor) { + return; + } + + const selections = editor.getSelections(); + const uniqueLines = Helper.getUniqueLines(selections); + + // process each unique line + uniqueLines.forEach((line) => { + _toggleLineBookmark(editor, line); + }); + } + + /** + * This function gets executed when users click on the go to next bookmark button in the navigate menu, + * or its keyboard shortcut + * This finds the next bookmark in the current file and moves the cursor there + */ + function goToNextBookmark() { + const editor = EditorManager.getFocusedEditor(); + if (!editor) { + return; + } + + // get the file path and line as these values are needed when searching in the bookmarks list + const currentFile = editor.document.file.fullPath; + const currentLine = editor.getCursorPos().line; + + // get all the bookmarks in current file (this is already sorted by line number) + const fileBookmarks = BookmarksList.filter((bookmark) => bookmark.file === currentFile); + if (fileBookmarks.length === 0) { + return; + } + + // find the next bookmark after current position + let nextBookmark = null; + + // find the first bookmark after current line + for (let i = 0; i < fileBookmarks.length; i++) { + if (fileBookmarks[i].line > currentLine) { + nextBookmark = fileBookmarks[i]; + break; + } + } + + // If no next bookmark found, we wrap around to get the first bookmark in this file + if (!nextBookmark && fileBookmarks.length > 0) { + nextBookmark = fileBookmarks[0]; + } + + // take the cursor to the bookmark + if (nextBookmark) { + editor.setCursorPos(nextBookmark.line, 0); + } + } + + /** + * This function gets executed when users click on the go to previous bookmark button in the navigate menu, + * or its keyboard shortcut + * This finds the previous bookmark in the current file and moves the cursor there + */ + function goToPrevBookmark() { + const editor = EditorManager.getFocusedEditor(); + if (!editor) { + return; + } + + const currentFile = editor.document.file.fullPath; + const currentLine = editor.getCursorPos().line; + + const fileBookmarks = BookmarksList.filter((bookmark) => bookmark.file === currentFile); + if (fileBookmarks.length === 0) { + return; + } + + let prevBookmark = null; + + for (let i = fileBookmarks.length - 1; i >= 0; i--) { + if (fileBookmarks[i].line < currentLine) { + prevBookmark = fileBookmarks[i]; + break; + } + } + + if (!prevBookmark && fileBookmarks.length > 0) { + prevBookmark = fileBookmarks[fileBookmarks.length - 1]; + } + + if (prevBookmark) { + editor.setCursorPos(prevBookmark.line, 0); + } + } + + exports.toggleBookmark = toggleBookmark; + exports.goToNextBookmark = goToNextBookmark; + exports.goToPrevBookmark = goToPrevBookmark; +}); diff --git a/src/extensionsIntegrated/Bookmarks/src/helper.js b/src/extensionsIntegrated/Bookmarks/src/helper.js new file mode 100644 index 0000000000..e071c60650 --- /dev/null +++ b/src/extensionsIntegrated/Bookmarks/src/helper.js @@ -0,0 +1,40 @@ +define(function (require, exports, module) { + // the bookmark svg icon + const bookmarkSvg = require("text!styles/images/bookmark.svg"); + + /** + * This function creates a bookmark marker element + * + * @returns {HTMLElement} The bookmark marker element + */ + function createBookmarkMarker() { + return $("
").addClass("bookmark-icon").html(bookmarkSvg)[0]; + } + + /** + * This function checks whether a line has a bookmark + * + * @param {Editor} editor - The current editor instance + * @param {number} line - The line number to check + * @param {string} gutterName - The name of the gutter + * @returns {boolean} True if the line has a bookmark, false otherwise + */ + function hasBookmark(editor, line, gutterName) { + return !!editor.getGutterMarker(line, gutterName); + } + + /** + * This function gets unique line numbers from all selections + * this is needed so that when multiple cursors are there at the same line, we can get the line only once + * + * @param {Array<{start: {line: number}}>} selections - Array of selections + * @returns {Array} Array of unique line numbers + */ + function getUniqueLines(selections) { + return [...new Set(selections.map((selection) => selection.start.line))]; + } + + exports.createBookmarkMarker = createBookmarkMarker; + exports.hasBookmark = hasBookmark; + exports.getUniqueLines = getUniqueLines; +}); diff --git a/src/extensionsIntegrated/loader.js b/src/extensionsIntegrated/loader.js index 2da9f10f68..33ace3a5bd 100644 --- a/src/extensionsIntegrated/loader.js +++ b/src/extensionsIntegrated/loader.js @@ -44,4 +44,5 @@ define(function (require, exports, module) { require("./indentGuides/main"); require("./CSSColorPreview/main"); require("./TabBar/main"); + require("./Bookmarks/main"); }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b996f7da4..c204444b64 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -434,6 +434,12 @@ define({ "CLOSE_UNMODIFIED_TABS": "Close Unmodified Tabs", "REOPEN_CLOSED_FILE": "Reopen Closed File", + // Bookmarks extension strings + "TOGGLE_BOOKMARK": "Toggle Bookmark", + "GOTO_PREV_BOOKMARK": "Go to Previous Bookmark", + "GOTO_NEXT_BOOKMARK": "Go to Next Bookmark", + "TOGGLE_BOOKMARKS_PANEL": "Toggle Bookmarks panel", + // CodeInspection: errors/warnings "ERRORS_NO_FILE": "No File Open", "ERRORS_PANEL_TITLE_MULTIPLE": "{0} Problems - {1}", diff --git a/src/styles/Extn-Bookmarks.less b/src/styles/Extn-Bookmarks.less new file mode 100644 index 0000000000..42d97b7ca0 --- /dev/null +++ b/src/styles/Extn-Bookmarks.less @@ -0,0 +1,16 @@ +.bookmark-icon { + width: 0.75em; + height: 0.75em; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-left: 0.2em; + margin-top: 0.35em; + + svg { + width: 100%; + height: 100%; + fill: white; + } +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 6b4b43a611..4788692ccb 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -44,6 +44,7 @@ @import "Extn-TabBar.less"; @import "Extn-DisplayShortcuts.less"; @import "Extn-CSSColorPreview.less"; +@import "Extn-Bookmarks.less"; /* Overall layout */ diff --git a/src/styles/images/bookmark.svg b/src/styles/images/bookmark.svg new file mode 100644 index 0000000000..bbde487856 --- /dev/null +++ b/src/styles/images/bookmark.svg @@ -0,0 +1 @@ +