--------------------------------------------------------------------------------
/walkthrough/inspiredInDelphiButOpenToOthers.nls.pt-br.md:
--------------------------------------------------------------------------------
1 | ## Inspirado no Delphi, mas aberto a outros
2 |
3 | A extensão foi inspirada no Delphi, pois eu fui um desenvolvedor Delphi por muito tempo, e adoro seus bookmarks. Mas ele suporta diferentes editores também.
4 |
5 | Para mudar um pouco como a extensão funciona (alternar e navegar), basta brincar com a configuração `numberedBookmarks.navigateThroughAllFiles`:
6 |
7 | Valor | Explicação
8 | --------- | ---------
9 | `false` | _padrão_ - mesmo comportamento de hoje
10 | `replace` | você não pode ter o mesmo numbered bookmark em arquivos distintos
11 | `allowDuplicates` | você pode ter o mesmo numbered bookmark em arquivos distintos, e se você acionar repetidamente ao mesmo número, ele irá olhar para os outros arquivos.
12 |
13 | ### Desenvolvedores IntelliJ / UltraEdit
14 |
15 | Se você é um usuário do **IntelliJ** ou **UltraEdit**, você vai notar que o numbered bookmarks funciona um pouco diferente do seu comportamento padrão.
16 |
17 | Para fazer **Numbered Bookmarks** funcionar do mesmo jeito dessas ferramentas, simplesmente adicione `"numberedBookmarks.navigateThroughAllFiles": replace"` nas suas configurações e aproveite.
18 |
19 |
--------------------------------------------------------------------------------
/src/quickpick/controllerPicker.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import path = require("path");
7 | import { l10n, QuickPickItem, window } from "vscode";
8 | import { codicons } from "vscode-ext-codicons";
9 | import { Controller } from "../core/controller";
10 |
11 | interface ControllerQuickPickItem extends QuickPickItem {
12 | controller: Controller;
13 | }
14 |
15 | export async function pickController(controllers: Controller[], activeController: Controller): Promise {
16 |
17 | if (controllers.length === 1) {
18 | return activeController;
19 | }
20 |
21 | const items: ControllerQuickPickItem[] = controllers.map(controller => {
22 | return {
23 | label: codicons.root_folder + ' ' + controller.workspaceFolder.name,
24 | description: path.dirname(controller.workspaceFolder.uri.path),
25 | controller: controller
26 | }
27 | }
28 | );
29 |
30 | const selection = await window.showQuickPick(items, {
31 | placeHolder: l10n.t('Select a workspace')
32 | });
33 |
34 | if (typeof selection === "undefined") {
35 | return undefined
36 | }
37 |
38 | return selection.controller;
39 | }
40 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "Launch Extension",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "runtimeExecutable": "${execPath}",
10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
11 | "stopOnEntry": false,
12 | "outFiles": ["${workspaceFolder}/dist/**/*.js"],
13 | // "skipFiles": ["/**", "**/node_modules/**", "**/app/out/vs/**", "**/extensions/**"],
14 | "smartStep": true,
15 | "sourceMaps": true
16 | },
17 | {
18 | "name": "Launch Tests",
19 | "type": "extensionHost",
20 | "request": "launch",
21 | "runtimeExecutable": "${execPath}",
22 | "args": [
23 | "--disable-extensions",
24 | "--extensionDevelopmentPath=${workspaceFolder}",
25 | "--extensionTestsPath=${workspaceFolder}/out/src/test/suite/index"
26 | ],
27 | "outFiles": [
28 | "${workspaceFolder}/out/src/test/**/*.js"
29 | ],
30 | "preLaunchTask": "npm: test-compile"
31 | },
32 | {
33 | "name": "Run Web Extension in VS Code",
34 | "type": "pwa-extensionHost",
35 | "debugWebWorkerHost": true,
36 | "request": "launch",
37 | "args": [
38 | "--extensionDevelopmentPath=${workspaceFolder}",
39 | "--extensionDevelopmentKind=web"
40 | ],
41 | "outFiles": ["${workspaceFolder}/dist/**/*.js"],
42 | "preLaunchTask": "npm: watch"
43 | }
44 | ]
45 | }
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # Controls when the action will run. Triggers the workflow on push or pull request
4 | # events but only for the master branch
5 | on:
6 | push:
7 | branches: [ master ]
8 | pull_request:
9 | branches: [ master ]
10 |
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 | # This workflow contains a single job called "build"
14 | build:
15 | # The type of runner that the job will run on
16 | strategy:
17 | matrix:
18 | os: [macos-latest, ubuntu-latest, windows-latest]
19 | runs-on: ${{ matrix.os }}
20 |
21 | # Steps represent a sequence of tasks that will be executed as part of the job
22 | steps:
23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 | with:
27 | submodules: true
28 |
29 | # Runs a set of commands using the runners shell
30 | - name: Install Node.js
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: 20.x
34 | cache: 'npm'
35 |
36 | # Runs the tests
37 | - name: Cache VS Code downloads
38 | uses: actions/cache@v4
39 | with:
40 | path: .vscode-test
41 | key: vscode-test-${{ matrix.os }}-${{ hashFiles('**/package.json') }}
42 | restore-keys: |
43 | vscode-test-${{ matrix.os }}-
44 | - run: npm ci
45 | - run: xvfb-run -a npm test
46 | if: runner.os == 'Linux'
47 | - run: npm test
48 | if: runner.os != 'Linux'
49 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the GPLv3 License. See License.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | //@ts-check
8 |
9 | 'use strict';
10 |
11 | const path = require('path');
12 | const TerserPlugin = require('terser-webpack-plugin');
13 | const webpack = require('webpack');
14 |
15 |
16 |
17 | /**@type {import('webpack').Configuration}*/
18 | const config = {
19 | entry: "./src/extension.ts",
20 | optimization: {
21 | minimizer: [new TerserPlugin({
22 | parallel: true,
23 | terserOptions: {
24 | ecma: 2019,
25 | keep_classnames: false,
26 | mangle: true,
27 | module: true
28 | }
29 | })],
30 | },
31 |
32 | devtool: 'source-map',
33 | externals: {
34 | vscode: "commonjs vscode" // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
35 | },
36 | resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
37 | extensions: ['.ts', '.js']
38 | },
39 | module: {
40 | rules: [{
41 | test: /\.ts$/,
42 | exclude: /node_modules/,
43 | use: [{
44 | loader: 'ts-loader',
45 | }]
46 | }]
47 | },
48 | };
49 |
50 | const nodeConfig = {
51 | ...config,
52 | target: "node",
53 | output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
54 | path: path.resolve(__dirname, 'dist'),
55 | filename: 'extension-node.js',
56 | libraryTarget: "commonjs2",
57 | devtoolModuleFilenameTemplate: "../[resource-path]",
58 | },
59 | }
60 |
61 | module.exports = [nodeConfig];
62 |
--------------------------------------------------------------------------------
/src/utils/reveal.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { Selection, Tab, TabInputText, TextEditorRevealType, Uri, ViewColumn, window, workspace } from "vscode";
7 | import { Bookmark } from "../core/bookmark";
8 | import { getRevealLocationConfig } from "./revealLocation";
9 |
10 | export function revealLine(line: number, directJump?: boolean) {
11 | const newSe = new Selection(line, 0, line, 0);
12 | window.activeTextEditor.selection = newSe;
13 | window.activeTextEditor.revealRange(newSe, getRevealLocationConfig(directJump));
14 | }
15 |
16 | export function revealPosition(line: number, column: number): void {
17 | if (isNaN(column)) {
18 | revealLine(line);
19 | } else {
20 | const revealType: TextEditorRevealType = getRevealLocationConfig(line === window.activeTextEditor.selection.active.line);
21 | const newPosition = new Selection(line, column, line, column);
22 | window.activeTextEditor.selection = newPosition;
23 | window.activeTextEditor.revealRange(newPosition, revealType);
24 | }
25 | }
26 |
27 | export async function previewPositionInDocument(point: Bookmark, uri: Uri): Promise {
28 | const textDocument = await workspace.openTextDocument(uri);
29 | await window.showTextDocument(textDocument, { preserveFocus: true, preview: true } );
30 | revealPosition(point.line - 1, point.column - 1);
31 | }
32 |
33 | export async function revealPositionInDocument(point: Bookmark, uri: Uri): Promise {
34 | const tabGroupColumn = findTabGroupColumn(uri, window.activeTextEditor.viewColumn);
35 |
36 | const textDocument = await workspace.openTextDocument(uri);
37 | await window.showTextDocument(textDocument, tabGroupColumn, false);
38 | revealPosition(point.line, point.column);
39 | }
40 |
41 | function findTabGroupColumn(uri: Uri, column: ViewColumn): ViewColumn {
42 | if (window.tabGroups.all.length === 1) {
43 | return column;
44 | }
45 |
46 | for (const tab of window.tabGroups.activeTabGroup.tabs) {
47 | if (isTabOfUri(tab, uri)) {
48 | return tab.group.viewColumn;
49 | }
50 | }
51 |
52 | for (const tabGroup of window.tabGroups.all) {
53 | if (tabGroup.viewColumn === column)
54 | continue;
55 |
56 | for (const tab of tabGroup.tabs) {
57 | if (isTabOfUri(tab, uri)) {
58 | return tab.group.viewColumn;
59 | }
60 | }
61 | }
62 |
63 | return column;
64 | }
65 |
66 | function isTabOfUri(tab: Tab, uri: Uri): boolean {
67 | return tab.input instanceof TabInputText &&
68 | tab.input.uri.fsPath.toLocaleLowerCase() === uri.fsPath.toLocaleLowerCase()
69 | }
70 |
--------------------------------------------------------------------------------
/src/storage/workspaceState.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { workspace, window, WorkspaceFolder, l10n } from "vscode";
7 | import { Container } from "../core/container";
8 | import { Controller } from "../core/controller";
9 | import { appendPath, createDirectoryUri, readFileUri, uriExists, writeFileUri } from "../utils/fs";
10 |
11 | export function canSaveBookmarksInProject(): boolean {
12 | let saveBookmarksInProject: boolean = workspace.getConfiguration("numberedBookmarks").get("saveBookmarksInProject", false);
13 |
14 | // really use saveBookmarksInProject
15 | // 0. has at least a folder opened
16 | // 1. is a valid workspace/folder
17 | // 2. has only one workspaceFolder
18 | // let hasBookmarksFile: boolean = false;
19 | if (saveBookmarksInProject && !workspace.workspaceFolders) {
20 | saveBookmarksInProject = false;
21 | }
22 |
23 | return saveBookmarksInProject;
24 | }
25 |
26 | export async function loadBookmarks(workspaceFolder: WorkspaceFolder): Promise {
27 | const saveBookmarksInProject: boolean = canSaveBookmarksInProject();
28 |
29 | const newController = new Controller(workspaceFolder);
30 |
31 | if (saveBookmarksInProject) {
32 | const bookmarksFileInProject = appendPath(appendPath(workspaceFolder.uri, ".vscode"), "numbered-bookmarks.json");
33 | if (! await uriExists(bookmarksFileInProject)) {
34 | return newController;
35 | }
36 |
37 | try {
38 | const contents = await readFileUri(bookmarksFileInProject);
39 | newController.loadFrom(contents, true);
40 | return newController;
41 | } catch (error) {
42 | window.showErrorMessage(l10n.t("Error loading Numbered Bookmarks: {0}", error.toString()));
43 | return newController;
44 | }
45 | } else {
46 | const savedBookmarks = Container.context.workspaceState.get("numberedBookmarks", "");
47 | if (savedBookmarks !== "") {
48 | newController.loadFrom(JSON.parse(savedBookmarks));
49 | }
50 | return newController;
51 | }
52 | }
53 |
54 | export function saveBookmarks(controller: Controller): void {
55 | const saveBookmarksInProject: boolean = canSaveBookmarksInProject();
56 |
57 | if (saveBookmarksInProject) {
58 | const bookmarksFileInProject = appendPath(appendPath(controller.workspaceFolder.uri, ".vscode"), "numbered-bookmarks.json");
59 | if (!uriExists(appendPath(controller.workspaceFolder.uri, ".vscode"))) {
60 | createDirectoryUri(appendPath(controller.workspaceFolder.uri, ".vscode"));
61 | }
62 | writeFileUri(bookmarksFileInProject, JSON.stringify(controller.zip(), null, "\t"));
63 | } else {
64 | Container.context.workspaceState.update("numberedBookmarks", JSON.stringify(controller.zip()));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/package.nls.pt-br.json:
--------------------------------------------------------------------------------
1 | {
2 | "numberedBookmarks.commands.toggleBookmark0.title": "Alternar Bookmark 0",
3 | "numberedBookmarks.commands.toggleBookmark1.title": "Alternar Bookmark 1",
4 | "numberedBookmarks.commands.toggleBookmark2.title": "Alternar Bookmark 2",
5 | "numberedBookmarks.commands.toggleBookmark3.title": "Alternar Bookmark 3",
6 | "numberedBookmarks.commands.toggleBookmark4.title": "Alternar Bookmark 4",
7 | "numberedBookmarks.commands.toggleBookmark5.title": "Alternar Bookmark 5",
8 | "numberedBookmarks.commands.toggleBookmark6.title": "Alternar Bookmark 6",
9 | "numberedBookmarks.commands.toggleBookmark7.title": "Alternar Bookmark 7",
10 | "numberedBookmarks.commands.toggleBookmark8.title": "Alternar Bookmark 8",
11 | "numberedBookmarks.commands.toggleBookmark9.title": "Alternar Bookmark 9",
12 | "numberedBookmarks.commands.jumpToBookmark0.title": "Pular para Bookmark 0",
13 | "numberedBookmarks.commands.jumpToBookmark1.title": "Pular para Bookmark 1",
14 | "numberedBookmarks.commands.jumpToBookmark2.title": "Pular para Bookmark 2",
15 | "numberedBookmarks.commands.jumpToBookmark3.title": "Pular para Bookmark 3",
16 | "numberedBookmarks.commands.jumpToBookmark4.title": "Pular para Bookmark 4",
17 | "numberedBookmarks.commands.jumpToBookmark5.title": "Pular para Bookmark 5",
18 | "numberedBookmarks.commands.jumpToBookmark6.title": "Pular para Bookmark 6",
19 | "numberedBookmarks.commands.jumpToBookmark7.title": "Pular para Bookmark 7",
20 | "numberedBookmarks.commands.jumpToBookmark8.title": "Pular para Bookmark 8",
21 | "numberedBookmarks.commands.jumpToBookmark9.title": "Pular para Bookmark 9",
22 | "numberedBookmarks.commands.list.title": "Listar",
23 | "numberedBookmarks.commands.listFromAllFiles.title": "Listar de Todos os Arquivos",
24 | "numberedBookmarks.commands.clear.title": "Limpar",
25 | "numberedBookmarks.commands.clearFromAllFiles.title": "Limpar de Todos os Arquivos",
26 | "numberedBookmarks.commands.whatsNew.title": "Novidades",
27 | "numberedBookmarks.commands.whatsNewContextMenu.title": "Novidades",
28 | "numberedBookmarks.context.toggle.label": "Numbered Bookmarks: Alternar",
29 | "numberedBookmarks.context.jump.label": "Numbered Bookmarks: Pular",
30 | "numberedBookmarks.configuration.title": "Numbered Bookmarks",
31 | "numberedBookmarks.configuration.saveBookmarksInProject.description": "Permite que os Bookmarks sejam salvos (e restaurados) localmente no Projeto/Pasta aberto ao invés do VS Code",
32 | "numberedBookmarks.configuration.experimental.enableNewStickyEngine.description": "Experimental. Ativa o novo motor Sticky engine com suporte a Formatadores, detecção de mudança de fonte e operações de desfazer melhoradas",
33 | "numberedBookmarks.configuration.keepBookmarksOnLineDelete.description": "Determina se bookmarks em linhas excluídas devem ser mantidos no arquivo, movendo-os para a próxima linha, ao invés de excluídos com a linha onde se encontravam",
34 | "numberedBookmarks.configuration.showBookmarkNotDefinedWarning.description": "Define se uma notificação deve ser apresentada quando o Bookmark não está definido.",
35 | "numberedBookmarks.configuration.navigateThroughAllFiles.description": "Permite que a navegação apresente Bookmarks em todos os arquivos do projeto, ao invés de apenas no arquivo corrente",
36 | "numberedBookmarks.configuration.gutterIconFillColor.description": "Cor de fundo do ícone de Bookmark",
37 | "numberedBookmarks.configuration.gutterIconNumberColor.description": "Cor do número no ícone de Bookmark",
38 | "numberedBookmarks.configuration.revealLocation.description": "Especifica o local onde a linha com bookmark será exibida",
39 | "numberedBookmarks.configuration.revealLocation.enumDescriptions.top": "Exibe a linha com bookmark no topo do editor",
40 | "numberedBookmarks.configuration.revealLocation.enumDescriptions.center": "Exibe a linha com bookmark no centro do editor",
41 | "numberedBookmarks.colors.lineBackground.description": "Cor de fundo para linha com Bookmark",
42 | "numberedBookmarks.colors.lineBorder.description": "Cor de fundo da borda ao redor da linha com Bookmark",
43 | "numberedBookmarks.colors.overviewRuler.description": "Cor do marcador de régua com Bookmarks"
44 | }
--------------------------------------------------------------------------------
/src/core/operations.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { Uri, workspace, WorkspaceFolder } from "vscode";
7 | import { Bookmark, BookmarkQuickPickItem } from "./bookmark";
8 | import { MAX_BOOKMARKS, NO_BOOKMARK_DEFINED } from "./constants";
9 | import { uriExists } from "../utils/fs";
10 | import { File } from "./file";
11 |
12 | export function isBookmarkDefined(bookmark: Bookmark): boolean {
13 | return bookmark.line !== NO_BOOKMARK_DEFINED.line;
14 | }
15 |
16 | export function hasBookmarks(file: File): boolean {
17 | let hasAny = false;
18 | for (const element of file.bookmarks) {
19 | hasAny = isBookmarkDefined(element);
20 | if (hasAny) {
21 | break;
22 | }
23 | }
24 | return hasAny;
25 | }
26 |
27 | export function listBookmarks(file: File, workspaceFolder: WorkspaceFolder) {
28 |
29 | // eslint-disable-next-line no-async-promise-executor
30 | return new Promise(async (resolve, reject) => {
31 |
32 | if (!hasBookmarks(file)) {
33 | resolve({});
34 | return;
35 | }
36 |
37 | let uriDocBookmark: Uri;
38 | if (file.uri) {
39 | uriDocBookmark = file.uri;
40 | } else {
41 | if (!workspaceFolder) {
42 | uriDocBookmark = Uri.file(file.path);
43 | } else {
44 | const prefix = workspaceFolder.uri.path.endsWith("/")
45 | ? workspaceFolder.uri.path
46 | : `${workspaceFolder.uri.path}/`;
47 | uriDocBookmark = workspaceFolder.uri.with({
48 | path: `${prefix}/${file.path}`
49 | });
50 | }
51 | }
52 |
53 | if (! await uriExists(uriDocBookmark)) {
54 | resolve({});
55 | return;
56 | }
57 |
58 | workspace.openTextDocument(uriDocBookmark).then(doc => {
59 |
60 | const items: BookmarkQuickPickItem[] = [];
61 | const invalids = [];
62 | for (const element of file.bookmarks) {
63 | // fix for modified files
64 | if (isBookmarkDefined(element)) {
65 | if (element.line <= doc.lineCount) {
66 | const bookmarkLine = element.line + 1;
67 | const bookmarkColumn = element.column + 1;
68 | const lineText = doc.lineAt(bookmarkLine - 1).text.trim();
69 | items.push({
70 | label: lineText,
71 | description: "(Ln " + bookmarkLine.toString() + ", Col " +
72 | bookmarkColumn.toString() + ")",
73 | detail: file.path,
74 | uri: uriDocBookmark
75 | });
76 | } else {
77 | invalids.push(element);
78 | }
79 | }
80 | }
81 |
82 | if (invalids.length > 0) {
83 | // tslint:disable-next-line:prefer-for-of
84 | for (let indexI = 0; indexI < invalids.length; indexI++) {
85 | file.bookmarks[ invalids[ indexI ] ] = NO_BOOKMARK_DEFINED;
86 | }
87 | }
88 |
89 | resolve(items);
90 | return;
91 | });
92 | });
93 | }
94 |
95 | export function clearBookmarks(file: File) {
96 | for (let index = 0; index < MAX_BOOKMARKS; index++) {
97 | file.bookmarks[ index ] = NO_BOOKMARK_DEFINED;
98 | }
99 | }
100 |
101 | export function indexOfBookmark(file: File, line: number): number {
102 | for (let index = 0; index < file.bookmarks.length; index++) {
103 | const bookmark = file.bookmarks[index];
104 | if (bookmark.line === line) {
105 | return index;
106 | }
107 | }
108 | return -1;
109 | }
110 |
--------------------------------------------------------------------------------
/src/utils/fs.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import os = require("os");
7 | import path = require("path");
8 | import { Uri, workspace, WorkspaceFolder } from "vscode";
9 | import { Bookmark } from "../core/bookmark";
10 | import { UNTITLED_SCHEME } from "../core/constants";
11 | import { File } from "../core/file";
12 |
13 | export function getRelativePath(folder: string, filePath: string) {
14 | if (!folder) {
15 | return filePath;
16 | }
17 |
18 | let relativePath = path.relative(folder, filePath);
19 |
20 | // multiplatform
21 | if (os.platform() === "win32") {
22 | relativePath = relativePath.replace(/\\/g, "/");
23 | }
24 |
25 | return relativePath;
26 | }
27 |
28 | export function appendPath(uri: Uri, pathSuffix: string): Uri {
29 | const pathPrefix = uri.path.endsWith("/") ? uri.path : `${uri.path}/`;
30 | const filePath = `${pathPrefix}${pathSuffix}`;
31 |
32 | return uri.with({
33 | path: filePath
34 | });
35 | }
36 |
37 | export function uriJoin(uri: Uri, ...paths: string[]): string {
38 | return path.join(uri.fsPath, ...paths);
39 | }
40 |
41 | export function uriWith(uri: Uri, prefix: string, filePath: string): Uri {
42 | const newPrefix = prefix === "/"
43 | ? ""
44 | : prefix;
45 |
46 | return uri.with({
47 | path: `${newPrefix}/${filePath}`
48 | });
49 | }
50 |
51 | export async function uriExists(uri: Uri): Promise {
52 |
53 | if (uri.scheme === UNTITLED_SCHEME) {
54 | return true;
55 | }
56 |
57 | try {
58 | await workspace.fs.stat(uri);
59 | return true;
60 | } catch {
61 | return false;
62 | }
63 | }
64 |
65 | export async function uriDelete(uri: Uri): Promise {
66 |
67 | if (uri.scheme === UNTITLED_SCHEME) {
68 | return true;
69 | }
70 |
71 | try {
72 | await workspace.fs.delete(uri);
73 | return true;
74 | } catch {
75 | return false;
76 | }
77 | }
78 |
79 | export async function fileExists(filePath: string): Promise {
80 | try {
81 | await workspace.fs.stat(Uri.parse(filePath));
82 | return true;
83 | } catch {
84 | return false;
85 | }
86 | }
87 |
88 | export async function createDirectoryUri(uri: Uri): Promise {
89 | return workspace.fs.createDirectory(uri);
90 | }
91 |
92 | export async function createDirectory(dir: string): Promise {
93 | return workspace.fs.createDirectory(Uri.parse(dir));
94 | }
95 |
96 | export async function readFile(filePath: string): Promise {
97 | const bytes = await workspace.fs.readFile(Uri.parse(filePath));
98 | return JSON.parse(new TextDecoder('utf-8').decode(bytes));
99 | }
100 |
101 | export async function readFileUri(uri: Uri): Promise {
102 | const bytes = await workspace.fs.readFile(uri);
103 | return JSON.parse(new TextDecoder('utf-8').decode(bytes));
104 | }
105 |
106 | export async function readRAWFileUri(uri: Uri): Promise {
107 | const bytes = await workspace.fs.readFile(uri);
108 | return new TextDecoder('utf-8').decode(bytes);
109 | }
110 |
111 | export async function writeFile(filePath: string, contents: string): Promise {
112 | const writeData = new TextEncoder().encode(contents);
113 | await workspace.fs.writeFile(Uri.parse(filePath), writeData);
114 | }
115 |
116 | export async function writeFileUri(uri: Uri, contents: string): Promise {
117 | const writeData = new TextEncoder().encode(contents);
118 | await workspace.fs.writeFile(uri, writeData);
119 | }
120 |
121 | export function parsePosition(position: string): Bookmark | undefined {
122 | const re = new RegExp(/\(Ln\s(\d+),\sCol\s(\d+)\)/);
123 | const matches = re.exec(position);
124 | if (matches) {
125 | return {
126 | line: parseInt(matches[ 1 ], 10),
127 | column: parseInt(matches[ 2 ], 10)
128 | };
129 | }
130 | return undefined;
131 | }
132 |
133 | export function getFileUri(file: File, workspaceFolder: WorkspaceFolder): Uri {
134 | if (file.uri) {
135 | return file.uri;
136 | }
137 |
138 | if (!workspaceFolder) {
139 | return Uri.file(file.path);
140 | }
141 |
142 | const prefix = workspaceFolder.uri.path.endsWith("/")
143 | ? workspaceFolder.uri.path
144 | : `${workspaceFolder.uri.path}/`;
145 | return uriWith(workspaceFolder.uri, prefix, file.path);
146 | }
147 |
--------------------------------------------------------------------------------
/src/decoration/decoration.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { DecorationRenderOptions, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, ThemeColor, Uri, workspace, window } from "vscode";
7 | import { createLineDecoration } from "vscode-ext-decoration";
8 | import { DEFAULT_GUTTER_ICON_NUMBER_COLOR, DEFAULT_GUTTER_ICON_FILL_COLOR, MAX_BOOKMARKS, NO_BOOKMARK_DEFINED } from "../core/constants";
9 | import { File } from "../core/file";
10 | import { clearBookmarks } from "../core/operations";
11 |
12 | function createGutterRulerDecoration(
13 | overviewRulerLane?: OverviewRulerLane,
14 | overviewRulerColor?: string | ThemeColor,
15 | gutterIconPath?: string | Uri): TextEditorDecorationType {
16 |
17 | const decorationOptions: DecorationRenderOptions = {
18 | gutterIconPath,
19 | overviewRulerLane,
20 | overviewRulerColor
21 | };
22 |
23 | decorationOptions.isWholeLine = false;
24 |
25 | return window.createTextEditorDecorationType(decorationOptions);
26 | }
27 |
28 | export interface TextEditorDecorationTypePair {
29 | gutterDecoration: TextEditorDecorationType;
30 | lineDecoration: TextEditorDecorationType;
31 | }
32 |
33 | export function createBookmarkDecorations(): TextEditorDecorationTypePair[] {
34 | const decorators: TextEditorDecorationTypePair[] = [];
35 | for (let number = 0; number <= 9; number++) {
36 | const iconFillColor = workspace.getConfiguration("numberedBookmarks").get("gutterIconFillColor", DEFAULT_GUTTER_ICON_FILL_COLOR);
37 | const iconNumberColor = workspace.getConfiguration("numberedBookmarks").get("gutterIconNumberColor", DEFAULT_GUTTER_ICON_NUMBER_COLOR);
38 | const iconPath = Uri.parse(
39 | `data:image/svg+xml,${encodeURIComponent(
40 | ``,
41 | )}`,
42 | );
43 |
44 | const overviewRulerColor = new ThemeColor('numberedBookmarks.overviewRuler');
45 | const lineBackground = new ThemeColor('numberedBookmarks.lineBackground');
46 | const lineBorder = new ThemeColor('numberedBookmarks.lineBorder');
47 |
48 | const gutterDecoration = createGutterRulerDecoration(OverviewRulerLane.Full, overviewRulerColor, iconPath);
49 | const lineDecoration = createLineDecoration(lineBackground, lineBorder);
50 | decorators.push( { gutterDecoration, lineDecoration });
51 | }
52 | return decorators;
53 | }
54 |
55 | export function updateDecorationsInActiveEditor(activeEditor: TextEditor, activeBookmark: File,
56 | getDecorationPair: (n: number) => TextEditorDecorationTypePair) {
57 |
58 | if (!activeEditor) {
59 | return;
60 | }
61 |
62 | if (!activeBookmark) {
63 | return;
64 | }
65 |
66 | let books: Range[] = [];
67 | // Remove all bookmarks if active file is empty
68 | if (activeEditor.document.lineCount === 1 && activeEditor.document.lineAt(0).text === "") {
69 | clearBookmarks(activeBookmark);
70 | } else {
71 | const invalids = [];
72 | for (let index = 0; index < MAX_BOOKMARKS; index++) {
73 | books = [];
74 | if (activeBookmark.bookmarks[ index ].line < 0) {
75 | const decors = getDecorationPair(index);
76 | activeEditor.setDecorations(decors.gutterDecoration, books);
77 | activeEditor.setDecorations(decors.lineDecoration, books);
78 | } else {
79 | const element = activeBookmark.bookmarks[ index ];
80 | if (element.line < activeEditor.document.lineCount) {
81 | const decoration = new Range(element.line, 0, element.line, 0);
82 | books.push(decoration);
83 | const decors = getDecorationPair(index);
84 | activeEditor.setDecorations(decors.gutterDecoration, books);
85 | activeEditor.setDecorations(decors.lineDecoration, books);
86 | } else {
87 | invalids.push(index);
88 | }
89 | }
90 | }
91 |
92 | if (invalids.length > 0) {
93 | // tslint:disable-next-line:prefer-for-of
94 | for (let indexI = 0; indexI < invalids.length; indexI++) {
95 | activeBookmark.bookmarks[ invalids[ indexI ] ] = NO_BOOKMARK_DEFINED;
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/package.nls.json:
--------------------------------------------------------------------------------
1 | {
2 | "numberedBookmarks.commands.toggleBookmark0.title": "Toggle Bookmark 0",
3 | "numberedBookmarks.commands.toggleBookmark1.title": "Toggle Bookmark 1",
4 | "numberedBookmarks.commands.toggleBookmark2.title": "Toggle Bookmark 2",
5 | "numberedBookmarks.commands.toggleBookmark3.title": "Toggle Bookmark 3",
6 | "numberedBookmarks.commands.toggleBookmark4.title": "Toggle Bookmark 4",
7 | "numberedBookmarks.commands.toggleBookmark5.title": "Toggle Bookmark 5",
8 | "numberedBookmarks.commands.toggleBookmark6.title": "Toggle Bookmark 6",
9 | "numberedBookmarks.commands.toggleBookmark7.title": "Toggle Bookmark 7",
10 | "numberedBookmarks.commands.toggleBookmark8.title": "Toggle Bookmark 8",
11 | "numberedBookmarks.commands.toggleBookmark9.title": "Toggle Bookmark 9",
12 | "numberedBookmarks.commands.jumpToBookmark0.title": "Jump to Bookmark 0",
13 | "numberedBookmarks.commands.jumpToBookmark1.title": "Jump to Bookmark 1",
14 | "numberedBookmarks.commands.jumpToBookmark2.title": "Jump to Bookmark 2",
15 | "numberedBookmarks.commands.jumpToBookmark3.title": "Jump to Bookmark 3",
16 | "numberedBookmarks.commands.jumpToBookmark4.title": "Jump to Bookmark 4",
17 | "numberedBookmarks.commands.jumpToBookmark5.title": "Jump to Bookmark 5",
18 | "numberedBookmarks.commands.jumpToBookmark6.title": "Jump to Bookmark 6",
19 | "numberedBookmarks.commands.jumpToBookmark7.title": "Jump to Bookmark 7",
20 | "numberedBookmarks.commands.jumpToBookmark8.title": "Jump to Bookmark 8",
21 | "numberedBookmarks.commands.jumpToBookmark9.title": "Jump to Bookmark 9",
22 | "numberedBookmarks.commands.list.title": "List",
23 | "numberedBookmarks.commands.listFromAllFiles.title": "List from All Files",
24 | "numberedBookmarks.commands.clear.title": "Clear",
25 | "numberedBookmarks.commands.clearFromAllFiles.title": "Clear from All Files",
26 | "numberedBookmarks.commands.whatsNew.title": "What's New",
27 | "numberedBookmarks.commands.whatsNewContextMenu.title": "What's New",
28 | "numberedBookmarks.context.toggle.label": "Numbered Bookmarks: Toggle",
29 | "numberedBookmarks.context.jump.label": "Numbered Bookmarks: Jump",
30 | "numberedBookmarks.configuration.title": "Numbered Bookmarks",
31 | "numberedBookmarks.configuration.saveBookmarksInProject.description": "Allow bookmarks to be saved (and restored) locally in the opened Project/Folder instead of VS Code",
32 | "numberedBookmarks.configuration.experimental.enableNewStickyEngine.description": "Experimental. Enables the new Sticky engine with support for Formatters, improved source change detections and undo operations",
33 | "numberedBookmarks.configuration.keepBookmarksOnLineDelete.description": "Specifies whether bookmarks on deleted line should be kept on file, moving it down to the next line, instead of deleting it with the line where it was toggled.",
34 | "numberedBookmarks.configuration.showBookmarkNotDefinedWarning.description": "Controls whether to show a warning when a bookmark is not defined",
35 | "numberedBookmarks.configuration.navigateThroughAllFiles.description": "Allow navigation look for bookmarks in all files in the project, instead of only the current",
36 | "numberedBookmarks.configuration.gutterIconFillColor.description": "Specify the color to use on gutter icon (fill color)",
37 | "numberedBookmarks.configuration.gutterIconNumberColor.description": "Specify the color to use on gutter icon (number color)",
38 | "numberedBookmarks.configuration.revealLocation.description": "Specifies the location where the bookmarked line will be revealed",
39 | "numberedBookmarks.configuration.revealLocation.enumDescriptions.top": "Reveals the bookmarked line at the top of the editor",
40 | "numberedBookmarks.configuration.revealLocation.enumDescriptions.center": "Reveals the bookmarked line in the center of the editor",
41 | "numberedBookmarks.colors.lineBackground.description": "Background color for the bookmarked line",
42 | "numberedBookmarks.colors.lineBorder.description": "Background color for the border around the bookmarked line",
43 | "numberedBookmarks.colors.overviewRuler.description": "Overview ruler marker color for bookmarks",
44 | "numberedBookmarks.walkthroughs.title": "Get Started with Numbered Bookmarks",
45 | "numberedBookmarks.walkthroughs.description": "Learn more about Numbered Bookmarks to optimize your workflow",
46 | "numberedBookmarks.walkthroughs.toggle.title": "Toggle Numbered Bookmarks",
47 | "numberedBookmarks.walkthroughs.toggle.description": "Easily Mark/Unmark Numbered Bookmarks at any position.\nAn icon is added to both the gutter and overview ruler to easily identify the lines with Numbered Bookmarks.",
48 | "numberedBookmarks.walkthroughs.navigateToNumberedBookmarks.title": "Navigate to Numbered Bookmarks",
49 | "numberedBookmarks.walkthroughs.navigateToNumberedBookmarks.description": "Quickly jump between bookmarked lines.\nSearch numbered bookmarks using the line's content and/or position.",
50 | "numberedBookmarks.walkthroughs.inspiredInDelphiButOpenToOthers.title": "Inspired in Delphi, but open to others",
51 | "numberedBookmarks.walkthroughs.inspiredInDelphiButOpenToOthers.description": "The extension was inspired in Delphi, because I was a Delphi developer for a long time, and love it's bookmarks, but it supports different editors as well.",
52 | "numberedBookmarks.walkthroughs.workingWithRemotes.title": "Working with Remotes",
53 | "numberedBookmarks.walkthroughs.workingWithRemotes.description": "The extension support [Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) scenarios. Even installed locally, you can use Numbered Bookmarks in WSL, Containers, SSH and Codespaces.",
54 | "numberedBookmarks.walkthroughs.customizingAppearance.title": "Customizing Appearance",
55 | "numberedBookmarks.walkthroughs.customizingAppearance.description": "Customize how Numbered Bookmarks are presented, its icon, line and overview ruler\n[Open Settings - Gutter Icon](command:workbench.action.openSettings?%5B%22numberedBookmarks.gutterIcon%22%5D)\n[Open Settings - Line](command:workbench.action.openSettingsJson?%5B%22workbench.colorCustomizations%22%5D)"
56 |
57 | }
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # Copilot Instructions for Numbered Bookmarks
2 |
3 | ## Project Overview
4 |
5 | Numbered Bookmarks is a Visual Studio Code extension that helps users navigate code by marking and jumping to important positions with numbered bookmarks (0-9), inspired by Delphi IDE.
6 |
7 | ## Technology Stack
8 |
9 | - **Language**: TypeScript
10 | - **Target Platform**: VS Code Extension (Node.js)
11 | - **Build Tool**: Webpack 5
12 | - **Testing Framework**: Mocha with @vscode/test-electron
13 | - **Minimum VS Code Version**: 1.73.0
14 | - **License**: GPL-3.0
15 |
16 | ## Project Structure
17 |
18 | ```
19 | src/
20 | ├── core/ # Core bookmark logic (Controller, File, Bookmark, operations)
21 | ├── decoration/ # Gutter and line decorations
22 | ├── sticky/ # Sticky bookmark engine (legacy and new)
23 | ├── storage/ # Workspace state persistence
24 | ├── quickpick/ # Quick pick UI components
25 | ├── utils/ # Utility functions (fs, reveal, revealLocation)
26 | ├── whats-new/ # What's New feature
27 | └── extension.ts # Main extension entry point
28 | ```
29 |
30 | ## Build and Test Commands
31 |
32 | ### Build
33 | - `npm run build` - Build for development with webpack
34 | - `npm run watch` - Watch mode for development
35 | - `npm run compile` - TypeScript compilation only
36 | - `npm run webpack` - Webpack build (development mode)
37 | - `npm run vscode:prepublish` - Production build (runs before publishing)
38 |
39 | ### Test
40 | - `npm test` - Run full test suite (compile, lint, and test)
41 | - `npm run just-test` - Run tests only (no compile or lint)
42 | - `npm run pretest` - Compile and lint before testing
43 | - `npm run lint` - Run ESLint on TypeScript files
44 |
45 | ### Development
46 | - `npm run webpack-dev` - Watch mode with webpack
47 |
48 | ## Code Style and Conventions
49 |
50 | ### General
51 | - Use 4 spaces for indentation (not tabs)
52 | - Use double quotes for strings
53 | - Include copyright header in all source files:
54 | ```typescript
55 | /*---------------------------------------------------------------------------------------------
56 | * Copyright (c) Alessandro Fragnani. All rights reserved.
57 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
58 | *--------------------------------------------------------------------------------------------*/
59 | ```
60 |
61 | ### TypeScript
62 | - Target: ES2020
63 | - Use strict mode (`alwaysStrict: true`)
64 | - Prefer explicit types over `any`
65 | - Use modern ES6+ features (arrow functions, const/let, template literals)
66 | - Import from 'vscode' module for VS Code API
67 | - Use named imports where possible
68 |
69 | ### Naming Conventions
70 | - Classes: PascalCase (e.g., `Controller`, `BookmarkQuickPickItem`)
71 | - Functions/Methods: camelCase (e.g., `loadBookmarks`, `isBookmarkDefined`)
72 | - Constants: UPPER_SNAKE_CASE (e.g., `NO_BOOKMARK_DEFINED`, `UNTITLED_SCHEME`)
73 | - Private class members: camelCase without underscore prefix
74 |
75 | ### Architecture Patterns
76 | - **Controller Pattern**: `Controller` class manages files and bookmarks per workspace folder
77 | - **File Pattern**: `File` class represents a document with its bookmarks
78 | - **Bookmark Pattern**: `Bookmark` interface with line and column positions
79 | - **Sticky Engine**: Two implementations (legacy and new) for maintaining bookmark positions during edits
80 | - **Decoration Pattern**: Separate decoration types for gutter icons and line backgrounds
81 |
82 | ## Extension Features to Maintain
83 |
84 | 1. **Toggle Bookmarks**: Commands 0-9 to mark/unmark positions
85 | 2. **Jump to Bookmarks**: Commands 0-9 to navigate to marked positions
86 | 3. **List Commands**: Show bookmarks from current file or all files
87 | 4. **Clear Commands**: Remove bookmarks from current file or all files
88 | 5. **Multi-root Workspace Support**: Manage bookmarks per workspace folder
89 | 6. **Remote Development Support**: Works with Docker, SSH, WSL
90 | 7. **Sticky Bookmarks**: Maintain positions during code edits
91 | 8. **Customizable Appearance**: Gutter icons, line backgrounds, colors
92 | 9. **Persistence**: Save bookmarks in workspace state or project files
93 |
94 | ## Configuration Settings
95 |
96 | Key settings in `package.json`:
97 | - `saveBookmarksInProject`: Save in `.vscode/numbered-bookmarks.json`
98 | - `experimental.enableNewStickyEngine`: Use new sticky engine (default: true)
99 | - `keepBookmarksOnLineDelete`: Move bookmarks to next line on delete
100 | - `showBookmarkNotDefinedWarning`: Show warning when bookmark not defined
101 | - `navigateThroughAllFiles`: How to navigate across files (false/replace/allowDuplicates)
102 | - `gutterIconFillColor`: Gutter icon background color
103 | - `gutterIconNumberColor`: Gutter icon number color
104 | - `revealLocation`: Where to reveal bookmarked line (top/center)
105 |
106 | ## Testing Guidelines
107 |
108 | - Use Mocha test framework with `suite` and `test` functions
109 | - Import VS Code API for extension testing
110 | - Test extension activation and core functionality
111 | - Use async/await for asynchronous operations
112 | - Include timeout utilities for async tests
113 |
114 | ## Important Notes
115 |
116 | - **Bookmark #0**: Reactivated but has no keyboard shortcut due to OS limitations
117 | - **macOS Shortcuts**: Use `Cmd` instead of `Ctrl` for some shortcuts (Cmd+Shift+3, Cmd+Shift+4)
118 | - **Untitled Files**: Special handling for untitled documents
119 | - **Path Handling**: Cross-platform path handling (Windows backslashes vs. Unix forward slashes)
120 | - **Localization**: Support for multiple languages via l10n and package.nls.*.json files
121 | - **Walkthrough**: Extension includes a getting started walkthrough
122 |
123 | ## Dependencies
124 |
125 | ### Production
126 | - `vscode-ext-codicons`: Codicon support
127 | - `vscode-ext-decoration`: Decoration utilities
128 | - `os-browserify`, `path-browserify`: Polyfills for browser compatibility
129 |
130 | ### Development
131 | - ESLint with `eslint-config-vscode-ext`
132 | - TypeScript ^4.4.4
133 | - Webpack 5 with ts-loader and terser plugin
134 |
135 | ## Common Tasks
136 |
137 | ### Adding a New Command
138 | 1. Add command definition in `package.json` > `contributes.commands`
139 | 2. Add localized string in `package.nls.json`
140 | 3. Register command handler in `src/extension.ts`
141 | 4. Add keyboard binding if needed in `package.json` > `contributes.keybindings`
142 | 5. Update menu contributions if applicable
143 |
144 | ### Modifying Bookmark Behavior
145 | - Core logic in `src/core/operations.ts`
146 | - Sticky behavior in `src/sticky/sticky.ts` or `src/sticky/stickyLegacy.ts`
147 | - Visual updates in `src/decoration/decoration.ts`
148 |
149 | ### Adding Tests
150 | - Create test files in `src/test/suite/`
151 | - Follow existing test patterns with Mocha
152 | - Update test suite index if needed
153 |
154 | ## External Resources
155 |
156 | - Extension published to VS Code Marketplace and Open VSX
157 | - Documentation in README.md
158 | - Changelog in CHANGELOG.md
159 | - Submodule: vscode-whats-new for What's New feature
160 |
--------------------------------------------------------------------------------
/src/core/controller.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import os = require("os");
7 | import path = require("path");
8 | import { Uri, WorkspaceFolder } from "vscode";
9 | import { File } from "./file";
10 | import { getFileUri, getRelativePath } from "../utils/fs";
11 | import { createFile } from "./file";
12 | import { isBookmarkDefined } from "./operations";
13 | import { UNTITLED_SCHEME } from "./constants";
14 |
15 | export class Controller {
16 | public files: File[];
17 | public workspaceFolder: WorkspaceFolder | undefined;
18 |
19 | constructor(workspaceFolder: WorkspaceFolder | undefined) {
20 | this.workspaceFolder = workspaceFolder;
21 | this.files = [];
22 | }
23 |
24 | public loadFrom(jsonObject, relativePath?) {
25 | if (jsonObject === "") {
26 | return;
27 | }
28 |
29 | // OLD format
30 | if ((jsonObject.bookmarks)) {
31 | const jsonBookmarks = jsonObject.bookmarks;
32 | // tslint:disable-next-line: prefer-for-of
33 | for (let idx = 0; idx < jsonBookmarks.length; idx++) {
34 | const jsonBookmark = jsonBookmarks[ idx ];
35 |
36 | // untitled files, ignore (for now)
37 | const ff = jsonBookmark.fsPath;
38 | if (ff.match(/Untitled-\d+/)) {
39 | continue;
40 | }
41 |
42 | const file = relativePath
43 | ? createFile(jsonBookmark.fsPath.replace(`$ROOTPATH$${path.sep}`, ""))
44 | : createFile(getRelativePath(this.workspaceFolder?.uri?.fsPath, jsonBookmark.fsPath));
45 |
46 | // Win32 uses `\\` but uris always uses `/`
47 | if (os.platform() === "win32") {
48 | file.path = file.path.replace(/\\/g, "/");
49 | }
50 |
51 | const bmksPosition = jsonBookmark.bookmarks.map(bmk => {
52 | return {line: bmk, column: 0}
53 | });
54 | file.bookmarks = [
55 | ...bmksPosition
56 | ]
57 | this.files.push(file);
58 | }
59 | } else { // NEW format
60 | for (const file of jsonObject.files) {
61 |
62 | const bookmark = createFile(file.path);//??, file.uri ? file.uri : undefined);
63 | bookmark.bookmarks = [
64 | ...file.bookmarks
65 | ]
66 | this.files.push(bookmark);
67 | }
68 | }
69 | }
70 |
71 | public fromUri(uri: Uri) {
72 | if (uri.scheme === UNTITLED_SCHEME) {
73 | for (const file of this.files) {
74 | if (file.uri?.toString() === uri.toString()) {
75 | return file;
76 | }
77 | }
78 | return;
79 | }
80 |
81 | const uriPath = !this.workspaceFolder
82 | ? uri.path
83 | : getRelativePath(this.workspaceFolder.uri.path, uri.path);
84 |
85 | for (const file of this.files) {
86 | if (file.path === uriPath) {
87 | return file;
88 | }
89 | }
90 | }
91 |
92 | public indexFromPath(filePath: string) {
93 | for (let index = 0; index < this.files.length; index++) {
94 | const file = this.files[index];
95 |
96 | if (file.path === filePath) {
97 | return index;
98 | }
99 | }
100 | }
101 |
102 | public addFile(uri: Uri) {
103 | if (uri.scheme === UNTITLED_SCHEME) {
104 | // let foundFile: File;
105 | // for (const file of this.files) {
106 | // if (file.uri?.toString() === uri.toString()) {
107 | // foundFile = file;
108 | // }
109 | // }
110 |
111 | // if (!foundFile) {
112 | const file = createFile(uri.path, uri);
113 | this.files.push(file);
114 | // }
115 | return;
116 | }
117 |
118 | const uriPath = !this.workspaceFolder
119 | ? uri.path
120 | : getRelativePath(this.workspaceFolder.uri.path, uri.path);
121 |
122 | const paths = this.files.map(file => file.path);
123 | if (paths.indexOf(uriPath) < 0) {
124 | const bookmark = createFile(uriPath);
125 | this.files.push(bookmark);
126 | }
127 | }
128 |
129 | public getFileUri(file: File) {
130 | return getFileUri(file, this.workspaceFolder);
131 | }
132 |
133 | public zip(): Controller {
134 | function isNotEmpty(book: File): boolean {
135 | let hasAny = false;
136 | for (const element of book.bookmarks) {
137 | hasAny = isBookmarkDefined(element);
138 | if (hasAny) {
139 | break;
140 | }
141 | }
142 | return hasAny;
143 | }
144 |
145 | function isValid(file: File): boolean {
146 | return !file.uri ; // Untitled files
147 | }
148 |
149 | function canBeSaved(file: File): boolean {
150 | return isValid(file) && isNotEmpty(file);
151 | }
152 |
153 | const newBookmarks: Controller = new Controller(this.workspaceFolder);
154 | newBookmarks.files = JSON.parse(JSON.stringify(this.files)).filter(canBeSaved);
155 |
156 | delete newBookmarks.workspaceFolder;
157 |
158 | return newBookmarks;
159 | }
160 |
161 | public updateFilePath(oldFilePath: string, newFilePath: string): void {
162 | for (const file of this.files) {
163 | if (file.path === oldFilePath) {
164 | file.path = newFilePath;
165 | break;
166 | }
167 | }
168 | }
169 |
170 | public updateDirectoryPath(oldDirectoryPath: string, newDirectoryPath: string): void {
171 | for (const file of this.files) {
172 | if (file.path.startsWith(oldDirectoryPath)) {
173 | file.path = file.path.replace(oldDirectoryPath, newDirectoryPath);
174 | }
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/whats-new/contentProvider.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Alessandro Fragnani. All rights reserved.
3 | * Licensed under the GPLv3 License. See License.md in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { ChangeLogItem, ChangeLogKind, ContentProvider, Header, Image, Sponsor, IssueKind, SupportChannel, SocialMediaProvider } from "../../vscode-whats-new/src/ContentProvider";
7 |
8 | export class NumberedBookmarksContentProvider implements ContentProvider {
9 | public provideHeader(logoUrl: string): Header {
10 | return {logo: {src: logoUrl, height: 50, width: 50},
11 | message: `Numbered Bookmarks helps you to navigate in your code, moving
12 | between important positions easily and quickly. No more need
13 | to search for code. All of this in Delphi style`};
14 | }
15 |
16 | public provideChangeLog(): ChangeLogItem[] {
17 | const changeLog: ChangeLogItem[] = [];
18 |
19 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "9.0.0", releaseDate: "November 2025" } });
20 | changeLog.push({
21 | kind: ChangeLogKind.NEW,
22 | detail: {
23 | message: "Fully Open Source again",
24 | id: 131,
25 | kind: IssueKind.Issue
26 | }
27 | });
28 | changeLog.push({
29 | kind: ChangeLogKind.FIXED,
30 | detail: {
31 | message: "Reuse opened file",
32 | id: 180,
33 | kind: IssueKind.Issue
34 | }
35 | });
36 | changeLog.push({
37 | kind: ChangeLogKind.INTERNAL,
38 | detail: {
39 | message: "Security Alert: word-wrap",
40 | id: 167,
41 | kind: IssueKind.PR,
42 | kudos: "dependabot"
43 | }
44 | });
45 | changeLog.push({
46 | kind: ChangeLogKind.INTERNAL,
47 | detail: {
48 | message: "Security Alert: webpack",
49 | id: 178,
50 | kind: IssueKind.PR,
51 | kudos: "dependabot"
52 | }
53 | });
54 | changeLog.push({
55 | kind: ChangeLogKind.INTERNAL,
56 | detail: {
57 | message: "Security Alert: serialize-javascript",
58 | id: 183,
59 | kind: IssueKind.PR,
60 | kudos: "dependabot"
61 | }
62 | });
63 | changeLog.push({
64 | kind: ChangeLogKind.INTERNAL,
65 | detail: {
66 | message: "Security Alert: braces",
67 | id: 176,
68 | kind: IssueKind.PR,
69 | kudos: "dependabot"
70 | }
71 | });
72 |
73 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "8.5.0", releaseDate: "March 2024" } });
74 | changeLog.push({
75 | kind: ChangeLogKind.NEW,
76 | detail: {
77 | message: "Published to Open VSX",
78 | id: 147,
79 | kind: IssueKind.Issue
80 | }
81 | });
82 | changeLog.push({
83 | kind: ChangeLogKind.NEW,
84 | detail: {
85 | message: "New setting to choose viewport position on navigation",
86 | id: 141,
87 | kind: IssueKind.Issue
88 | }
89 | });
90 |
91 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "8.4.0", releaseDate: "June 2023" } });
92 | changeLog.push({
93 | kind: ChangeLogKind.NEW,
94 | detail: {
95 | message: "Add Getting Started/Walkthrough support",
96 | id: 117,
97 | kind: IssueKind.Issue
98 | }
99 | });
100 | changeLog.push({
101 | kind: ChangeLogKind.NEW,
102 | detail: {
103 | message: "Add Localization (l10n) support",
104 | id: 151,
105 | kind: IssueKind.Issue
106 | }
107 | });
108 | changeLog.push({
109 | kind: ChangeLogKind.CHANGED,
110 | detail: {
111 | message: "Avoid What's New when using Gitpod",
112 | id: 168,
113 | kind: IssueKind.Issue
114 | }
115 | });
116 | changeLog.push({
117 | kind: ChangeLogKind.CHANGED,
118 | detail: {
119 | message: "Avoid What's New when installing lower versions",
120 | id: 168,
121 | kind: IssueKind.Issue
122 | }
123 | });
124 | changeLog.push({
125 | kind: ChangeLogKind.FIXED,
126 | detail: {
127 | message: "Repeated gutter icon on line wrap",
128 | id: 149,
129 | kind: IssueKind.Issue
130 | }
131 | });
132 | changeLog.push({
133 | kind: ChangeLogKind.INTERNAL,
134 | detail: {
135 | message: "Improve Startup speed",
136 | id: 145,
137 | kind: IssueKind.Issue
138 | }
139 | });
140 | changeLog.push({
141 | kind: ChangeLogKind.INTERNAL,
142 | detail: {
143 | message: "Security Alert: webpack",
144 | id: 156,
145 | kind: IssueKind.PR,
146 | kudos: "dependabot"
147 | }
148 | });
149 | changeLog.push({
150 | kind: ChangeLogKind.INTERNAL,
151 | detail: {
152 | message: "Security Alert: terser",
153 | id: 143,
154 | kind: IssueKind.PR,
155 | kudos: "dependabot"
156 | }
157 | });
158 |
159 |
160 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "8.3.1", releaseDate: "June 2022" } });
161 | changeLog.push({
162 | kind: ChangeLogKind.INTERNAL,
163 | detail: "Add GitHub Sponsors support"
164 | });
165 |
166 | changeLog.push({ kind: ChangeLogKind.VERSION, detail: { releaseNumber: "8.3.0", releaseDate: "April 2022" } });
167 | changeLog.push({
168 | kind: ChangeLogKind.NEW,
169 | detail: {
170 | message: "New setting to decide if should delete bookmark if associated line is deleted",
171 | id: 27,
172 | kind: IssueKind.Issue
173 | }
174 | });
175 | changeLog.push({
176 | kind: ChangeLogKind.NEW,
177 | detail: {
178 | message: "Update bookmark reference on file renames",
179 | id: 120,
180 | kind: IssueKind.Issue
181 | }
182 | });
183 | changeLog.push({
184 | kind: ChangeLogKind.CHANGED,
185 | detail: {
186 | message: "Replace custom icons with on the fly approach",
187 | id: 129,
188 | kind: IssueKind.Issue
189 | }
190 | });
191 |
192 | return changeLog;
193 | }
194 |
195 | public provideSupportChannels(): SupportChannel[] {
196 | const supportChannels: SupportChannel[] = [];
197 | supportChannels.push({
198 | title: "Become a sponsor on GitHub",
199 | link: "https://github.com/sponsors/alefragnani",
200 | message: "Become a Sponsor"
201 | });
202 | supportChannels.push({
203 | title: "Donate via PayPal",
204 | link: "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=EP57F3B6FXKTU&lc=US&item_name=Alessandro%20Fragnani&item_number=vscode%20extensions¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted",
205 | message: "Donate via PayPal"
206 | });
207 | return supportChannels;
208 | }
209 | }
210 |
211 | export class NumberedBookmarksSocialMediaProvider implements SocialMediaProvider {
212 | public provideSocialMedias() {
213 | return [{
214 | title: "Follow me on Twitter",
215 | link: "https://www.twitter.com/alefragnani"
216 | }];
217 | }
218 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # What's new in Numbered Bookmarks 9
7 |
8 | * Fully Open Source again
9 | * Published to **Open VSX**
10 | * Adds **Getting Started / Walkthrough**
11 | * Adds **Rename file** support
12 | * New **Sticky Engine**
13 |
14 | # Support
15 |
16 | **Numbered Bookmarks** is an extension created for **Visual Studio Code**. If you find it useful, please consider supporting it.
17 |
18 |