├── .gitignore
├── resources
├── gutter_icons
│ └── placeholder.txt
├── editor.gif
├── footer.gif
├── activity_bar.gif
├── vsc-labeled-bookmarks-logo.png
├── gutter_icon_c.svg
├── gutter_icon_l.svg
├── gutter_icon_bm.svg
├── gutter_icon_unicode.svg
├── gutter_icon_h.svg
├── gutter_icon_st.svg
├── gutter_icon_ct.svg
├── gutter_icon_lt.svg
├── gutter_icon_bmt.svg
├── gutter_icon_ht.svg
└── gutter_icon_stt.svg
├── .vscodeignore
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── src
├── interface
│ ├── bookmark_data_provider.ts
│ └── bookmark_manager.ts
├── color_pick_item.ts
├── test
│ ├── suite
│ │ ├── extension.test.ts
│ │ └── index.ts
│ └── runTest.ts
├── shape_pick_item.ts
├── serializable_group.ts
├── logger
│ └── logger.ts
├── group_pick_item.ts
├── serializable_bookmark.ts
├── bookmark_pick_item.ts
├── tree_view
│ ├── active_group_tree_data_rovider.ts
│ ├── by_file_tree_data_provider.ts
│ ├── inactive_groups_tree_data_provider.ts
│ ├── bookmark_tree_item.ts
│ ├── bookmark_tree_data_provider.ts
│ └── bookmark_tree_view.ts
├── rate_limiter
│ └── rate_limiter.ts
├── bookmark.ts
├── group.ts
├── extension.ts
├── decoration_factory.ts
└── main.ts
├── .eslintrc.json
├── tsconfig.json
├── LICENSE
├── CHANGELOG.md
├── vsc-extension-quickstart.md
├── README.md
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *vsix
6 | todo.txt
7 |
--------------------------------------------------------------------------------
/resources/gutter_icons/placeholder.txt:
--------------------------------------------------------------------------------
1 | This is where the generated gutter icon files should go.
--------------------------------------------------------------------------------
/resources/editor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koalamer/vsc-labeled-bookmarks/HEAD/resources/editor.gif
--------------------------------------------------------------------------------
/resources/footer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koalamer/vsc-labeled-bookmarks/HEAD/resources/footer.gif
--------------------------------------------------------------------------------
/resources/activity_bar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koalamer/vsc-labeled-bookmarks/HEAD/resources/activity_bar.gif
--------------------------------------------------------------------------------
/resources/vsc-labeled-bookmarks-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koalamer/vsc-labeled-bookmarks/HEAD/resources/vsc-labeled-bookmarks-logo.png
--------------------------------------------------------------------------------
/resources/gutter_icon_c.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/gutter_icon_l.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/gutter_icon_bm.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | out/test/**
4 | src/**
5 | .gitignore
6 | .yarnrc
7 | vsc-extension-quickstart.md
8 | **/tsconfig.json
9 | **/.eslintrc.json
10 | **/*.map
11 | **/*.ts
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/resources/gutter_icon_unicode.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/gutter_icon_h.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/gutter_icon_st.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/interface/bookmark_data_provider.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from '../bookmark';
2 | import { Group } from '../group';
3 |
4 | export interface BookmarkDataProvider {
5 | getBookmarks(): Array;
6 | getGroups(): Array;
7 | getActiveGroup(): Group;
8 | }
--------------------------------------------------------------------------------
/resources/gutter_icon_ct.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/gutter_icon_lt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/color_pick_item.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem } from 'vscode';
2 |
3 | export class ColorPickItem implements QuickPickItem {
4 | color: string;
5 | label: string;
6 | description: string;
7 | detail: string;
8 |
9 | constructor(color: string, label: string, description: string, detail: string) {
10 | this.color = color;
11 | this.label = label;
12 | this.description = description;
13 | this.detail = detail;
14 | }
15 | }
--------------------------------------------------------------------------------
/resources/gutter_icon_bmt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off"
11 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": "warn",
13 | "@typescript-eslint/semi": "warn",
14 | "curly": "warn",
15 | "eqeqeq": "warn",
16 | "no-throw-literal": "warn",
17 | "semi": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/resources/gutter_icon_ht.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/test/suite/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | import * as vscode from 'vscode';
6 | // import * as myExtension from '../../extension';
7 |
8 | suite('Extension Test Suite', () => {
9 | vscode.window.showInformationMessage('Start all tests.');
10 |
11 | test('Sample test', () => {
12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5));
13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/resources/gutter_icon_stt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/shape_pick_item.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem } from 'vscode';
2 |
3 | export class ShapePickItem implements QuickPickItem {
4 | shape: string;
5 | iconText: string;
6 | label: string;
7 | description: string;
8 | detail: string;
9 |
10 | constructor(shape: string, iconText: string, label: string, description: string, detail: string) {
11 | this.shape = shape;
12 | this.iconText = iconText;
13 | this.label = label;
14 | this.description = description;
15 | this.detail = detail;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/interface/bookmark_manager.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from '../bookmark';
2 | import { Group } from '../group';
3 | import { TextEditor } from 'vscode';
4 |
5 | export interface BookmarManager {
6 | actionDeleteOneBookmark(bookmark: Bookmark): void;
7 | actionDeleteOneGroup(group: Group): void;
8 | deleteBookmarksOfFile(fsPath: string, group: Group | null): void;
9 | getNearestActiveBookmarkInFile(textEditor: TextEditor, group: Group | null): Bookmark | null;
10 | relabelBookmark(bookmark: Bookmark): void;
11 | renameGroup(group: Group): void;
12 | setActiveGroup(groupName: string): void;
13 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": [
7 | "es6"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true /* enable all strict type-checking options */
12 | /* Additional Checks */
13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
16 | },
17 | "exclude": [
18 | "node_modules",
19 | ".vscode-test"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | import { runTests } from 'vscode-test';
4 |
5 | async function main() {
6 | try {
7 | // The folder containing the Extension Manifest package.json
8 | // Passed to `--extensionDevelopmentPath`
9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../');
10 |
11 | // The path to test runner
12 | // Passed to --extensionTestsPath
13 | const extensionTestsPath = path.resolve(__dirname, './suite/index');
14 |
15 | // Download VS Code, unzip it and run the integration test
16 | await runTests({ extensionDevelopmentPath, extensionTestsPath });
17 | } catch (err) {
18 | console.error('Failed to run tests');
19 | process.exit(1);
20 | }
21 | }
22 |
23 | main();
24 |
--------------------------------------------------------------------------------
/src/serializable_group.ts:
--------------------------------------------------------------------------------
1 | import { Group } from "./group";
2 |
3 | export class SerializableGroup {
4 | name: string;
5 | color: string;
6 | shape: string;
7 | iconText: string;
8 |
9 | constructor(
10 | name: string,
11 | color: string,
12 | shape: string,
13 | iconText: string,
14 | ) {
15 | this.name = name;
16 | this.color = color;
17 | this.shape = shape;
18 | this.iconText = iconText;
19 | }
20 |
21 | public static fromGroup(group: Group): SerializableGroup {
22 | return new SerializableGroup(
23 | group.name,
24 | group.color,
25 | group.shape,
26 | group.iconText
27 | );
28 | }
29 | }
--------------------------------------------------------------------------------
/src/logger/logger.ts:
--------------------------------------------------------------------------------
1 | import { OutputChannel, window } from "vscode";
2 |
3 | export class Logger {
4 | private isEnabled: boolean;
5 | private name: string;
6 | private output: OutputChannel | null;
7 |
8 | constructor(name: string, isEnabled: boolean = true) {
9 | this.name = name;
10 | this.isEnabled = false;
11 | this.output = null;
12 | this.setIsEnabled(isEnabled);
13 | }
14 |
15 | setIsEnabled(isEnabled: boolean) {
16 | if (isEnabled && this.output === null) {
17 | this.output = window.createOutputChannel(this.name);
18 | }
19 | this.isEnabled = isEnabled;
20 | }
21 |
22 | public log(message: string) {
23 | if (!this.isEnabled) {
24 | return;
25 | }
26 |
27 | let date = new Date();
28 | this.output?.appendLine(date.toISOString() + " " + message);
29 | }
30 | }
--------------------------------------------------------------------------------
/src/test/suite/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as Mocha from 'mocha';
3 | import * as glob from 'glob';
4 |
5 | export function run(): Promise {
6 | // Create the mocha test
7 | const mocha = new Mocha({
8 | ui: 'tdd',
9 | color: true
10 | });
11 |
12 | const testsRoot = path.resolve(__dirname, '..');
13 |
14 | return new Promise((c, e) => {
15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
16 | if (err) {
17 | return e(err);
18 | }
19 |
20 | // Add files to the test suite
21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
22 |
23 | try {
24 | // Run the mocha test
25 | mocha.run(failures => {
26 | if (failures > 0) {
27 | e(new Error(`${failures} tests failed.`));
28 | } else {
29 | c();
30 | }
31 | });
32 | } catch (err) {
33 | console.error(err);
34 | e(err);
35 | }
36 | });
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/group_pick_item.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem } from 'vscode';
2 | import { Group } from "./group";
3 |
4 | export class GroupPickItem implements QuickPickItem {
5 | group: Group;
6 | label: string;
7 | description?: string;
8 | detail?: string;
9 | picked: boolean;
10 | alwaysShow: boolean;
11 |
12 | constructor(group: Group, label: string, description?: string, detail?: string, picked: boolean = false, alwaysShow: boolean = false) {
13 | this.group = group;
14 | this.label = label;
15 | this.description = description;
16 | this.detail = detail;
17 | this.picked = picked;
18 | this.alwaysShow = alwaysShow;
19 | }
20 |
21 | public static fromGroup(group: Group, bookmarkCount: number): GroupPickItem {
22 | let label = group.name;
23 | label = (group.isActive ? "● " : "◌ ") + label;
24 |
25 | let description = " $(bookmark) " + bookmarkCount;
26 | let detail = "";
27 | return new GroupPickItem(group, label, description, detail);
28 | }
29 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | },
20 | {
21 | "name": "Extension Tests",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "args": [
25 | "--extensionDevelopmentPath=${workspaceFolder}",
26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
27 | ],
28 | "outFiles": [
29 | "${workspaceFolder}/out/test/**/*.js"
30 | ],
31 | "preLaunchTask": "${defaultBuildTask}"
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Balázs Fodor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/serializable_bookmark.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from "./bookmark";
2 |
3 | export class SerializableBookmark {
4 | fsPath: string;
5 | lineNumber: number;
6 | characterNumber: number;
7 | label?: string;
8 | lineText: string;
9 | isLineNumberChanged: boolean;
10 | groupName: string;
11 |
12 | constructor(
13 | fsPath: string,
14 | lineNumber: number,
15 | characterNumber: number,
16 | label: string | undefined,
17 | lineText: string,
18 | groupName: string
19 | ) {
20 | this.fsPath = fsPath;
21 | this.lineNumber = lineNumber;
22 | this.characterNumber = characterNumber;
23 | this.label = label;
24 | this.lineText = lineText;
25 | this.isLineNumberChanged = false;
26 | this.groupName = groupName;
27 | }
28 |
29 | public static fromBookmark(bookmark: Bookmark): SerializableBookmark {
30 | return new SerializableBookmark(
31 | bookmark.fsPath,
32 | bookmark.lineNumber,
33 | bookmark.characterNumber,
34 | bookmark.label,
35 | bookmark.lineText,
36 | bookmark.group.name
37 | );
38 | }
39 | }
--------------------------------------------------------------------------------
/src/bookmark_pick_item.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem, workspace } from 'vscode';
2 | import { Bookmark } from "./bookmark";
3 |
4 | export class BookmarkPickItem implements QuickPickItem {
5 | bookmark: Bookmark;
6 | label: string;
7 | description?: string;
8 | detail?: string;
9 | picked: boolean;
10 | alwaysShow: boolean;
11 |
12 | constructor(
13 | bookmark: Bookmark,
14 | label: string,
15 | description?: string,
16 | detail?: string
17 | ) {
18 | this.bookmark = bookmark;
19 | this.label = label;
20 | this.description = description;
21 | this.detail = detail;
22 | this.picked = false;
23 | this.alwaysShow = false;
24 | }
25 |
26 | public static fromBookmark(bookmark: Bookmark, withGroupName: boolean): BookmarkPickItem {
27 | let label = (typeof bookmark.label !== "undefined" ? "$(tag) " + bookmark.label + "\u2003" : "")
28 | + bookmark.lineText;
29 | let description = withGroupName ? "(" + bookmark.group.name + ")" : "";
30 | let detail = "line " + (bookmark.lineNumber + 1) + " - "
31 | + workspace.asRelativePath(bookmark.fsPath);
32 | if (label === "") {
33 | description = "empty line " + description;
34 | }
35 |
36 | if (bookmark.failedJump) {
37 | label = "$(warning) " + label;
38 | detail = "$(warning) " + detail;
39 | }
40 |
41 | return new BookmarkPickItem(bookmark, label, description, detail);
42 | }
43 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "vsc-labeled-bookmarks" extension will be documented in this file.
4 |
5 | ## [1.1.11] - 2022-07-11
6 |
7 | - Fix gutter icons that disappeared after latest VSCode rollout
8 |
9 | ## [1.1.10] - 2021-09-26
10 |
11 | - Split the 'By Group' tree view into active and inactive group views
12 |
13 | ## [1.1.9] - 2021-07-11
14 |
15 | - More tree view fine tuning
16 |
17 | ## [1.1.8] - 2021-07-06
18 |
19 | - Modified the tree view reveal logic a bit
20 |
21 | ## [1.1.7] - 2021-07-03
22 |
23 | - Added group name and bookmark label modification action to the tree view
24 | - Added operation to move bookmarks from the active group into another group
25 |
26 | ## [1.1.6] - 2021-06-12
27 |
28 | - Added untrusted workspace support declaration
29 |
30 | ## [1.1.5] - 2021-06-05
31 |
32 | - Fix gutter icon remaining when labeled bookmark is untoggled
33 |
34 | ## [1.1.4] - 2021-05-30
35 |
36 | - Refactor tree view for better initialization
37 |
38 | ## [1.1.3] - 2021-05-28
39 |
40 | - Fix decoration refresh bugs even more
41 |
42 | ## [1.1.2] - 2021-05-27
43 |
44 | - Fix decoration refresh bugs
45 |
46 | ## [1.1.1] - 2021-05-22
47 |
48 | - Add delete actions to tree view
49 |
50 | ## [1.1.0] - 2012-05-21
51 |
52 | - Add tree view to activity bar
53 |
54 | ## [1.0.2] - 2012-05-06
55 |
56 | - Spellcheck readme
57 |
58 | ## [1.0.1] - 2021-05-04
59 |
60 | - Fix warning when there is no active view to navigate back to after the navigation quick pick is closed
61 |
62 | ## [1.0.0] - 2021-05-01
63 |
64 | - Initial release
65 |
--------------------------------------------------------------------------------
/src/tree_view/active_group_tree_data_rovider.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from '../bookmark';
2 | import { BookmarkTreeItem } from "./bookmark_tree_item";
3 | import { Group } from "../group";
4 | import { BookmarkDataProvider } from "../interface/bookmark_data_provider";
5 | import { BookmarkTreeDataProvider } from "./bookmark_tree_data_provider";
6 |
7 | export class ActiveGroupTreeDataProvider extends BookmarkTreeDataProvider {
8 |
9 | constructor(bookmarkDataProvider: BookmarkDataProvider) {
10 | super(bookmarkDataProvider);
11 | this.collapseGroupNodes = false;
12 | this.collapseFileNodes = false;
13 | }
14 |
15 | protected setRootElements() {
16 | let activeGroup = this.bookmarkDataProvider.getActiveGroup();
17 |
18 | this.rootElements = this.bookmarkDataProvider.getGroups()
19 | .filter(g => { return g === activeGroup; })
20 | .map(group => BookmarkTreeItem.fromGroup(group, this.collapseGroupNodes));
21 | }
22 |
23 | public getAnyTarget(): BookmarkTreeItem | null {
24 | if (this.rootElements.length > 0) {
25 | return this.rootElements[0];
26 | }
27 |
28 | return null;
29 | }
30 |
31 | public async getTargetForBookmark(bookmark: Bookmark): Promise {
32 | await this.handlePendingRefresh();
33 |
34 | for (let [parent, children] of this.childElements) {
35 | let target = children.find(child => child.getBaseBookmark() === bookmark);
36 | if (typeof target !== "undefined") {
37 | return target;
38 | }
39 | }
40 |
41 | return BookmarkTreeItem.fromNone();
42 | }
43 | }
--------------------------------------------------------------------------------
/src/tree_view/by_file_tree_data_provider.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from '../bookmark';
2 | import { BookmarkTreeItem } from "./bookmark_tree_item";
3 | import { Group } from "../group";
4 | import { BookmarkDataProvider } from "../interface/bookmark_data_provider";
5 | import { BookmarkTreeDataProvider } from "./bookmark_tree_data_provider";
6 |
7 | export class ByFileTreeDataProvider extends BookmarkTreeDataProvider {
8 |
9 | constructor(bookmarkDataProvider: BookmarkDataProvider) {
10 | super(bookmarkDataProvider);
11 | this.collapseGroupNodes = true;
12 | this.collapseFileNodes = true;
13 | }
14 |
15 | protected setRootElements() {
16 | this.rootElements = this.getFiles(this.bookmarkDataProvider.getBookmarks())
17 | .map(fsPath => BookmarkTreeItem.fromFSPath(fsPath, null, this.collapseFileNodes));
18 | }
19 |
20 | public async getTargetForGroup(group: Group): Promise {
21 | await this.handlePendingRefresh();
22 |
23 | let parent = this.rootElements.find(element => { return group === element.getBaseGroup(); });
24 | if (typeof parent === "undefined") {
25 | return null;
26 | }
27 |
28 | let children = this.childElements.get(parent);
29 | if (typeof children === "undefined") {
30 | return null;
31 | }
32 |
33 | if (children.length === 0) {
34 | return null;
35 | }
36 |
37 | return children[0];
38 | }
39 |
40 | public async getTargetForBookmark(bookmark: Bookmark): Promise {
41 | await this.handlePendingRefresh();
42 |
43 | for (let [parent, children] of this.childElements) {
44 | let target = children.find(child => child.getBaseBookmark() === bookmark);
45 | if (typeof target !== "undefined") {
46 | return target;
47 | }
48 | }
49 |
50 | return BookmarkTreeItem.fromNone();
51 | }
52 | }
--------------------------------------------------------------------------------
/src/tree_view/inactive_groups_tree_data_provider.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark } from '../bookmark';
2 | import { BookmarkTreeItem } from "./bookmark_tree_item";
3 | import { Group } from "../group";
4 | import { BookmarkDataProvider } from "../interface/bookmark_data_provider";
5 | import { BookmarkTreeDataProvider } from "./bookmark_tree_data_provider";
6 |
7 | export class InactiveGroupsTreeDataProvider extends BookmarkTreeDataProvider {
8 |
9 | constructor(bookmarkDataProvider: BookmarkDataProvider) {
10 | super(bookmarkDataProvider);
11 | this.collapseGroupNodes = true;
12 | this.collapseFileNodes = false;
13 | }
14 |
15 | protected setRootElements() {
16 | let activeGroup = this.bookmarkDataProvider.getActiveGroup();
17 |
18 | this.rootElements = this.bookmarkDataProvider.getGroups()
19 | .filter(g => { return g !== activeGroup; })
20 | .map(group => BookmarkTreeItem.fromGroup(group, this.collapseGroupNodes));
21 | }
22 |
23 | public async getTargetForGroup(group: Group): Promise {
24 | await this.handlePendingRefresh();
25 |
26 | let parent = this.rootElements.find(element => { return group === element.getBaseGroup(); });
27 | if (typeof parent === "undefined") {
28 | return null;
29 | }
30 |
31 | let children = this.childElements.get(parent);
32 | if (typeof children === "undefined") {
33 | return null;
34 | }
35 |
36 | if (children.length === 0) {
37 | return null;
38 | }
39 |
40 | return children[0];
41 | }
42 |
43 | public async getTargetForBookmark(bookmark: Bookmark): Promise {
44 | await this.handlePendingRefresh();
45 |
46 | for (let [parent, children] of this.childElements) {
47 | let target = children.find(child => child.getBaseBookmark() === bookmark);
48 | if (typeof target !== "undefined") {
49 | return target;
50 | }
51 | }
52 |
53 | return BookmarkTreeItem.fromNone();
54 | }
55 | }
--------------------------------------------------------------------------------
/src/rate_limiter/rate_limiter.ts:
--------------------------------------------------------------------------------
1 | export class RateLimiter {
2 | private limitedFunc: () => void;
3 | private pendingCallCount: number;
4 | private initialDelay: number;
5 | private isInitialDelayOver: boolean;
6 | private repeatInterval: number;
7 | private initialTimeout: NodeJS.Timeout | null;
8 | private repeatTimeout: NodeJS.Timeout | null;
9 |
10 | constructor(limitedFunc: () => void, initialDelay: number, repeatInterval: number) {
11 | this.limitedFunc = limitedFunc;
12 | this.pendingCallCount = 0;
13 | this.initialDelay = initialDelay;
14 | this.isInitialDelayOver = false;
15 | this.repeatInterval = repeatInterval;
16 | this.initialTimeout = null;
17 | this.repeatTimeout = null;
18 | }
19 |
20 | public fire(repeated: boolean = false) {
21 | if (!repeated) {
22 | this.pendingCallCount++;
23 | }
24 |
25 | if (!this.isInitialDelayOver) {
26 | if (this.initialDelay > 0) {
27 | if (this.initialTimeout !== null) {
28 | return;
29 | }
30 |
31 | this.startInitialTimeout();
32 | return;
33 | }
34 |
35 | this.isInitialDelayOver = true;
36 | }
37 |
38 | if (this.repeatTimeout !== null) {
39 | return;
40 | }
41 |
42 | this.pendingCallCount = 0;
43 | this.limitedFunc();
44 |
45 | this.repeatTimeout = setTimeout(
46 | () => {
47 | this.repeatTimeout = null;
48 | if (this.pendingCallCount === 0) {
49 | this.reset();
50 | return;
51 | }
52 | this.fire(true);
53 | },
54 | this.repeatInterval
55 | );
56 | }
57 |
58 | private startInitialTimeout() {
59 | this.initialTimeout = setTimeout(
60 | () => {
61 | this.isInitialDelayOver = true;
62 | this.fire(true);
63 | },
64 | this.initialDelay
65 | );
66 | }
67 |
68 | private reset() {
69 | this.initialTimeout = null;
70 | this.isInitialDelayOver = false;
71 | this.repeatTimeout = null;
72 |
73 | }
74 | }
--------------------------------------------------------------------------------
/vsc-extension-quickstart.md:
--------------------------------------------------------------------------------
1 | # Welcome to your VS Code Extension
2 |
3 | ## What's in the folder
4 |
5 | * This folder contains all of the files necessary for your extension.
6 | * `package.json` - this is the manifest file in which you declare your extension and command.
7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command.
9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
11 |
12 | ## Get up and running straight away
13 |
14 | * Press `F5` to open a new window with your extension loaded.
15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension.
17 | * Find output from your extension in the debug console.
18 |
19 | ## Make changes
20 |
21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
23 |
24 |
25 | ## Explore the API
26 |
27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
28 |
29 | ## Run tests
30 |
31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
32 | * Press `F5` to run the tests in a new window with your extension loaded.
33 | * See the output of the test result in the debug console.
34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder.
35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`.
36 | * You can create folders inside the `test` folder to structure your tests any way you want.
37 |
38 | ## Go further
39 |
40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace.
42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
43 |
--------------------------------------------------------------------------------
/src/tree_view/bookmark_tree_item.ts:
--------------------------------------------------------------------------------
1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri as string, workspace } from 'vscode';
2 | import { Bookmark } from '../bookmark';
3 | import { Group } from '../group';
4 |
5 | export class BookmarkTreeItem extends TreeItem {
6 | private base: Bookmark | Group | string | null = null;
7 | private parent: BookmarkTreeItem | null = null;
8 | private filterGroup: Group | null = null;
9 |
10 | static fromNone(): BookmarkTreeItem {
11 | let result = new BookmarkTreeItem(" ", TreeItemCollapsibleState.None);
12 | result.contextValue = "none";
13 | result.description = "none";
14 | result.tooltip = "none";
15 | result.base = null;
16 | return result;
17 | }
18 |
19 | static fromBookmark(bookmark: Bookmark, collapse: boolean): BookmarkTreeItem {
20 | let label = (bookmark.lineNumber + 1) + (typeof bookmark.label !== "undefined" ? ": " + bookmark.label : "");
21 | let result = new BookmarkTreeItem(label, TreeItemCollapsibleState.None);
22 | result.contextValue = "bookmark";
23 | result.description = bookmark.lineText;
24 | result.iconPath = bookmark.group.decorationSvg;
25 | result.base = bookmark;
26 | result.tooltip = workspace.asRelativePath(bookmark.fsPath) + ": " + label;
27 | result.command = {
28 | "title": "jump to bookmark",
29 | "command": "vsc-labeled-bookmarks.jumpToBookmark",
30 | "arguments": [bookmark, true]
31 | };
32 | return result;
33 | }
34 |
35 | static fromGroup(group: Group, collapse: boolean): BookmarkTreeItem {
36 | let label = group.name;
37 | let result = new BookmarkTreeItem(label);
38 | result.contextValue = "group";
39 | result.iconPath = group.decorationSvg;
40 | result.base = group;
41 | result.filterGroup = group;
42 | result.tooltip = "Group '" + group.name + "'";
43 | result.collapsibleState = collapse ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.Expanded;
44 | return result;
45 | }
46 |
47 | static fromFSPath(fsPath: string, filterGroup: Group | null, collapse: boolean): BookmarkTreeItem {
48 | let result = new BookmarkTreeItem(string.file(fsPath));
49 | result.contextValue = "file";
50 | result.iconPath = ThemeIcon.File;
51 | result.base = fsPath;
52 | result.filterGroup = filterGroup;
53 | result.tooltip = workspace.asRelativePath(fsPath);
54 | result.collapsibleState = collapse ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.Expanded;
55 | return result;
56 | }
57 |
58 | public setParent(parent: BookmarkTreeItem | null) {
59 | this.parent = parent;
60 | }
61 |
62 | public getParent(): BookmarkTreeItem | null {
63 | return this.parent;
64 | }
65 |
66 | public getBaseBookmark(): Bookmark | null {
67 | if (this.base instanceof Bookmark) {
68 | return this.base;
69 | }
70 | return null;
71 | }
72 |
73 | public getBaseGroup(): Group | null {
74 | if (this.base instanceof Group) {
75 | return this.base;
76 | }
77 | return null;
78 | }
79 |
80 | public getBaseFSPath(): string | null {
81 | if (typeof this.base === "string") {
82 | return this.base;
83 | }
84 | return null;
85 | }
86 |
87 | public getFilterGroup(): Group | null {
88 | return this.filterGroup;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/bookmark.ts:
--------------------------------------------------------------------------------
1 | import { DecorationFactory } from "./decoration_factory";
2 | import { TextEditorDecorationType, Uri } from "vscode";
3 | import { SerializableBookmark } from "./serializable_bookmark";
4 | import { Group } from "./group";
5 |
6 | export class Bookmark {
7 | public fsPath: string;
8 | public lineNumber: number;
9 | public characterNumber: number;
10 | public label?: string;
11 | public lineText: string;
12 | public failedJump: boolean;
13 | public isLineNumberChanged: boolean;
14 | public group: Group;
15 | public decorationFactory: DecorationFactory;
16 |
17 | private ownDecoration: TextEditorDecorationType | null;
18 | private bookmarkDecorationUpdatedHandler: (bookmark: Bookmark) => void;
19 | private decorationRemovedHandler: (decoration: TextEditorDecorationType) => void;
20 |
21 | constructor(
22 | fsPath: string,
23 | lineNumber: number,
24 | characterNumber: number,
25 | label: string | undefined,
26 | lineText: string,
27 | group: Group,
28 | decorationFactory: DecorationFactory
29 | ) {
30 | this.fsPath = fsPath;
31 | this.lineNumber = lineNumber;
32 | this.characterNumber = characterNumber;
33 | this.label = label;
34 | this.lineText = lineText;
35 | this.failedJump = false;
36 | this.isLineNumberChanged = false;
37 | this.group = group;
38 | this.decorationFactory = decorationFactory;
39 | this.ownDecoration = null;
40 | this.bookmarkDecorationUpdatedHandler = (bookmark: Bookmark) => { return; };
41 | this.decorationRemovedHandler = (decoration: TextEditorDecorationType) => { return; };
42 | }
43 |
44 | public static fromSerializableBookMark(
45 | serialized: SerializableBookmark,
46 | groupGetter: (groupName: string) => Group,
47 | decorationFactory: DecorationFactory
48 | ): Bookmark {
49 | return new Bookmark(
50 | serialized.fsPath,
51 | serialized.lineNumber,
52 | serialized.characterNumber,
53 | serialized.label,
54 | serialized.lineText,
55 | groupGetter(serialized.groupName),
56 | decorationFactory
57 | );
58 | }
59 |
60 | public static sortByLocation(a: Bookmark, b: Bookmark): number {
61 | return a.fsPath.localeCompare(b.fsPath)
62 | || (a.lineNumber - b.lineNumber)
63 | || (a.characterNumber - b.characterNumber);
64 | }
65 |
66 | public resetIsLineNumberChangedFlag() {
67 | this.isLineNumberChanged = false;
68 | }
69 |
70 | public setLineAndCharacterNumbers(lineNumber: number, characterNumber: number) {
71 | this.characterNumber = characterNumber;
72 |
73 | if (lineNumber === this.lineNumber) {
74 | return;
75 | }
76 |
77 | this.lineNumber = lineNumber;
78 | this.isLineNumberChanged = true;
79 | }
80 |
81 | public getDecoration(): TextEditorDecorationType | null {
82 | if (this.group.isActive && this.group.isVisible) {
83 | return this.ownDecoration || this.group.getActiveDecoration();
84 | } else {
85 | return this.group.getActiveDecoration();
86 | }
87 | }
88 |
89 | public onBookmarkDecorationUpdated(fn: (bookmark: Bookmark) => void) {
90 | this.bookmarkDecorationUpdatedHandler = fn;
91 | }
92 |
93 | public onDecorationRemoved(fn: (decoration: TextEditorDecorationType) => void) {
94 | this.decorationRemovedHandler = fn;
95 | }
96 |
97 | public async initDecoration() {
98 | if (typeof this.label === "undefined") {
99 | return;
100 | }
101 |
102 | let previousDecoration = this.ownDecoration;
103 | let tempSvg: Uri;
104 |
105 | [this.ownDecoration, tempSvg] = await this.decorationFactory.create(
106 | this.group.shape,
107 | this.group.color,
108 | this.group.iconText,
109 | this.label
110 | );
111 |
112 | if (previousDecoration !== null) {
113 | this.decorationRemovedHandler(previousDecoration);
114 | }
115 |
116 | this.bookmarkDecorationUpdatedHandler(this);
117 | }
118 |
119 | public switchDecoration() {
120 | if (this.ownDecoration !== null) {
121 | this.decorationRemovedHandler(this.ownDecoration);
122 | }
123 |
124 | this.bookmarkDecorationUpdatedHandler(this);
125 | }
126 | }
--------------------------------------------------------------------------------
/src/tree_view/bookmark_tree_data_provider.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter, TreeDataProvider, TreeItem } from "vscode";
2 | import { Bookmark } from '../bookmark';
3 | import { BookmarkTreeItem } from "./bookmark_tree_item";
4 | import { BookmarkDataProvider } from "../interface/bookmark_data_provider";
5 |
6 | export class BookmarkTreeDataProvider implements TreeDataProvider {
7 | protected bookmarkDataProvider: BookmarkDataProvider;
8 |
9 | protected rootElements: Array = [];
10 | protected childElements: Map>;
11 |
12 | protected changeEmitter = new EventEmitter();
13 | readonly onDidChangeTreeData = this.changeEmitter.event;
14 |
15 | // workaround for tree views not updating when hidden
16 | protected isRefreshPending = false;
17 | protected readonly refreshGracePeriod = 100;
18 |
19 | protected collapseGroupNodes = false;
20 | protected collapseFileNodes = false;
21 |
22 | constructor(bookmarkDataProvider: BookmarkDataProvider) {
23 | this.bookmarkDataProvider = bookmarkDataProvider;
24 | this.childElements = new Map();
25 | }
26 |
27 | public getTreeItem(element: BookmarkTreeItem): TreeItem {
28 | return element;
29 | }
30 |
31 | public getChildren(element?: BookmarkTreeItem | undefined): Thenable {
32 | if (!element) {
33 | this.isRefreshPending = false;
34 | this.setRootElements();
35 | return Promise.resolve(this.rootElements);
36 | }
37 |
38 | let filterGroup = element.getFilterGroup();
39 |
40 | let baseFSPath = element.getBaseFSPath();
41 | if (baseFSPath !== null) {
42 | let bookmarks = this.bookmarkDataProvider.getBookmarks().filter(bookmark => { return (bookmark.fsPath === baseFSPath); });
43 | if (filterGroup !== null) {
44 | bookmarks = bookmarks.filter(bookmark => { return bookmark.group === filterGroup; });
45 | }
46 |
47 | let children: Array;
48 |
49 | if (bookmarks.length === 0) {
50 | children = [BookmarkTreeItem.fromNone()];
51 | } else {
52 | children = bookmarks.map(bookmark => BookmarkTreeItem.fromBookmark(bookmark, this.collapseFileNodes));
53 | }
54 |
55 | children.forEach(child => child.setParent(element));
56 | this.childElements.set(element, children);
57 | return Promise.resolve(children);
58 | }
59 |
60 | let baseGroup = element.getBaseGroup();
61 | if (baseGroup !== null) {
62 | let files = this.getFiles(this.bookmarkDataProvider.getBookmarks().filter(bookmark => { return bookmark.group === filterGroup; }));
63 |
64 | let children: Array;
65 |
66 | if (files.length === 0) {
67 | children = [BookmarkTreeItem.fromNone()];
68 | } else {
69 | children = files.map(fsPath => BookmarkTreeItem.fromFSPath(fsPath, filterGroup, this.collapseFileNodes));
70 | }
71 |
72 | children.forEach(child => child.setParent(element));
73 | this.childElements.set(element, children);
74 | return Promise.resolve(children);
75 | }
76 |
77 | return Promise.resolve([]);
78 | }
79 |
80 | protected setRootElements() {
81 | this.rootElements = [];
82 | }
83 |
84 | public refresh() {
85 | this.isRefreshPending = true;
86 | this.changeEmitter.fire();
87 | }
88 |
89 | public async init() {
90 | let nodesToProcess = new Array();
91 | nodesToProcess.push(undefined);
92 |
93 | while (nodesToProcess.length > 0) {
94 | let node = nodesToProcess.pop();
95 | let moreNodes = await this.getChildren(node);
96 | moreNodes.forEach(newNode => {
97 | if (typeof newNode !== "undefined") {
98 | nodesToProcess.push(newNode);
99 | }
100 | });
101 | }
102 | };
103 |
104 | public getParent(element: BookmarkTreeItem): BookmarkTreeItem | null | undefined {
105 | return element.getParent();
106 | }
107 |
108 | protected getFiles(bookmarks: Array): Array {
109 | let files = new Array();
110 | for (let i = 0; i < bookmarks.length; i++) {
111 |
112 | if (i === 0 || bookmarks[i].fsPath !== bookmarks[i - 1].fsPath) {
113 | files.push(bookmarks[i].fsPath);
114 | }
115 | }
116 | return files;
117 | }
118 |
119 | protected async handlePendingRefresh() {
120 | if (this.isRefreshPending) {
121 | await this.init();
122 | await new Promise(resolve => setTimeout(resolve, this.refreshGracePeriod));
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/src/group.ts:
--------------------------------------------------------------------------------
1 | import { TextEditorDecorationType, Uri } from 'vscode';
2 | import { DecorationFactory } from "./decoration_factory";
3 | import { SerializableGroup } from "./serializable_group";
4 |
5 | export class Group {
6 | static readonly inactiveTransparency: string = "33";
7 |
8 | public name: string;
9 | public color: string;
10 | public shape: string;
11 | public iconText: string;
12 | public isActive: boolean;
13 | public isVisible: boolean;
14 | public decoration: TextEditorDecorationType;
15 | public decorationSvg: Uri;
16 |
17 | private decorationFactory: DecorationFactory;
18 | private inactiveColor: string;
19 | private isInitialized: boolean;
20 | private inactiveDecoration: TextEditorDecorationType;
21 | private inactiveDecorationSvg: Uri;
22 | private groupDecorationUpdatedHandler: (group: Group) => void;
23 | private groupDecorationSwitchedHandler: (group: Group) => void;
24 | private decorationRemovedHandler: (decoration: TextEditorDecorationType) => void;
25 |
26 | constructor(
27 | name: string,
28 | color: string,
29 | shape: string,
30 | iconText: string,
31 | decorationFactory: DecorationFactory
32 | ) {
33 | this.decorationFactory = decorationFactory;
34 | this.name = name;
35 | this.color = this.decorationFactory.normalizeColorFormat(color);
36 | this.shape = shape;
37 | this.iconText = iconText;
38 | this.inactiveColor = this.color.substring(0, 6) + Group.inactiveTransparency;
39 | this.isActive = false;
40 | this.isVisible = false;
41 | this.isInitialized = false;
42 | this.decoration = this.decorationFactory.placeholderDecoration;
43 | this.decorationSvg = this.decorationFactory.placeholderDecorationUri;
44 | this.inactiveDecoration = this.decorationFactory.placeholderDecoration;
45 | this.inactiveDecorationSvg = this.decorationFactory.placeholderDecorationUri;
46 | this.groupDecorationUpdatedHandler = (group: Group) => { return; };
47 | this.groupDecorationSwitchedHandler = (group: Group) => { return; };
48 | this.decorationRemovedHandler = (decoration: TextEditorDecorationType) => { return; };
49 | }
50 |
51 | public static fromSerializableGroup(sg: SerializableGroup, decorationFactory: DecorationFactory): Group {
52 | return new Group(sg.name, sg.color, sg.shape, sg.iconText, decorationFactory);
53 | }
54 |
55 | public static sortByName(a: Group, b: Group): number {
56 | return a.name.localeCompare(b.name);
57 | }
58 |
59 | public onGroupDecorationUpdated(fn: (group: Group) => void) {
60 | this.groupDecorationUpdatedHandler = fn;
61 | }
62 |
63 | public onGroupDecorationSwitched(fn: (group: Group) => void) {
64 | this.groupDecorationSwitchedHandler = fn;
65 | }
66 |
67 | public onDecorationRemoved(fn: (decoration: TextEditorDecorationType) => void) {
68 | this.decorationRemovedHandler = fn;
69 | }
70 |
71 | public async initDecorations() {
72 | [this.decoration, this.decorationSvg] = await this.decorationFactory.create(
73 | this.shape,
74 | this.color,
75 | this.iconText
76 | );
77 | [this.inactiveDecoration, this.inactiveDecorationSvg] = await this.decorationFactory.create(
78 | this.shape,
79 | this.inactiveColor,
80 | this.iconText
81 | );
82 | this.isInitialized = true;
83 | this.groupDecorationUpdatedHandler(this);
84 | }
85 |
86 | public getColor(): string {
87 | return this.color;
88 | }
89 |
90 | public getActiveDecoration(): TextEditorDecorationType | null {
91 | if (!this.isVisible || !this.isInitialized) {
92 | return null;
93 | }
94 |
95 | if (this.isActive) {
96 | return this.decoration;
97 | }
98 |
99 | return this.inactiveDecoration;
100 | }
101 |
102 | public setIsActive(isActive: boolean) {
103 | if (this.isActive === isActive) {
104 | return;
105 | }
106 |
107 | let activeDecoration = this.getActiveDecoration();
108 | if (activeDecoration !== null) {
109 | this.decorationRemovedHandler(activeDecoration);
110 | }
111 |
112 | this.isActive = isActive;
113 | this.groupDecorationSwitchedHandler(this);
114 | }
115 |
116 | public setIsVisible(isVisible: boolean) {
117 | if (this.isVisible === isVisible) {
118 | return;
119 | }
120 |
121 | let activeDecoration = this.getActiveDecoration();
122 | if (activeDecoration !== null) {
123 | this.decorationRemovedHandler(activeDecoration);
124 | }
125 |
126 | this.isVisible = isVisible;
127 | this.groupDecorationSwitchedHandler(this);
128 | }
129 |
130 | public setShapeAndIconText(shape: string, iconText: string) {
131 | if (this.shape === shape && this.iconText === iconText) {
132 | return;
133 | }
134 |
135 | this.removeDecorations();
136 |
137 | this.shape = shape;
138 | this.iconText = iconText;
139 |
140 | this.initDecorations();
141 | }
142 |
143 | public setColor(color: string) {
144 | if (this.color === color) {
145 | return;
146 | }
147 |
148 | this.removeDecorations();
149 |
150 | this.color = this.decorationFactory.normalizeColorFormat(color);
151 | this.inactiveColor = this.color.substring(0, 6) + Group.inactiveTransparency;
152 |
153 | this.initDecorations();
154 | }
155 |
156 | public redoDecorations() {
157 | this.removeDecorations();
158 | this.initDecorations();
159 | }
160 |
161 | public removeDecorations() {
162 | this.isInitialized = false;
163 |
164 | this.decorationRemovedHandler(this.decoration);
165 | this.decorationRemovedHandler(this.inactiveDecoration);
166 | }
167 | }
--------------------------------------------------------------------------------
/src/tree_view/bookmark_tree_view.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { TreeView } from 'vscode';
3 | import { Main } from '../main';
4 | import { BookmarkTreeItem } from './bookmark_tree_item';
5 | import { RateLimiter } from '../rate_limiter/rate_limiter';
6 | import { ActiveGroupTreeDataProvider } from './active_group_tree_data_rovider';
7 | import { InactiveGroupsTreeDataProvider } from './inactive_groups_tree_data_provider';
8 | import { ByFileTreeDataProvider } from './by_file_tree_data_provider';
9 |
10 | export class BookmarkTreeView {
11 | private main: Main | null = null;
12 |
13 | private treeViewByActiveGroup: TreeView | null = null;
14 | private treeViewByInactiveGroups: TreeView | null = null;
15 | private treeViewByFile: TreeView | null = null;
16 |
17 | private treeDataProviderByActiveGroup: ActiveGroupTreeDataProvider | null = null;
18 | private treeDataProviderByInactiveGroups: InactiveGroupsTreeDataProvider | null = null;
19 | private treeDataProviderByFile: ByFileTreeDataProvider | null = null;
20 |
21 | private proxyRefreshCallback = () => { };
22 | private refreshLimiter: RateLimiter = new RateLimiter(() => { }, 0, 1000);
23 |
24 | private isInitDone: boolean = false;
25 |
26 | public async init(main: Main) {
27 | this.main = main;
28 |
29 | this.treeDataProviderByActiveGroup = new ActiveGroupTreeDataProvider(this.main);
30 | this.treeDataProviderByInactiveGroups = new InactiveGroupsTreeDataProvider(this.main);
31 | this.treeDataProviderByFile = new ByFileTreeDataProvider(this.main);
32 |
33 | this.treeViewByActiveGroup = vscode.window.createTreeView('bookmarksByActiveGroup', {
34 | treeDataProvider: this.treeDataProviderByActiveGroup
35 | });
36 |
37 | this.treeViewByInactiveGroups = vscode.window.createTreeView('bookmarksByInactiveGroups', {
38 | treeDataProvider: this.treeDataProviderByInactiveGroups
39 | });
40 |
41 | this.treeViewByFile = vscode.window.createTreeView('bookmarksByFile', {
42 | treeDataProvider: this.treeDataProviderByFile
43 | });
44 |
45 | await this.treeDataProviderByActiveGroup.init();
46 | await this.treeDataProviderByInactiveGroups.init();
47 | await this.treeDataProviderByFile.init();
48 |
49 | this.refreshLimiter = new RateLimiter(
50 | this.actualRefresh.bind(this),
51 | 50,
52 | 800
53 | );
54 | this.proxyRefreshCallback = this.refreshLimiter.fire.bind(this.refreshLimiter);
55 |
56 | this.isInitDone = true;
57 |
58 | this.refreshCallback();
59 | }
60 |
61 | public refreshCallback() {
62 | this.proxyRefreshCallback();
63 | }
64 |
65 | public deleteItem(treeItem: BookmarkTreeItem) {
66 | if (!this.isInitDone) {
67 | return;
68 | }
69 |
70 | let bookmark = treeItem.getBaseBookmark();
71 | if (bookmark !== null) {
72 | this.main?.actionDeleteOneBookmark(bookmark);
73 | return;
74 | }
75 |
76 | let group = treeItem.getBaseGroup();
77 | if (group !== null) {
78 | this.main?.actionDeleteOneGroup(group);
79 | return;
80 | }
81 |
82 | let fsPath = treeItem.getBaseFSPath();
83 | if (fsPath !== null) {
84 | this.main?.deleteBookmarksOfFile(fsPath, treeItem.getFilterGroup());
85 | }
86 | }
87 |
88 |
89 | public activateItem(treeItem: BookmarkTreeItem) {
90 | if (!this.isInitDone) {
91 | return;
92 | }
93 |
94 | let group = treeItem.getBaseGroup();
95 | if (group === null) {
96 | return;
97 | }
98 |
99 | this.main?.setActiveGroup(group.name);
100 | }
101 |
102 | public editItem(treeItem: BookmarkTreeItem) {
103 | if (!this.isInitDone) {
104 | return;
105 | }
106 |
107 | let bookmark = treeItem.getBaseBookmark();
108 | if (bookmark !== null) {
109 | this.main?.relabelBookmark(bookmark);
110 | return;
111 | }
112 |
113 | let group = treeItem.getBaseGroup();
114 | if (group !== null) {
115 | this.main?.renameGroup(group);
116 | return;
117 | }
118 | }
119 |
120 | public async show() {
121 | try {
122 | if (!this.isInitDone
123 | || this.main === null
124 | || this.treeDataProviderByActiveGroup === null
125 | || this.treeViewByActiveGroup === null) {
126 | return;
127 | }
128 |
129 | if (!this.treeViewByActiveGroup.visible) {
130 | let anytarget = this.treeDataProviderByActiveGroup.getAnyTarget();
131 | if (anytarget !== null) {
132 | this.treeViewByActiveGroup.reveal(anytarget);
133 | }
134 | }
135 |
136 | let textEditor = vscode.window.activeTextEditor;
137 |
138 | if (typeof textEditor === "undefined" && vscode.window.visibleTextEditors.length > 0) {
139 | textEditor = vscode.window.visibleTextEditors[0];
140 | }
141 |
142 | if (typeof textEditor === "undefined") {
143 | return;
144 | }
145 |
146 | let nearestBookmarkInFile = this.main.getNearestActiveBookmarkInFile(textEditor, this.main.getActiveGroup());
147 |
148 | if (nearestBookmarkInFile === null) {
149 | return;
150 | }
151 |
152 | let targetBookmark = await this.treeDataProviderByActiveGroup.getTargetForBookmark(nearestBookmarkInFile);
153 | if (targetBookmark !== null) {
154 | this.treeViewByActiveGroup.reveal(targetBookmark);
155 | }
156 | } catch (e) {
157 | console.log(e);
158 | vscode.window.showErrorMessage("Bookmark tree view init error " + e);
159 | }
160 | }
161 |
162 | private actualRefresh() {
163 | this.treeDataProviderByActiveGroup?.refresh();
164 | this.treeDataProviderByInactiveGroups?.refresh();
165 | this.treeDataProviderByFile?.refresh();
166 | }
167 | }
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { ExtensionContext } from 'vscode';
3 | import { Bookmark } from './bookmark';
4 | import { Main } from './main';
5 | import { BookmarkTreeView } from './tree_view/bookmark_tree_view';
6 | import { BookmarkTreeItem } from './tree_view/bookmark_tree_item';
7 |
8 | let main: Main;
9 | let treeView: BookmarkTreeView;
10 |
11 | export function activate(context: ExtensionContext) {
12 | treeView = new BookmarkTreeView();
13 | main = new Main(context, treeView.refreshCallback.bind(treeView));
14 |
15 | let disposable: vscode.Disposable;
16 |
17 | disposable = vscode.commands.registerTextEditorCommand(
18 | 'vsc-labeled-bookmarks.toggleBookmark',
19 | (textEditor) => main.editorActionToggleBookmark(textEditor)
20 | );
21 | context.subscriptions.push(disposable);
22 |
23 | disposable = vscode.commands.registerTextEditorCommand(
24 | 'vsc-labeled-bookmarks.toggleLabeledBookmark',
25 | (textEditor) => main.editorActionToggleLabeledBookmark(textEditor)
26 | );
27 | context.subscriptions.push(disposable);
28 |
29 | disposable = vscode.commands.registerTextEditorCommand(
30 | 'vsc-labeled-bookmarks.navigateToNextBookmark',
31 | (textEditor) => main.editorActionnavigateToNextBookmark(textEditor));
32 | context.subscriptions.push(disposable);
33 |
34 | disposable = vscode.commands.registerTextEditorCommand(
35 | 'vsc-labeled-bookmarks.navigateToPreviousBookmark',
36 | (textEditor) => main.editorActionNavigateToPreviousBookmark(textEditor));
37 | context.subscriptions.push(disposable);
38 |
39 | disposable = vscode.commands.registerCommand(
40 | 'vsc-labeled-bookmarks.navigateToBookmark',
41 | () => main.actionNavigateToBookmark());
42 | context.subscriptions.push(disposable);
43 |
44 | disposable = vscode.commands.registerCommand(
45 | 'vsc-labeled-bookmarks.navigateToBookmarkOfAnyGroup',
46 | () => main.actionNavigateToBookmarkOfAnyGroup());
47 | context.subscriptions.push(disposable);
48 |
49 | disposable = vscode.commands.registerCommand(
50 | 'vsc-labeled-bookmarks.selectGroup',
51 | () => main.actionSelectGroup());
52 | context.subscriptions.push(disposable);
53 |
54 | disposable = vscode.commands.registerCommand(
55 | 'vsc-labeled-bookmarks.addGroup',
56 | () => main.actionAddGroup());
57 | context.subscriptions.push(disposable);
58 |
59 | disposable = vscode.commands.registerCommand(
60 | 'vsc-labeled-bookmarks.deleteGroup',
61 | () => main.actionDeleteGroup());
62 | context.subscriptions.push(disposable);
63 |
64 | disposable = vscode.commands.registerCommand(
65 | 'vsc-labeled-bookmarks.setGroupIconShape',
66 | () => main.actionSetGroupIconShape());
67 | context.subscriptions.push(disposable);
68 |
69 | disposable = vscode.commands.registerCommand(
70 | 'vsc-labeled-bookmarks.setGroupIconColor',
71 | () => main.actionSetGroupIconColor());
72 | context.subscriptions.push(disposable);
73 |
74 | disposable = vscode.commands.registerCommand(
75 | 'vsc-labeled-bookmarks.deleteBookmark',
76 | () => main.actionDeleteBookmark());
77 | context.subscriptions.push(disposable);
78 |
79 | disposable = vscode.commands.registerCommand(
80 | 'vsc-labeled-bookmarks.toggleHideAll',
81 | () => main.actionToggleHideAll());
82 | context.subscriptions.push(disposable);
83 |
84 | disposable = vscode.commands.registerCommand(
85 | 'vsc-labeled-bookmarks.toggleHideInactiveGroups',
86 | () => main.actionToggleHideInactiveGroups());
87 | context.subscriptions.push(disposable);
88 |
89 | disposable = vscode.commands.registerCommand(
90 | 'vsc-labeled-bookmarks.clearFailedJumpFlags',
91 | () => main.actionClearFailedJumpFlags());
92 | context.subscriptions.push(disposable);
93 |
94 | disposable = vscode.commands.registerCommand(
95 | 'vsc-labeled-bookmarks.moveBookmarksFromActiveGroup',
96 | () => main.actionMoveBookmarksFromActiveGroup());
97 | context.subscriptions.push(disposable);
98 |
99 | disposable = vscode.commands.registerTextEditorCommand(
100 | 'vsc-labeled-bookmarks.expandSelectionToNextBookmark',
101 | (textEditor) => main.actionExpandSelectionToNextBookmark(textEditor));
102 | context.subscriptions.push(disposable);
103 |
104 | disposable = vscode.commands.registerTextEditorCommand(
105 | 'vsc-labeled-bookmarks.expandSelectionToPreviousBookmark',
106 | (textEditor) => main.actionExpandSelectionToPreviousBookmark(textEditor));
107 | context.subscriptions.push(disposable);
108 |
109 | disposable = vscode.commands.registerCommand(
110 | 'vsc-labeled-bookmarks.jumpToBookmark',
111 | (bookmark: Bookmark, preview: boolean) => main.jumpToBookmark(bookmark, preview));
112 | context.subscriptions.push(disposable);
113 |
114 | vscode.window.onDidChangeActiveTextEditor(textEditor => {
115 | main.updateEditorDecorations(textEditor);
116 | });
117 |
118 | vscode.workspace.onDidChangeTextDocument(textDocumentChangeEvent => {
119 | main.onEditorDocumentChanged(textDocumentChangeEvent);
120 | });
121 |
122 | vscode.workspace.onDidRenameFiles(fileRenameEvent => {
123 | main.onFilesRenamed(fileRenameEvent);
124 | });
125 |
126 | vscode.workspace.onDidDeleteFiles(fileDeleteEvent => {
127 | main.onFilesDeleted(fileDeleteEvent);
128 | });
129 |
130 | vscode.workspace.onDidChangeConfiguration(() => {
131 | main.readSettings();
132 | });
133 |
134 | disposable = vscode.commands.registerCommand(
135 | 'vsc-labeled-bookmarks.refreshTreeView',
136 | () => treeView.refreshCallback());
137 | context.subscriptions.push(disposable);
138 |
139 | disposable = vscode.commands.registerCommand(
140 | 'vsc-labeled-bookmarks.activateTreeItem',
141 | (item: BookmarkTreeItem) => treeView.activateItem(item));
142 | context.subscriptions.push(disposable);
143 |
144 | disposable = vscode.commands.registerCommand(
145 | 'vsc-labeled-bookmarks.editTreeItem',
146 | (item: BookmarkTreeItem) => treeView.editItem(item));
147 | context.subscriptions.push(disposable);
148 |
149 | disposable = vscode.commands.registerCommand(
150 | 'vsc-labeled-bookmarks.deleteTreeItem',
151 | (item: BookmarkTreeItem) => treeView.deleteItem(item));
152 | context.subscriptions.push(disposable);
153 |
154 | disposable = vscode.commands.registerCommand(
155 | 'vsc-labeled-bookmarks.showTreeView',
156 | () => treeView.show());
157 | context.subscriptions.push(disposable);
158 |
159 | treeView.init(main);
160 | }
161 |
162 | export function deactivate() {
163 | main.saveState();
164 | }
165 |
--------------------------------------------------------------------------------
/src/decoration_factory.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as vscode from 'vscode';
3 | import { DecorationRangeBehavior, OverviewRulerLane, Uri } from "vscode";
4 | import { TextEditorDecorationType } from "vscode";
5 |
6 | const svgBookmark = ``;
9 |
10 | const svgBookmarkWithText = ``;
18 |
19 | const svgCircle = ``;
22 |
23 | const svgCircleWithText = ``;
31 |
32 | const svgHeart = ``;
36 |
37 | const svgHeartWithText = ``;
46 |
47 | const svgLabel = ``;
51 |
52 | const svgLabelWithText = ``;
60 |
61 | const svgStar = ``;
65 |
66 | const svgStarWithText = ``;
75 |
76 | const svgUnicodeChar = ``;
80 |
81 | export class DecorationFactory {
82 | private readonly singleCharacterLabelPatern = /^[a-zA-Z0-9!?+-=\/\$%#]$/;
83 |
84 | public readonly placeholderDecorationUri = Uri.file(
85 | path.join(__dirname, "..", "resources", "gutter_icon_bm.svg")
86 | );
87 |
88 | public readonly placeholderDecoration = vscode.window.createTextEditorDecorationType(
89 | {
90 | gutterIconPath: this.placeholderDecorationUri.fsPath,
91 | gutterIconSize: 'contain',
92 | }
93 | );
94 |
95 | public svgDir: Uri;
96 | public overviewRulerLane: OverviewRulerLane | undefined;
97 | public lineEndLabelType: string;
98 |
99 | constructor(svgDir: Uri, overviewRulerLane: OverviewRulerLane | undefined, lineEndLabelType: string) {
100 | this.svgDir = svgDir;
101 | this.overviewRulerLane = overviewRulerLane;
102 | this.lineEndLabelType = lineEndLabelType;
103 | }
104 |
105 | async create(shape: string, color: string, iconText: string, lineLabel?: string): Promise<[TextEditorDecorationType, Uri]> {
106 | iconText = iconText.normalize();
107 |
108 | if (shape !== "unicode") {
109 | if (!this.singleCharacterLabelPatern.test(iconText)) {
110 | iconText = "";
111 | } else {
112 | iconText = iconText.substring(0, 1).toUpperCase();
113 | }
114 | }
115 |
116 | let fileNamePostfix = '';
117 | let svg: string;
118 |
119 | if (iconText === "") {
120 | switch (shape) {
121 | case "circle": svg = svgCircle; break;
122 | case "heart": svg = svgHeart; break;
123 | case "label": svg = svgLabel; break;
124 | case "star": svg = svgStar; break;
125 | default:
126 | svg = svgBookmark;
127 | shape = "bookmark";
128 | }
129 | } else {
130 | switch (shape) {
131 | case "circle": svg = svgCircleWithText; break;
132 | case "heart": svg = svgHeartWithText; break;
133 | case "label": svg = svgLabelWithText; break;
134 | case "star": svg = svgStarWithText; break;
135 | case "unicode": svg = svgUnicodeChar; break;
136 | default:
137 | svg = svgBookmarkWithText;
138 | shape = "bookmark";
139 | }
140 | let codePoint = (iconText.codePointAt(0) ?? 0).toString(10);
141 | svg = svg.replace(">Q<", ">" + codePoint + ";<");
142 | fileNamePostfix = codePoint;
143 | }
144 |
145 | color = this.normalizeColorFormat(color);
146 | svg = svg.replace("888888ff", color);
147 |
148 | let fileName = shape + "_" + color + "_" + fileNamePostfix + ".svg";
149 | let bytes = Uint8Array.from(svg.split("").map(c => { return c.charCodeAt(0); }));
150 | let svgUri = Uri.joinPath(this.svgDir, fileName);
151 |
152 | try {
153 | let stat = await vscode.workspace.fs.stat(svgUri);
154 | if (stat.size < 1) {
155 | await vscode.workspace.fs.writeFile(svgUri, bytes);
156 | }
157 | } catch (e) {
158 | await vscode.workspace.fs.writeFile(svgUri, bytes);
159 | }
160 |
161 | let decorationOptions = {
162 | gutterIconPath: svgUri,
163 | gutterIconSize: 'contain',
164 | overviewRulerColor: (typeof this.overviewRulerLane !== "undefined")
165 | ? '#' + color
166 | : undefined,
167 | overviewRulerLane: this.overviewRulerLane,
168 | rangeBehavior: DecorationRangeBehavior.ClosedClosed,
169 | after: {},
170 | isWholeLine: true,
171 | };
172 |
173 | if (typeof lineLabel !== "undefined") {
174 | switch (this.lineEndLabelType) {
175 | case "bordered":
176 | decorationOptions.after = {
177 | border: "1px solid #" + color,
178 | color: "#" + color,
179 | contentText: "\u2002" + lineLabel + "\u2002",
180 | margin: "0px 0px 0px 10px",
181 | };
182 | break;
183 | case "inverse":
184 | decorationOptions.after = {
185 | backgroundColor: "#" + color,
186 | color: new vscode.ThemeColor("editor.background"),
187 | contentText: "\u2002" + lineLabel + "\u2002",
188 | margin: "0px 0px 0px 10px",
189 | };
190 | break;
191 | }
192 | }
193 |
194 | let result = vscode.window.createTextEditorDecorationType(decorationOptions);
195 |
196 | return [result, svgUri];
197 | }
198 |
199 | public normalizeColorFormat(color: string): string {
200 | if (color.match(/^#?[0-9a-f]+$/i) === null) {
201 | return "888888ff";
202 | }
203 |
204 | if (color.charAt(0) === "#") {
205 | color = color.substr(1, 8);
206 | } else {
207 | color = color.substr(0, 8);
208 | }
209 |
210 | color = color.toLowerCase();
211 |
212 | if (color.length < 8) {
213 | color = color.padEnd(8, "f");
214 | }
215 |
216 | return color;
217 | }
218 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vsc-labeled-bookmarks README
2 |
3 | Bookmarks with customizable icons, colors and other visuals, organized into groups between which you can switch, keyboard shortcut for most functions.
4 |
5 | 
6 |
7 | **Important note on using breakpoints:** Line decorations (in general) seem to interfere with placing breakpoints for debugging. To work around this, you can toggle the decorations on and off using `ctrl+alt+b h`. All operations still work the same when the decorations are hidden.
8 |
9 | **Important note on confidentiality:** If you bookmark a line containing sensitive information, remember that the line's text will be saved as part of the extension state persisted to disk according to the persistence settings.
10 |
11 | ## Features
12 |
13 | ### Bookmarks
14 |
15 | * You can set an unnamed bookmark on a line using `ctrl+alt+m`. If there already is a bookmark on the line, it is removed. If you have a multi line selection, the first line will be bookmarked. If you have multiple selections or multiple cursors, each receives the action.
16 | * Labeled bookmarks can be set using `ctrl+alt+l`. A prompt appears where you can type the label for the bookmark. If you have some text selected, that will be the default value of the input box. **You can have multiple bookmarks with the same label, but if you use a single character as the label, that label will be kept unique (VIM style relocating the bookmark instead of adding another one.)** This action does not work on multiple cursors, only on (the first) one.
17 | * The labeled bookmark creation is multi functional: it expects the input in the format of "bookmark label" or "bookmark label@@group name" or just "@@group name. When a group name is specified, the group is created if it does not yet exist and it is set to be the active group. Then, if the label is specified, a new bookmark is created using it.
18 | * Delete bookmarks using `ctrl+alt+b d`, or by using the above toggle commands on a line that already has a bookmark (of the active group).
19 |
20 | ### Navigation
21 |
22 | * Go to the next bookmark: `ctrl+alt+k`
23 | * Go to the previous bookmark: `ctrl+alt+j`
24 | * Navigate to a bookmark by selecting it from a list: `ctrl+alt+n` A quick pick list appears where the bookmarks are filtered as you type. All bookmarks are displayed in the list, including the unnamed ones.
25 | * Navigate to a bookmark of any group (same as `ctrl+alt+n` but not limited to the active group): `ctrl+alt+b n`
26 |
27 | ### Expand Selection
28 |
29 | * You can expand the current selection to the next bookmark by using `shift+alt+k`
30 | * To expand the selection to the previous bookmark, use `shift+alt+j`
31 |
32 | Both operations work in a double tap fashion: the selection is expanded first up to but not including the bookmarked line, and on a second use it is expanded to include that line as well.
33 |
34 | ### Groups
35 |
36 | Most operations work on the currently active group's bookmarks. Each group has its own icon shape/color, but the inactive group icons appear semi transparent (or are hidden when `ctrl+alt+b i` is toggled).
37 |
38 | Groups were implemented to be able to separate one set of bookmarks (for one topic/bug/branch etc.) from others. You can work with the set that is currently relevant, without other bookmarks littering the forward/backward navigation and without having to delete them to avoid this.
39 |
40 | * You can create a group and switch to it implicitly by using the "@@" when creating a labeled bookmark with `ctrl+alt+l`. See the relevant section above for details.
41 | * Alternatively, you can create a group using `ctrl+alt+b alt+g`
42 | * Delete one or multiple groups using `ctrl+alt+b shift+g`
43 | * Select the active group from a list of the available groups: `ctrl+alt+b g`
44 | * Move bookmarks from the active group to another one using `ctrl+alt+b m`
45 |
46 | ### Display Options
47 |
48 | * Hide / unhide bookmark icons (might be necessary to set a breakpoint on a line that also has a bookmark): `ctrl+alt+b h`
49 | * Hide / unhide the icons of inactive groups: `ctrl+alt+b i`
50 | * Setting to control the appearance of overview ruler (scrollbar) decorations
51 | * Setting to control how the label text for labeled bookmarks should appear at the line's end
52 |
53 | ### Customizing Group Icons
54 |
55 | Group icons come in two variants: vector icons (fixed set) and unicode character icons (customizable set).
56 |
57 | * Vector icons provide a fixed set of shapes to chose from, and they should appear the same across all devices. When a new group is created it uses the shape specified as the default shape in the configuration options. If your group has a single character name, and it matches `[a-zA-Z0-9!?+-=\/\$%#]`, then the uppercased character is displayed on the icon.
58 | * Unicode character icons can be customized using the `labeledBookmarks.unicodeMarkers` configuration option. You can define which unicode characters/symbol/emojis you would like to use as the group icon. These can be applied using the shape selection command `ctrl+alt+b s`. If none is defined, a default set is used. (Emojis have their own color and so the color setting remains ineffective on those, but it works as expected on the rest of the unicode alphabets and symbols.)
59 |
60 | The other display option for group icons is the color.
61 |
62 | * The icon color can be chosen selected from a list using `ctrl+alt+b c`. You can define the elements of this list with the configuration option `labeledBookmarks.colors`. If it is not defined a default color set is used.
63 |
64 | ## Extension Settings
65 |
66 | * `labeledBookmarks.unicodeMarkers`: list of unicode characters to be made available in the shape selection list. It should be in the form of: `[["look", "👀"], ["bug","🐞"]]`
67 | * `labeledBookmarks.colors`: list of colors to be made available when creating new bookmark groups or when setting the color of an existing one. It should be in the form of: `[["red", "ff0000"], ["green", "00ff00"]]`
68 | * `labeledBookmarks.defaultShape`: set which vector icon should be used as the default for new groups
69 | * `labeledBookmarks.overviewRulerLane`: set how the bookmark should be marked on the overview ruler (scrollbar)
70 | * `labeledBookmarks.lineEndLabelType`: set how the line end labels for labeled bookmarks should be displayed
71 |
72 | ## Tree View in the Activity Bar
73 |
74 | There are three views of the bookmarks available in the activity bar under the bookmark icon:
75 |
76 | * one for the active group
77 | * one for the inactive groups
78 | * one that shows all bookmarks grouped by files
79 |
80 | There are action buttons to activate groups, to rename items and to delete items.
81 |
82 | You can directly access this by using `ctrl+alt+b t`. If a text document is active at the time of using this shortcut and it has bookmarks, the nearest bookmark of the active group is highlighted in the tree view.
83 |
84 | 
85 |
86 | ## Status Bar
87 |
88 | The current active group and the number of bookmarks within it are displayed in the status bar.
89 |
90 | 
91 |
92 | ## Invalid Bookmarks
93 |
94 | This extension tries to follow file rename and delete actions initiated from within VSCode.
95 |
96 | * Upon rename, the bookmark is assigned to the new file name.
97 | * Upon delete, bookmarks belonging to the file are deleted.
98 |
99 | If a bookmark becomes invalid because of other kind of file changes (the file or the line it points to becomes unavailable), then the next time you try (and fail) to navigate to it, it gets flagged as having failed the jump. This is signaled with a warning icon in the navigation list. Such flagged bookmarks are skipped the next time. By repeatedly using the navigate to next/previous bookmark action, you can have all broken bookmarks marked as failing, and then the navigation works on the rest of the bookmarks normally.
100 |
101 | You can remove this broken bookmark flags:
102 |
103 | * by successfully navigating to them using `ctrl+alt+n` or `ctrl+alt+b n`
104 | * or by clearing all the flags using `ctrl+alt+b f`
105 |
106 | Or you can delete them using `ctrl+alt+b d` and selecting them manually.
107 |
108 | ## Known Issues
109 |
110 | * Bookmark icons might interfere with placing breakpoints. Use `ctrl+alt+b h` to hide/unhide the bookmark icons to avoid this.
111 | * On Mac the backward navigation shortcut `ctrl+alt+j` is also used by the notebook editor command "join with next cell" with the activation condition "notebookEditorFocused". If you happen to be using that, you might want to change the assignment of either of these conflicting actions. If you are not using notebooks, there should be no problem.
112 | * The content of the bookmarked line is stored within the bookmark itself for display purposes, and it is updated when changes in the file/line trigger a file changed event. However, not all changes to files trigger such an event. Git operations and even source code formatters might cause file changes without triggering a proper update of the bookmarks, and so markings can drift off the originally marked code and the stored line text might get off sync from the actual line content.
113 | * If a bookmark becomes invalid because the file got truncated by an outside action, and it now points to a not existing line, the bookmark's icon will float around at the end of the file. I don't want to go overboard with file system watching and what not, so if you see a suspiciously placed bookmark icon, try navigating to the next bookmark. If it is in fact invalid, it will get marked as such, and it will be easy to identify and delete it using `ctrl+alt+b d`.
114 | * Showing the closest bookmark in the tree view using `ctrl+alt+b t` sometimes fails to do its job properly. This is caused by the fact that tree views' inner state is not updated when they are invisible. Unfortunately, focusing on the closest bookmark would require that the tree view already knows about the current state of the bookmarks, even before it is visible. If the tree becomes visible, but does not jump to the closest bookmark, repeating the command should work as intended.
115 |
116 | ## If You Find Bugs
117 |
118 | Raise an issue on GitHub if you find something, but make sure to also provide information on how to reliably reproduce it.
119 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "labeled-bookmarks",
3 | "displayName": "Labeled Bookmarks",
4 | "description": "Bookmarks, multiple groups, customizable visuals",
5 | "version": "1.1.11",
6 | "publisher": "koalamer",
7 | "icon": "resources/vsc-labeled-bookmarks-logo.png",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/koalamer/vsc-labeled-bookmarks.git"
11 | },
12 | "engines": {
13 | "vscode": "^1.54.0"
14 | },
15 | "categories": [
16 | "Other"
17 | ],
18 | "activationEvents": [
19 | "onStartupFinished"
20 | ],
21 | "main": "./out/extension.js",
22 | "capabilities": {
23 | "untrustedWorkspaces": {
24 | "supported": true
25 | }
26 | },
27 | "contributes": {
28 | "commands": [
29 | {
30 | "command": "vsc-labeled-bookmarks.toggleBookmark",
31 | "title": "Bookmarks: toggle bookmark"
32 | },
33 | {
34 | "command": "vsc-labeled-bookmarks.toggleLabeledBookmark",
35 | "title": "Bookmarks: toggle labeled bookmark"
36 | },
37 | {
38 | "command": "vsc-labeled-bookmarks.navigateToBookmark",
39 | "title": "Bookmarks: navigate to bookmark"
40 | },
41 | {
42 | "command": "vsc-labeled-bookmarks.navigateToBookmarkOfAnyGroup",
43 | "title": "Bookmarks: navigate to bookmark of any group"
44 | },
45 | {
46 | "command": "vsc-labeled-bookmarks.selectGroup",
47 | "title": "Bookmarks: select group"
48 | },
49 | {
50 | "command": "vsc-labeled-bookmarks.addGroup",
51 | "title": "Bookmarks: add group"
52 | },
53 | {
54 | "command": "vsc-labeled-bookmarks.deleteGroup",
55 | "title": "Bookmarks: delete group"
56 | },
57 | {
58 | "command": "vsc-labeled-bookmarks.setGroupIconShape",
59 | "title": "Bookmarks: set group icon shape"
60 | },
61 | {
62 | "command": "vsc-labeled-bookmarks.setGroupIconColor",
63 | "title": "Bookmarks: set group icon color"
64 | },
65 | {
66 | "command": "vsc-labeled-bookmarks.deleteBookmark",
67 | "title": "Bookmarks: delete bookmark"
68 | },
69 | {
70 | "command": "vsc-labeled-bookmarks.toggleHideAll",
71 | "title": "Bookmarks: toggle hiding all bookmark decorations"
72 | },
73 | {
74 | "command": "vsc-labeled-bookmarks.toggleHideInactiveGroups",
75 | "title": "Bookmarks: toggle hiding inactive group decorations"
76 | },
77 | {
78 | "command": "vsc-labeled-bookmarks.navigateToNextBookmark",
79 | "title": "Bookmarks: navigate to next bookmark"
80 | },
81 | {
82 | "command": "vsc-labeled-bookmarks.navigateToPreviousBookmark",
83 | "title": "Bookmarks: navigate to previous bookmark"
84 | },
85 | {
86 | "command": "vsc-labeled-bookmarks.clearFailedJumpFlags",
87 | "title": "Bookmarks: clear failed jump flags from bookmarks"
88 | },
89 | {
90 | "command": "vsc-labeled-bookmarks.expandSelectionToNextBookmark",
91 | "title": "Bookmarks: expand selection to next bookmark"
92 | },
93 | {
94 | "command": "vsc-labeled-bookmarks.expandSelectionToPreviousBookmark",
95 | "title": "Bookmarks: expand selection to previous bookmark"
96 | },
97 | {
98 | "command": "vsc-labeled-bookmarks.refreshTreeView",
99 | "title": "Bookmarks: refresh tree view",
100 | "icon": "$(refresh)"
101 | },
102 | {
103 | "command": "vsc-labeled-bookmarks.jumpToBookmark",
104 | "title": "Bookmarks: jump to bookmark"
105 | },
106 | {
107 | "command": "vsc-labeled-bookmarks.showTreeView",
108 | "title": "Bookmarks: show tree view"
109 | },
110 | {
111 | "command": "vsc-labeled-bookmarks.moveBookmarksFromActiveGroup",
112 | "title": "Bookmarks: move bookmarks"
113 | },
114 | {
115 | "command": "vsc-labeled-bookmarks.activateTreeItem",
116 | "title": "Activate bookmark tree item",
117 | "icon": "$(eye)"
118 | },
119 | {
120 | "command": "vsc-labeled-bookmarks.editTreeItem",
121 | "title": "Edit bookmark tree item",
122 | "icon": "$(edit)"
123 | },
124 | {
125 | "command": "vsc-labeled-bookmarks.deleteTreeItem",
126 | "title": "Remove bookmark tree item",
127 | "icon": "$(remove-close)"
128 | }
129 | ],
130 | "keybindings": [
131 | {
132 | "command": "vsc-labeled-bookmarks.toggleBookmark",
133 | "key": "ctrl+alt+m",
134 | "when": "editorTextFocus"
135 | },
136 | {
137 | "command": "vsc-labeled-bookmarks.toggleLabeledBookmark",
138 | "key": "ctrl+alt+l",
139 | "when": "editorTextFocus"
140 | },
141 | {
142 | "command": "vsc-labeled-bookmarks.navigateToNextBookmark",
143 | "key": "ctrl+alt+k",
144 | "when": "editorTextFocus"
145 | },
146 | {
147 | "command": "vsc-labeled-bookmarks.navigateToPreviousBookmark",
148 | "key": "ctrl+alt+j",
149 | "when": "editorTextFocus"
150 | },
151 | {
152 | "command": "vsc-labeled-bookmarks.expandSelectionToNextBookmark",
153 | "key": "shift+alt+k",
154 | "when": "editorTextFocus"
155 | },
156 | {
157 | "command": "vsc-labeled-bookmarks.expandSelectionToPreviousBookmark",
158 | "key": "shift+alt+j",
159 | "when": "editorTextFocus"
160 | },
161 | {
162 | "command": "vsc-labeled-bookmarks.navigateToBookmark",
163 | "key": "ctrl+alt+n"
164 | },
165 | {
166 | "command": "vsc-labeled-bookmarks.selectGroup",
167 | "key": "ctrl+alt+b g"
168 | },
169 | {
170 | "command": "vsc-labeled-bookmarks.addGroup",
171 | "key": "ctrl+alt+b alt+g"
172 | },
173 | {
174 | "command": "vsc-labeled-bookmarks.deleteGroup",
175 | "key": "ctrl+alt+b shift+g"
176 | },
177 | {
178 | "command": "vsc-labeled-bookmarks.setGroupIconShape",
179 | "key": "ctrl+alt+b s"
180 | },
181 | {
182 | "command": "vsc-labeled-bookmarks.setGroupIconColor",
183 | "key": "ctrl+alt+b c"
184 | },
185 | {
186 | "command": "vsc-labeled-bookmarks.navigateToBookmarkOfAnyGroup",
187 | "key": "ctrl+alt+b n"
188 | },
189 | {
190 | "command": "vsc-labeled-bookmarks.deleteBookmark",
191 | "key": "ctrl+alt+b d"
192 | },
193 | {
194 | "command": "vsc-labeled-bookmarks.toggleHideAll",
195 | "key": "ctrl+alt+b h"
196 | },
197 | {
198 | "command": "vsc-labeled-bookmarks.toggleHideInactiveGroups",
199 | "key": "ctrl+alt+b i"
200 | },
201 | {
202 | "command": "vsc-labeled-bookmarks.clearFailedJumpFlags",
203 | "key": "ctrl+alt+b f"
204 | },
205 | {
206 | "command": "vsc-labeled-bookmarks.showTreeView",
207 | "key": "ctrl+alt+b t"
208 | },
209 | {
210 | "command": "vsc-labeled-bookmarks.moveBookmarksFromActiveGroup",
211 | "key": "ctrl+alt+b m"
212 | }
213 | ],
214 | "configuration": {
215 | "title": "Labeled Bookmarks",
216 | "properties": {
217 | "labeledBookmarks.colors": {
218 | "type": "array",
219 | "description": "Array of names and color codes that will be available as bookmark group colors, eg: [[\"red\": \"ff0000\"], [\"green\": \"00ff00\"]]",
220 | "default": [
221 | [
222 | "teal",
223 | "#00dddd"
224 | ],
225 | [
226 | "blue",
227 | "#0000dd"
228 | ],
229 | [
230 | "magenta",
231 | "#dd00dd"
232 | ],
233 | [
234 | "red",
235 | "#dd0000"
236 | ],
237 | [
238 | "yellow",
239 | "#dddd00"
240 | ],
241 | [
242 | "green",
243 | "#00dd00"
244 | ]
245 | ]
246 | },
247 | "labeledBookmarks.unicodeMarkers": {
248 | "type": "array",
249 | "description": "Array of names and unicode characters that will be available as extra decoration shapes for bookmark groups, eg: [[\"look\": \"👀\"], [\"bug\",\"🐞\"]]",
250 | "default": [
251 | [
252 | "look",
253 | "👀"
254 | ],
255 | [
256 | "bug",
257 | "🐞"
258 | ],
259 | [
260 | "asterix",
261 | "✱"
262 | ],
263 | [
264 | "diamond",
265 | "◈"
266 | ]
267 | ]
268 | },
269 | "labeledBookmarks.defaultShape": {
270 | "type": "string",
271 | "description": "The default shape for new bookmark groups",
272 | "default": "bookmark",
273 | "enum": [
274 | "bookmark",
275 | "circle",
276 | "heart",
277 | "label",
278 | "star"
279 | ]
280 | },
281 | "labeledBookmarks.overviewRulerLane": {
282 | "type": "string",
283 | "description": "The placement of bookmark markers in the overview ruler (the scrollbar)",
284 | "default": "center",
285 | "enum": [
286 | "center",
287 | "full",
288 | "left",
289 | "right",
290 | "none"
291 | ]
292 | },
293 | "labeledBookmarks.lineEndLabelType": {
294 | "type": "string",
295 | "description": "The style of how the line end decorations for labeled bookmarks should appear",
296 | "default": "bordered",
297 | "enum": [
298 | "none",
299 | "bordered",
300 | "inverse"
301 | ]
302 | }
303 | }
304 | },
305 | "viewsContainers": {
306 | "activitybar": [
307 | {
308 | "id": "labeled-bookmarks-tree-view",
309 | "title": "Labeled Bookmarks",
310 | "icon": "$(bookmark)"
311 | }
312 | ]
313 | },
314 | "views": {
315 | "labeled-bookmarks-tree-view": [
316 | {
317 | "id": "bookmarksByActiveGroup",
318 | "name": "Active Group",
319 | "icon": "$(bookmark)"
320 | },
321 | {
322 | "id": "bookmarksByInactiveGroups",
323 | "name": "Inactive Groups",
324 | "icon": "$(bookmark)"
325 | },
326 | {
327 | "id": "bookmarksByFile",
328 | "name": "By File",
329 | "icon": "$(bookmark)"
330 | }
331 | ]
332 | },
333 | "viewsWelcome": [
334 | {
335 | "view": "bookmarksByActiveGroup",
336 | "contents": "Loading..."
337 | }
338 | ],
339 | "menus": {
340 | "view/item/context": [
341 | {
342 | "command": "vsc-labeled-bookmarks.activateTreeItem",
343 | "when": "view == bookmarksByInactiveGroups && viewItem == group",
344 | "group": "inline"
345 | },
346 | {
347 | "command": "vsc-labeled-bookmarks.editTreeItem",
348 | "when": "view == bookmarksByActiveGroup && viewItem != none && viewItem != file || view == bookmarksByInactiveGroups && viewItem != none && viewItem != file || view == bookmarksByFile && viewItem != none && viewItem != file",
349 | "group": "inline"
350 | },
351 | {
352 | "command": "vsc-labeled-bookmarks.deleteTreeItem",
353 | "when": "view == bookmarksByActiveGroup && viewItem != none || view == bookmarksByInactiveGroups && viewItem != none || view == bookmarksByFile && viewItem != none ",
354 | "group": "inline"
355 | }
356 | ]
357 | }
358 | },
359 | "scripts": {
360 | "vscode:prepublish": "npm run compile",
361 | "compile": "tsc -p ./",
362 | "watch": "tsc -watch -p ./",
363 | "pretest": "npm run compile && npm run lint",
364 | "lint": "eslint src --ext ts",
365 | "test": "node ./out/test/runTest.js"
366 | },
367 | "devDependencies": {
368 | "@types/glob": "^7.1.3",
369 | "@types/mocha": "^8.0.4",
370 | "@types/node": "^12.11.7",
371 | "@types/vscode": "^1.54.0",
372 | "@typescript-eslint/eslint-plugin": "^4.14.1",
373 | "@typescript-eslint/parser": "^4.14.1",
374 | "eslint": "^7.19.0",
375 | "glob": "^7.1.6",
376 | "mocha": "^10.0.0",
377 | "typescript": "^4.1.3",
378 | "vscode-test": "^1.5.0"
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Group } from "./group";
3 | import {
4 | ExtensionContext,
5 | FileDeleteEvent, FileRenameEvent,
6 | OverviewRulerLane,
7 | Range, Selection,
8 | StatusBarItem,
9 | TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType
10 | } from 'vscode';
11 | import { DecorationFactory } from './decoration_factory';
12 | import { GroupPickItem } from './group_pick_item';
13 | import { BookmarkPickItem } from './bookmark_pick_item';
14 | import { ShapePickItem } from './shape_pick_item';
15 | import { ColorPickItem } from './color_pick_item';
16 | import { Bookmark } from "./bookmark";
17 | import { SerializableGroup } from "./serializable_group";
18 | import { SerializableBookmark } from "./serializable_bookmark";
19 | import { BookmarkDataProvider } from './interface/bookmark_data_provider';
20 | import { BookmarManager } from './interface/bookmark_manager';
21 |
22 | export class Main implements BookmarkDataProvider, BookmarManager {
23 | public ctx: ExtensionContext;
24 | private treeViewRefreshCallback = () => { };
25 |
26 | public readonly savedBookmarksKey = "vscLabeledBookmarks.bookmarks";
27 | public readonly savedGroupsKey = "vscLabeledBookmarks.groups";
28 | public readonly savedActiveGroupKey = "vscLabeledBookmarks.activeGroup";
29 | public readonly savedHideInactiveGroupsKey = "vscLabeledBookmarks.hideInactiveGroups";
30 | public readonly savedHideAllKey = "vscLabeledBookmarks.hideAll";
31 |
32 | public readonly configRoot = "labeledBookmarks";
33 | public readonly configKeyColors = "colors";
34 | public readonly configKeyUnicodeMarkers = "unicodeMarkers";
35 | public readonly configKeyDefaultShape = "defaultShape";
36 | public readonly configOverviewRulerLane = "overviewRulerLane";
37 | public readonly configLineEndLabelType = "lineEndLabelType";
38 |
39 | public readonly maxGroupNameLength = 40;
40 |
41 | public readonly defaultGroupName: string;
42 |
43 | public groups: Array;
44 | private bookmarks: Array;
45 |
46 | public activeGroup: Group;
47 | public fallbackColor: string = "00ddddff";
48 | public fallbackColorName: string = "teal";
49 |
50 | public colors: Map;
51 | public unicodeMarkers: Map;
52 | public readonly shapes: Map;
53 | public defaultShape = "bookmark";
54 |
55 | public hideInactiveGroups: boolean;
56 | public hideAll: boolean;
57 |
58 | private statusBarItem: StatusBarItem;
59 |
60 | private removedDecorations: Map;
61 |
62 | private tempDocumentBookmarks: Map>;
63 | private tempGroupBookmarks: Map>;
64 | private tempDocumentDecorations: Map>>;
65 |
66 | private decorationFactory: DecorationFactory;
67 |
68 | constructor(ctx: ExtensionContext, treeviewRefreshCallback: () => void) {
69 | this.ctx = ctx;
70 | this.treeViewRefreshCallback = treeviewRefreshCallback;
71 |
72 | let gutterIconDirUri = vscode.Uri.joinPath(this.ctx.extensionUri, 'resources', 'gutter_icons');
73 | this.decorationFactory = new DecorationFactory(gutterIconDirUri, OverviewRulerLane.Center, "bordered");
74 |
75 | this.bookmarks = new Array();
76 | this.groups = new Array();
77 | this.defaultGroupName = "default";
78 | this.activeGroup = new Group(this.defaultGroupName, this.fallbackColor, this.defaultShape, "", this.decorationFactory);
79 |
80 | this.colors = new Map();
81 | this.unicodeMarkers = new Map();
82 | this.shapes = new Map([
83 | ["bookmark", "bookmark"],
84 | ["circle", "circle"],
85 | ["heart", "heart"],
86 | ["label", "label"],
87 | ["star", "star"]
88 | ]);
89 |
90 | this.removedDecorations = new Map();
91 |
92 | this.tempDocumentBookmarks = new Map>();
93 | this.tempGroupBookmarks = new Map>();
94 | this.tempDocumentDecorations = new Map>>();
95 |
96 | this.readSettings();
97 |
98 | if (this.colors.size < 1) {
99 | this.colors.set(this.fallbackColorName, this.decorationFactory.normalizeColorFormat(this.fallbackColor));
100 | }
101 |
102 | this.hideInactiveGroups = false;
103 | this.hideAll = false;
104 |
105 | this.restoreSavedState();
106 |
107 | this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1);
108 | this.statusBarItem.command = 'vsc-labeled-bookmarks.selectGroup';
109 | this.statusBarItem.show();
110 |
111 | this.saveState();
112 |
113 | this.updateDecorations();
114 | }
115 |
116 | public saveState() {
117 | let serializedGroups = this.groups.map(group => SerializableGroup.fromGroup(group));
118 | this.ctx.workspaceState.update(this.savedGroupsKey, serializedGroups);
119 |
120 | let serializedBookmarks = this.bookmarks.map(bookmark => SerializableBookmark.fromBookmark(bookmark));
121 | this.ctx.workspaceState.update(this.savedBookmarksKey, serializedBookmarks);
122 |
123 | this.ctx.workspaceState.update(this.savedActiveGroupKey, this.activeGroup.name);
124 | this.ctx.workspaceState.update(this.savedHideInactiveGroupsKey, this.hideInactiveGroups);
125 | this.ctx.workspaceState.update(this.savedHideAllKey, this.hideAll);
126 |
127 | this.updateStatusBar();
128 | }
129 |
130 | public handleDecorationRemoved(decoration: TextEditorDecorationType) {
131 | this.removedDecorations.set(decoration, true);
132 | }
133 |
134 | public handleGroupDecorationUpdated(group: Group) {
135 | this.tempDocumentDecorations.clear();
136 | this.tempGroupBookmarks.get(group)?.forEach(bookmark => {
137 | bookmark.initDecoration();
138 | });
139 | this.updateDecorations();
140 | this.treeViewRefreshCallback();
141 | }
142 |
143 | public handleGroupDecorationSwitched(group: Group) {
144 | this.tempDocumentDecorations.clear();
145 | this.tempGroupBookmarks.get(group)?.forEach(bookmark => {
146 | bookmark.switchDecoration();
147 | });
148 | this.updateDecorations();
149 | this.treeViewRefreshCallback();
150 | }
151 |
152 | public handleBookmarkDecorationUpdated(bookmark: Bookmark) {
153 | this.tempDocumentDecorations.delete(bookmark.fsPath);
154 | this.updateDecorations();
155 | }
156 |
157 | public getGroups(): Array {
158 | return this.groups;
159 | }
160 |
161 | public getBookmarks(): Array {
162 | return this.bookmarks;
163 | }
164 |
165 | public getActiveGroup(): Group {
166 | return this.activeGroup;
167 | }
168 |
169 | private updateDecorations() {
170 | for (let editor of vscode.window.visibleTextEditors) {
171 | this.updateEditorDecorations(editor);
172 | }
173 |
174 | this.removedDecorations.clear();
175 | }
176 |
177 | private getGroupByName(groupName: string): Group {
178 | for (let g of this.groups) {
179 | if (g.name === groupName) {
180 | return g;
181 | }
182 | }
183 |
184 | return this.activeGroup;
185 | }
186 |
187 | public updateEditorDecorations(textEditor: TextEditor | undefined) {
188 | if (typeof textEditor === "undefined") {
189 | return;
190 | }
191 |
192 | let fsPath = textEditor.document.uri.fsPath;
193 | let editorDecorations = this.getTempDocumentDecorationsList(fsPath);
194 |
195 | for (let [removedDecoration, b] of this.removedDecorations) {
196 | if (editorDecorations.has(removedDecoration)) {
197 | continue;
198 | }
199 |
200 | editorDecorations.set(removedDecoration, []);
201 | }
202 |
203 | for (let [decoration, ranges] of editorDecorations) {
204 | textEditor.setDecorations(decoration, ranges);
205 | }
206 | }
207 |
208 | public onEditorDocumentChanged(event: TextDocumentChangeEvent) {
209 | let fsPath = event.document.uri.fsPath;
210 | let fileBookmarkList = this.getTempDocumentBookmarkList(fsPath);
211 |
212 | if (fileBookmarkList.length === 0) {
213 | return;
214 | }
215 |
216 | let bookmarksChanged = false;
217 |
218 | for (let change of event.contentChanges) {
219 | let newLineCount = this.getNlCount(change.text);
220 |
221 | let oldFirstLine = change.range.start.line;
222 | let oldLastLine = change.range.end.line;
223 | let oldLineCount = oldLastLine - oldFirstLine;
224 |
225 | if (newLineCount === oldLineCount) {
226 | let updateCount = this.updateBookmarkLineTextInRange(
227 | event.document,
228 | fileBookmarkList,
229 | oldFirstLine,
230 | oldLastLine
231 | );
232 | if (updateCount > 0) {
233 | this.treeViewRefreshCallback();
234 | }
235 | continue;
236 | }
237 |
238 |
239 | if (newLineCount > oldLineCount) {
240 | let shiftDownBy = newLineCount - oldLineCount;
241 | let newLastLine = oldFirstLine + newLineCount;
242 |
243 | let firstLinePrefix = event.document.getText(
244 | new Range(oldFirstLine, 0, oldFirstLine, change.range.start.character)
245 | );
246 | let isFirstLinePrefixEmpty = firstLinePrefix.trim() === "";
247 |
248 | let shiftDownFromLine = (isFirstLinePrefixEmpty ? oldFirstLine : oldFirstLine + 1);
249 |
250 | for (let bookmark of fileBookmarkList) {
251 | if (bookmark.lineNumber >= shiftDownFromLine) {
252 | bookmark.lineNumber += shiftDownBy;
253 | bookmarksChanged = true;
254 | }
255 |
256 | if (bookmark.lineNumber >= oldFirstLine && bookmark.lineNumber <= newLastLine) {
257 | this.updateBookmarkLineText(event.document, bookmark);
258 | this.treeViewRefreshCallback();
259 | }
260 | }
261 | continue;
262 | }
263 |
264 |
265 | if (newLineCount < oldLineCount) {
266 | let shiftUpBy = oldLineCount - newLineCount;
267 | let newLastLine = oldFirstLine + newLineCount;
268 |
269 | let firstLinePrefix = event.document.getText(
270 | new Range(oldFirstLine, 0, oldFirstLine, change.range.start.character)
271 | );
272 | let isFirstLineBookkmarkDeletable = firstLinePrefix.trim() === "";
273 |
274 | if (!isFirstLineBookkmarkDeletable) {
275 | let firstLineBookmark = fileBookmarkList.find(bookmark => bookmark.lineNumber === oldFirstLine);
276 | if (typeof firstLineBookmark === "undefined") {
277 | isFirstLineBookkmarkDeletable = true;
278 | }
279 | }
280 |
281 | let deleteFromLine = (isFirstLineBookkmarkDeletable ? oldFirstLine : oldFirstLine + 1);
282 | let shiftFromLine = deleteFromLine + shiftUpBy;
283 |
284 | for (let bookmark of fileBookmarkList) {
285 | if (bookmark.lineNumber < oldFirstLine) {
286 | continue;
287 | }
288 |
289 | if (bookmark.lineNumber >= deleteFromLine && bookmark.lineNumber < shiftFromLine) {
290 | this.deleteBookmark(bookmark);
291 | bookmarksChanged = true;
292 | continue;
293 | }
294 |
295 | if (bookmark.lineNumber >= shiftFromLine) {
296 | bookmark.lineNumber -= shiftUpBy;
297 | bookmarksChanged = true;
298 | }
299 |
300 | if (bookmark.lineNumber >= oldFirstLine && bookmark.lineNumber <= newLastLine) {
301 | this.updateBookmarkLineText(event.document, bookmark);
302 | this.treeViewRefreshCallback();
303 | }
304 | }
305 | continue;
306 | }
307 | }
308 |
309 | if (bookmarksChanged) {
310 | this.tempDocumentDecorations.delete(fsPath);
311 | this.saveState();
312 | this.updateDecorations();
313 | this.treeViewRefreshCallback();
314 | }
315 | }
316 |
317 | private getTempDocumentBookmarkList(fsPath: string): Array {
318 | let list = this.tempDocumentBookmarks.get(fsPath);
319 |
320 | if (typeof list !== "undefined") {
321 | return list;
322 | }
323 |
324 | list = this.bookmarks.filter((bookmark) => { return bookmark.fsPath === fsPath; });
325 | this.tempDocumentBookmarks.set(fsPath, list);
326 |
327 | return list;
328 | }
329 |
330 | private getTempGroupBookmarkList(group: Group): Array {
331 | let list = this.tempGroupBookmarks.get(group);
332 |
333 | if (typeof list !== "undefined") {
334 | return list;
335 | }
336 |
337 | list = this.bookmarks.filter((bookmark) => { return bookmark.group === group; });
338 | this.tempGroupBookmarks.set(group, list);
339 |
340 | return list;
341 | }
342 |
343 | private getTempDocumentDecorationsList(fsPath: string): Map> {
344 | let editorDecorations = this.tempDocumentDecorations.get(fsPath);
345 |
346 | if (typeof editorDecorations !== "undefined") {
347 | return editorDecorations;
348 | }
349 |
350 | let lineDecorations = new Map();
351 | let fileBookmarks = this.bookmarks
352 | .filter((bookmark) => {
353 | return bookmark.fsPath === fsPath && bookmark.getDecoration !== null;
354 | });
355 |
356 | fileBookmarks.filter(bookmark => bookmark.group === this.activeGroup)
357 | .forEach(bookmark => {
358 | let decoration = bookmark.getDecoration();
359 | if (decoration !== null) {
360 | lineDecorations.set(bookmark.lineNumber, decoration);
361 | }
362 | });
363 |
364 | fileBookmarks.filter(bookmark => bookmark.group !== this.activeGroup)
365 | .forEach((bookmark) => {
366 | let decoration = bookmark.getDecoration();
367 | if (decoration !== null) {
368 | if (!lineDecorations.has(bookmark.lineNumber)) {
369 | lineDecorations.set(bookmark.lineNumber, decoration);
370 | } else {
371 | this.handleDecorationRemoved(decoration);
372 | }
373 | }
374 | });
375 |
376 | editorDecorations = new Map();
377 | for (let [lineNumber, decoration] of lineDecorations) {
378 | let ranges = editorDecorations.get(decoration);
379 | if (typeof ranges === "undefined") {
380 | ranges = new Array();
381 | editorDecorations.set(decoration, ranges);
382 | }
383 |
384 | ranges.push(new Range(lineNumber, 0, lineNumber, 0));
385 | }
386 |
387 | this.tempDocumentDecorations.set(fsPath, editorDecorations);
388 |
389 | return editorDecorations;
390 | }
391 |
392 | private resetTempLists() {
393 | this.tempDocumentBookmarks.clear();
394 | this.tempGroupBookmarks.clear();
395 | this.tempDocumentDecorations.clear();
396 | }
397 |
398 | private updateBookmarkLineTextInRange(
399 | document: TextDocument,
400 | bookmarks: Array,
401 | firstLine: number,
402 | lastLine: number
403 | ): number {
404 | let updateCount = 0;
405 | bookmarks.filter(bookmark => {
406 | return bookmark.lineNumber >= firstLine && bookmark.lineNumber <= lastLine;
407 | }).forEach(bookmark => {
408 | this.updateBookmarkLineText(document, bookmark);
409 | updateCount++;
410 | });
411 | return updateCount;
412 | }
413 |
414 | private updateBookmarkLineText(document: TextDocument, bookmark: Bookmark) {
415 | let line = document.lineAt(bookmark.lineNumber);
416 | bookmark.characterNumber = Math.min(bookmark.characterNumber, line.range.end.character);
417 | bookmark.lineText = line.text.trim();
418 | }
419 |
420 | public actionDeleteOneBookmark(bookmark: Bookmark) {
421 | this.deleteBookmark(bookmark);
422 | this.saveState();
423 | this.updateDecorations();
424 | this.treeViewRefreshCallback();
425 | }
426 |
427 | public deleteBookmarksOfFile(fsPath: string, group: Group | null) {
428 | this.bookmarks
429 | .filter(b => (b.fsPath === fsPath && (group === null || group === b.group)))
430 | .forEach(b => this.deleteBookmark(b));
431 | this.saveState();
432 | this.updateDecorations();
433 | this.treeViewRefreshCallback();
434 | }
435 |
436 | private deleteBookmark(bookmark: Bookmark) {
437 | let index = this.bookmarks.indexOf(bookmark);
438 | if (index < 0) {
439 | return;
440 | }
441 |
442 | this.bookmarks.splice(index, 1);
443 |
444 | this.tempDocumentBookmarks.delete(bookmark.fsPath);
445 | this.tempDocumentDecorations.delete(bookmark.fsPath);
446 | this.tempGroupBookmarks.delete(bookmark.group);
447 | let bookmarkDecoration = bookmark.getDecoration();
448 | if (bookmarkDecoration !== null) {
449 | this.handleDecorationRemoved(bookmarkDecoration);
450 | this.handleDecorationRemoved(bookmark.group.decoration);
451 | }
452 | }
453 |
454 | public relabelBookmark(bookmark: Bookmark) {
455 | let defaultQuickInputText = bookmark.label ?? '';
456 |
457 | vscode.window.showInputBox({
458 | placeHolder: "new bookmark label",
459 | prompt: "Enter new bookmark label",
460 | value: defaultQuickInputText,
461 | valueSelection: [0, defaultQuickInputText.length],
462 | }).then(input => {
463 | if (typeof input === "undefined") {
464 | return;
465 | }
466 |
467 | let newLabel: string | undefined = input.trim();
468 |
469 | if (newLabel === defaultQuickInputText) {
470 | return;
471 | }
472 |
473 | if (newLabel.length === 1) {
474 | let existingBookmark = this.getTempDocumentBookmarkList(bookmark.fsPath)
475 | .find((bm) => {
476 | return bm.group === bookmark.group
477 | && typeof bm.label !== "undefined"
478 | && bm.label === newLabel;
479 | });
480 |
481 | if (typeof existingBookmark !== "undefined") {
482 | this.deleteBookmark(existingBookmark);
483 | }
484 | }
485 |
486 | if (newLabel.length === 0) {
487 | newLabel = undefined;
488 | }
489 |
490 | let newBookmark = new Bookmark(
491 | bookmark.fsPath,
492 | bookmark.lineNumber,
493 | bookmark.characterNumber,
494 | newLabel,
495 | bookmark.lineText,
496 | bookmark.group,
497 | this.decorationFactory
498 | );
499 |
500 | this.deleteBookmark(bookmark);
501 |
502 | this.addNewDecoratedBookmark(newBookmark);
503 | this.bookmarks.sort(Bookmark.sortByLocation);
504 |
505 | this.tempDocumentDecorations.delete(bookmark.fsPath);
506 | this.tempDocumentBookmarks.delete(bookmark.fsPath);
507 | this.tempGroupBookmarks.delete(this.activeGroup);
508 | this.saveState();
509 | this.updateDecorations();
510 | this.treeViewRefreshCallback();
511 | });
512 | }
513 |
514 | public renameGroup(group: Group) {
515 | let defaultQuickInputText = group.name;
516 |
517 | vscode.window.showInputBox({
518 | placeHolder: "new group name",
519 | prompt: "Enter new group name",
520 | value: defaultQuickInputText,
521 | valueSelection: [0, defaultQuickInputText.length],
522 | }).then(input => {
523 | if (typeof input === "undefined") {
524 | return;
525 | }
526 |
527 | let newName = input.trim();
528 |
529 | if (newName.length === 0) {
530 | return;
531 | }
532 |
533 | if (newName === defaultQuickInputText) {
534 | return;
535 | }
536 |
537 | if (newName.length > this.maxGroupNameLength) {
538 | vscode.window.showErrorMessage(
539 | "Choose a maximum " +
540 | this.maxGroupNameLength +
541 | " character long group name."
542 | );
543 | return;
544 | }
545 |
546 | if (typeof this.groups.find(g => {
547 | return g !== group && g.name === newName;
548 | }) !== "undefined") {
549 | vscode.window.showErrorMessage("The entered bookmark group name is already in use");
550 | return;
551 | }
552 |
553 | group.name = newName;
554 |
555 | this.saveState();
556 | this.treeViewRefreshCallback();
557 | this.updateStatusBar();
558 | });
559 | }
560 |
561 | public editorActionToggleBookmark(textEditor: TextEditor) {
562 | if (textEditor.selections.length === 0) {
563 | return;
564 | }
565 |
566 | let documentFsPath = textEditor.document.uri.fsPath;
567 | for (let selection of textEditor.selections) {
568 | let lineNumber = selection.start.line;
569 | let characterNumber = selection.start.character;
570 | let lineText = textEditor.document.lineAt(lineNumber).text.trim();
571 | this.toggleBookmark(
572 | documentFsPath,
573 | lineNumber,
574 | characterNumber,
575 | lineText,
576 | this.activeGroup
577 | );
578 | }
579 |
580 | this.updateDecorations();
581 | this.treeViewRefreshCallback();
582 | }
583 |
584 | private toggleBookmark(
585 | fsPath: string,
586 | lineNumber: number,
587 | characterNumber: number,
588 | lineText: string,
589 | group: Group
590 | ) {
591 | let existingBookmark = this.getTempDocumentBookmarkList(fsPath)
592 | .find((bookmark) => { return bookmark.lineNumber === lineNumber && bookmark.group === group; });
593 |
594 | if (typeof existingBookmark !== "undefined") {
595 | this.deleteBookmark(existingBookmark);
596 | this.saveState();
597 | return;
598 | }
599 |
600 | let bookmark = new Bookmark(fsPath,
601 | lineNumber,
602 | characterNumber,
603 | undefined,
604 | lineText,
605 | group,
606 | this.decorationFactory
607 | );
608 | this.bookmarks.push(bookmark);
609 | this.bookmarks.sort(Bookmark.sortByLocation);
610 |
611 | this.tempDocumentBookmarks.delete(fsPath);
612 | this.tempDocumentDecorations.delete(fsPath);
613 | this.tempGroupBookmarks.delete(group);
614 |
615 | this.saveState();
616 | }
617 |
618 | public editorActionToggleLabeledBookmark(textEditor: TextEditor) {
619 | if (textEditor.selections.length === 0) {
620 | return;
621 | }
622 |
623 | let fsPath = textEditor.document.uri.fsPath;
624 | let lineNumber = textEditor.selection.start.line;
625 |
626 | let existingBookmark = this.getTempDocumentBookmarkList(fsPath)
627 | .find((bookmark) => { return bookmark.lineNumber === lineNumber && bookmark.group === this.activeGroup; });
628 |
629 | if (typeof existingBookmark !== "undefined") {
630 | this.deleteBookmark(existingBookmark);
631 | this.saveState();
632 | this.updateDecorations();
633 | this.treeViewRefreshCallback();
634 | return;
635 | }
636 |
637 | let selectedText = textEditor.document.getText(textEditor.selection).trim();
638 | let firstNlPos = selectedText.indexOf("\n");
639 | if (firstNlPos >= 0) {
640 | selectedText = selectedText.substring(0, firstNlPos).trim();
641 | }
642 | selectedText = selectedText.replace(/[\s\t\r\n]+/, " ").replace("@", "@\u200b");
643 |
644 | vscode.window.showInputBox({
645 | placeHolder: "label or label@@group or @@group",
646 | prompt: "Enter label and/or group to be created",
647 | value: selectedText,
648 | valueSelection: [0, selectedText.length],
649 | }).then(input => {
650 | if (typeof input === "undefined") {
651 | return;
652 | }
653 |
654 | input = input.trim();
655 | if (input === "") {
656 | return;
657 | }
658 |
659 | let label = "";
660 | let groupName = "";
661 |
662 | let separatorPos = input.indexOf('@@');
663 | if (separatorPos >= 0) {
664 | label = input.substring(0, separatorPos).trim();
665 | groupName = input.substring(separatorPos + 2).trim();
666 | } else {
667 | label = input.replace("@\u200b", "@");
668 | }
669 |
670 | if (label === "" && groupName === "") {
671 | return;
672 | }
673 |
674 | if (groupName.length > this.maxGroupNameLength) {
675 | vscode.window.showErrorMessage(
676 | "Choose a maximum " +
677 | this.maxGroupNameLength +
678 | " character long group name."
679 | );
680 | return;
681 | }
682 |
683 | if (groupName !== "") {
684 | this.activateGroup(groupName);
685 | }
686 |
687 | if (label.length === 1) {
688 | let existingLabeledBookmark = this.getTempDocumentBookmarkList(fsPath)
689 | .find((bookmark) => {
690 | return bookmark.group === this.activeGroup
691 | && typeof bookmark.label !== "undefined"
692 | && bookmark.label === label;
693 | });
694 |
695 | if (typeof existingLabeledBookmark !== "undefined") {
696 | this.deleteBookmark(existingLabeledBookmark);
697 | }
698 | }
699 |
700 | if (label !== "") {
701 | let characterNumber = textEditor.selection.start.character;
702 | let lineText = textEditor.document.lineAt(lineNumber).text.trim();
703 |
704 | let bookmark = new Bookmark(
705 | fsPath,
706 | lineNumber,
707 | characterNumber,
708 | label,
709 | lineText,
710 | this.activeGroup,
711 | this.decorationFactory
712 | );
713 | this.addNewDecoratedBookmark(bookmark);
714 | this.bookmarks.sort(Bookmark.sortByLocation);
715 | }
716 |
717 | this.tempDocumentDecorations.delete(fsPath);
718 | this.tempDocumentBookmarks.delete(fsPath);
719 | this.tempGroupBookmarks.delete(this.activeGroup);
720 | this.saveState();
721 | this.updateDecorations();
722 | this.treeViewRefreshCallback();
723 | });
724 | }
725 |
726 | public editorActionnavigateToNextBookmark(textEditor: TextEditor) {
727 | if (textEditor.selections.length === 0) {
728 | return;
729 | }
730 |
731 | let documentFsPath = textEditor.document.uri.fsPath;
732 | let lineNumber = textEditor.selection.start.line;
733 |
734 | let nextBookmark = this.nextBookmark(documentFsPath, lineNumber);
735 | if (typeof nextBookmark === "undefined") {
736 | return;
737 | }
738 |
739 | this.jumpToBookmark(nextBookmark);
740 | }
741 |
742 | public nextBookmark(fsPath: string, line: number): Bookmark | undefined {
743 | let brokenBookmarkCount = 0;
744 |
745 | let groupBookmarkList = this.getTempGroupBookmarkList(this.activeGroup);
746 |
747 | let firstCandidate = groupBookmarkList.find((bookmark, i) => {
748 | if (bookmark.failedJump) {
749 | brokenBookmarkCount++;
750 | return false;
751 | }
752 |
753 | let fileComparisonResult = bookmark.fsPath.localeCompare(fsPath);
754 |
755 | if (fileComparisonResult < 0) {
756 | return false;
757 | }
758 | if (fileComparisonResult > 0) {
759 | return true;
760 | }
761 |
762 | return line < bookmark.lineNumber;
763 | });
764 |
765 | if (typeof firstCandidate === "undefined" && groupBookmarkList.length > 0) {
766 | if (groupBookmarkList.length > brokenBookmarkCount) {
767 | for (let bookmark of groupBookmarkList) {
768 | if (!bookmark.failedJump) {
769 | return bookmark;
770 | }
771 | }
772 | }
773 | vscode.window.showWarningMessage("All bookmarks are broken, time for some cleanup");
774 | }
775 |
776 | return firstCandidate;
777 | }
778 |
779 | public editorActionNavigateToPreviousBookmark(textEditor: TextEditor) {
780 | if (textEditor.selections.length === 0) {
781 | return;
782 | }
783 |
784 | let documentFsPath = textEditor.document.uri.fsPath;
785 | let lineNumber = textEditor.selection.start.line;
786 |
787 | let previousBookmark = this.previousBookmark(documentFsPath, lineNumber);
788 | if (typeof previousBookmark === "undefined") {
789 | return;
790 | }
791 |
792 | this.jumpToBookmark(previousBookmark);
793 | }
794 |
795 | public previousBookmark(fsPath: string, line: number): Bookmark | undefined {
796 | let brokenBookmarkCount = 0;
797 |
798 | let groupBookmarkList = this.getTempGroupBookmarkList(this.activeGroup);
799 |
800 | let firstCandidate: Bookmark | undefined;
801 |
802 | for (let i = groupBookmarkList.length - 1; i >= 0; i--) {
803 | let bookmark = groupBookmarkList[i];
804 |
805 | if (bookmark.failedJump) {
806 | brokenBookmarkCount++;
807 | continue;
808 | }
809 |
810 | let fileComparisonResult = bookmark.fsPath.localeCompare(fsPath);
811 | if (fileComparisonResult > 0) {
812 | continue;
813 | }
814 |
815 | if (fileComparisonResult < 0) {
816 | firstCandidate = bookmark;
817 | break;
818 | }
819 |
820 | if (bookmark.lineNumber < line) {
821 | firstCandidate = bookmark;
822 | break;
823 | }
824 | }
825 |
826 | if (typeof firstCandidate === "undefined" && groupBookmarkList.length > 0) {
827 | if (groupBookmarkList.length > brokenBookmarkCount) {
828 | for (let i = groupBookmarkList.length - 1; i >= 0; i--) {
829 | if (!groupBookmarkList[i].failedJump) {
830 | return groupBookmarkList[i];
831 | }
832 | }
833 | }
834 | vscode.window.showWarningMessage("All bookmarks are broken, time for some cleanup");
835 | }
836 |
837 | return firstCandidate;
838 | }
839 |
840 | public actionExpandSelectionToNextBookmark(editor: TextEditor) {
841 | let bookmarks = this.getTempDocumentBookmarkList(editor.document.uri.fsPath);
842 | if (typeof bookmarks === "undefined") {
843 | return;
844 | }
845 |
846 | let selection = editor.selection;
847 |
848 | let endLineRange = editor.document.lineAt(selection.end.line).range;
849 | let selectionEndsAtLineEnd = selection.end.character >= endLineRange.end.character;
850 |
851 | let searchFromLine = selection.end.line;
852 | if (selectionEndsAtLineEnd) {
853 | searchFromLine++;
854 | }
855 |
856 | let nextBookmark = bookmarks.find(
857 | bookmark => {
858 | return bookmark.group === this.activeGroup && bookmark.lineNumber >= searchFromLine;
859 | }
860 | );
861 |
862 | if (typeof nextBookmark === "undefined") {
863 | return;
864 | }
865 |
866 | let newSelectionEndCharacter: number;
867 | if (nextBookmark.lineNumber === selection.end.line) {
868 | newSelectionEndCharacter = endLineRange.end.character;
869 | } else {
870 | newSelectionEndCharacter = 0;
871 | }
872 |
873 | editor.selection = new Selection(
874 | selection.start.line,
875 | selection.start.character,
876 | nextBookmark.lineNumber,
877 | newSelectionEndCharacter
878 | );
879 |
880 | editor.revealRange(new Range(
881 | nextBookmark.lineNumber,
882 | newSelectionEndCharacter,
883 | nextBookmark.lineNumber,
884 | newSelectionEndCharacter
885 | ));
886 | }
887 |
888 | public actionExpandSelectionToPreviousBookmark(editor: TextEditor) {
889 | let bookmarks = this.getTempDocumentBookmarkList(editor.document.uri.fsPath);
890 | if (typeof bookmarks === "undefined") {
891 | return;
892 | }
893 |
894 | let selection = editor.selection;
895 |
896 | let startLineRange = editor.document.lineAt(selection.start.line).range;
897 | let selectionStartsAtLineStart = selection.start.character === 0;
898 |
899 | let searchFromLine = selection.start.line;
900 | if (selectionStartsAtLineStart) {
901 | searchFromLine--;
902 | }
903 |
904 | let nextBookmark: Bookmark | undefined;
905 | for (let i = bookmarks.length - 1; i >= 0; i--) {
906 | if (bookmarks[i].group === this.activeGroup && bookmarks[i].lineNumber <= searchFromLine) {
907 | nextBookmark = bookmarks[i];
908 | break;
909 | }
910 | }
911 |
912 | if (typeof nextBookmark === "undefined") {
913 | return;
914 | }
915 |
916 | let newSelectionStartCharacter: number;
917 | if (nextBookmark.lineNumber === selection.start.line) {
918 | newSelectionStartCharacter = 0;
919 | } else {
920 | newSelectionStartCharacter = editor.document.lineAt(nextBookmark.lineNumber).range.end.character;
921 | }
922 |
923 | editor.selection = new Selection(
924 | nextBookmark.lineNumber,
925 | newSelectionStartCharacter,
926 | selection.end.line,
927 | selection.end.character
928 | );
929 |
930 | editor.revealRange(new Range(
931 | nextBookmark.lineNumber,
932 | newSelectionStartCharacter,
933 | nextBookmark.lineNumber,
934 | newSelectionStartCharacter
935 | ));
936 | }
937 |
938 | public actionNavigateToBookmark() {
939 | this.navigateBookmarkList(
940 | "navigate to bookmark",
941 | this.getTempGroupBookmarkList(this.activeGroup),
942 | false
943 | );
944 | }
945 |
946 |
947 | public actionNavigateToBookmarkOfAnyGroup() {
948 | this.navigateBookmarkList(
949 | "navigate to bookmark of any bookmark group",
950 | this.bookmarks,
951 | true
952 | );
953 | }
954 |
955 | private navigateBookmarkList(placeholderText: string, bookmarks: Array, withGroupNames: boolean) {
956 | let currentEditor = vscode.window.activeTextEditor;
957 | let currentDocument: TextDocument;
958 | let currentSelection: Selection;
959 | if (typeof currentEditor !== "undefined") {
960 | currentSelection = currentEditor.selection;
961 | currentDocument = currentEditor.document;
962 | }
963 | let didNavigateBeforeClosing = false;
964 |
965 | let pickItems = bookmarks.map(
966 | bookmark => BookmarkPickItem.fromBookmark(bookmark, withGroupNames)
967 | );
968 |
969 | vscode.window.showQuickPick(
970 | pickItems,
971 | {
972 | canPickMany: false,
973 | matchOnDescription: true,
974 | placeHolder: placeholderText,
975 | ignoreFocusOut: true,
976 | onDidSelectItem: (selected: BookmarkPickItem) => {
977 | didNavigateBeforeClosing = true;
978 | this.jumpToBookmark(selected.bookmark, true);
979 | }
980 | }
981 | ).then(selected => {
982 | if (typeof selected !== "undefined") {
983 | this.jumpToBookmark(selected.bookmark);
984 | return;
985 | }
986 |
987 | if (!didNavigateBeforeClosing) {
988 | return;
989 | }
990 |
991 | if (
992 | typeof currentDocument === "undefined"
993 | || typeof currentSelection === "undefined"
994 | || currentDocument === null
995 | || currentSelection === null) {
996 | return;
997 | }
998 |
999 | vscode.window.showTextDocument(currentDocument, { preview: false }).then(
1000 | textEditor => {
1001 | try {
1002 | textEditor.selection = currentSelection;
1003 | textEditor.revealRange(new Range(currentSelection.start, currentSelection.end));
1004 | } catch (e) {
1005 | vscode.window.showWarningMessage("Failed to navigate to origin (1): " + e);
1006 | return;
1007 | }
1008 | },
1009 | rejectReason => {
1010 | vscode.window.showWarningMessage("Failed to navigate to origin (2): " + rejectReason.message);
1011 | }
1012 | );
1013 | });
1014 | }
1015 |
1016 | public actionSetGroupIconShape() {
1017 | let iconText = this.activeGroup.iconText;
1018 |
1019 | let shapePickItems = new Array();
1020 | for (let [label, id] of this.shapes) {
1021 | label = (this.activeGroup.shape === id ? "● " : "◌ ") + label;
1022 | shapePickItems.push(new ShapePickItem(id, iconText, label, "vector", ""));
1023 | }
1024 |
1025 | for (let [name, marker] of this.unicodeMarkers) {
1026 | let label = (this.activeGroup.shape === "unicode" && this.activeGroup.iconText === marker ? "● " : "◌ ");
1027 | label += marker + " " + name;
1028 | shapePickItems.push(new ShapePickItem("unicode", marker, label, "unicode", ""));
1029 | }
1030 |
1031 | vscode.window.showQuickPick(
1032 | shapePickItems,
1033 | {
1034 | canPickMany: false,
1035 | matchOnDescription: false,
1036 | placeHolder: "select bookmark group icon shape"
1037 | }
1038 | ).then(selected => {
1039 | if (typeof selected !== "undefined") {
1040 | let shape = (selected as ShapePickItem).shape;
1041 | let iconText = (selected as ShapePickItem).iconText;
1042 | this.activeGroup.setShapeAndIconText(shape, iconText);
1043 | this.saveState();
1044 | }
1045 | });
1046 | }
1047 |
1048 | public actionSetGroupIconColor() {
1049 | let colorPickItems = new Array();
1050 | for (let [name, color] of this.colors) {
1051 | let label = (this.activeGroup.color === color ? "● " : "◌ ") + name;
1052 |
1053 | colorPickItems.push(new ColorPickItem(color, label, "", ""));
1054 | }
1055 |
1056 | vscode.window.showQuickPick(
1057 | colorPickItems,
1058 | {
1059 | canPickMany: false,
1060 | matchOnDescription: false,
1061 | placeHolder: "select bookmark group icon color"
1062 | }
1063 | ).then(selected => {
1064 | if (typeof selected !== "undefined") {
1065 | let color = (selected as ColorPickItem).color;
1066 | this.activeGroup.setColor(color);
1067 | this.saveState();
1068 | }
1069 | });
1070 | }
1071 |
1072 | public actionSelectGroup() {
1073 | let pickItems = this.groups.map(
1074 | group => GroupPickItem.fromGroup(group, this.getTempGroupBookmarkList(group).length)
1075 | );
1076 |
1077 | vscode.window.showQuickPick(
1078 | pickItems,
1079 | {
1080 | canPickMany: false,
1081 | matchOnDescription: false,
1082 | placeHolder: "select bookmark group"
1083 | }
1084 | ).then(selected => {
1085 | if (typeof selected !== "undefined") {
1086 | this.setActiveGroup((selected as GroupPickItem).group.name);
1087 | }
1088 | });
1089 | }
1090 |
1091 | public setActiveGroup(groupName: string) {
1092 | this.activateGroup(groupName);
1093 | this.updateDecorations();
1094 | this.saveState();
1095 | }
1096 |
1097 | public actionAddGroup() {
1098 | vscode.window.showInputBox({
1099 | placeHolder: "group name",
1100 | prompt: "Enter group name to create or switch to"
1101 | }).then(groupName => {
1102 | if (typeof groupName === "undefined") {
1103 | return;
1104 | }
1105 |
1106 | groupName = groupName.trim();
1107 | if (groupName === "") {
1108 | return;
1109 | }
1110 |
1111 | if (groupName.length > this.maxGroupNameLength) {
1112 | vscode.window.showErrorMessage(
1113 | "Choose a maximum " +
1114 | this.maxGroupNameLength +
1115 | " character long group name."
1116 | );
1117 | return;
1118 | }
1119 |
1120 | this.activateGroup(groupName);
1121 | this.updateDecorations();
1122 | this.saveState();
1123 | this.treeViewRefreshCallback();
1124 | });
1125 | }
1126 |
1127 | public actionDeleteGroup() {
1128 | let pickItems = this.groups.map(
1129 | group => GroupPickItem.fromGroup(group, this.getTempGroupBookmarkList(group).length)
1130 | );
1131 |
1132 | vscode.window.showQuickPick(
1133 | pickItems,
1134 | {
1135 | canPickMany: true,
1136 | matchOnDescription: false,
1137 | placeHolder: "select bookmark groups to be deleted"
1138 | }
1139 | ).then(selecteds => {
1140 | if (typeof selecteds !== "undefined") {
1141 | this.deleteGroups(selecteds.map(pickItem => pickItem.group));
1142 | }
1143 | });
1144 | }
1145 |
1146 | public actionDeleteOneGroup(group: Group) {
1147 | this.deleteGroups([group]);
1148 | }
1149 |
1150 | private deleteGroups(groups: Array) {
1151 | let wasActiveGroupDeleted = false;
1152 |
1153 | for (let group of groups) {
1154 | wasActiveGroupDeleted ||= (group === this.activeGroup);
1155 |
1156 | this.getTempGroupBookmarkList(group).forEach(bookmark => {
1157 | this.deleteBookmark(bookmark);
1158 | });
1159 |
1160 | let index = this.groups.indexOf(group);
1161 | if (index >= 0) {
1162 | this.groups.splice(index, 1);
1163 | }
1164 |
1165 | group.removeDecorations();
1166 | this.tempGroupBookmarks.delete(group);
1167 | }
1168 |
1169 | if (this.groups.length === 0) {
1170 | this.activateGroup(this.defaultGroupName);
1171 | } else if (wasActiveGroupDeleted) {
1172 | this.activateGroup(this.groups[0].name);
1173 | }
1174 |
1175 | this.updateDecorations();
1176 | this.saveState();
1177 | this.treeViewRefreshCallback();
1178 | }
1179 |
1180 | public actionDeleteBookmark() {
1181 | let currentEditor = vscode.window.activeTextEditor;
1182 | let currentDocument: TextDocument;
1183 | let currentSelection: Selection;
1184 | if (typeof currentEditor !== "undefined") {
1185 | currentSelection = currentEditor.selection;
1186 | currentDocument = currentEditor.document;
1187 | }
1188 | let didNavigateBeforeClosing = false;
1189 |
1190 | let pickItems = this.getTempGroupBookmarkList(this.activeGroup).map(
1191 | bookmark => BookmarkPickItem.fromBookmark(bookmark, false)
1192 | );
1193 |
1194 | vscode.window.showQuickPick(
1195 | pickItems,
1196 | {
1197 | canPickMany: true,
1198 | matchOnDescription: false,
1199 | placeHolder: "select bookmarks to be deleted",
1200 | ignoreFocusOut: true,
1201 | onDidSelectItem: (selected: BookmarkPickItem) => {
1202 | didNavigateBeforeClosing = true;
1203 | this.jumpToBookmark(selected.bookmark, true);
1204 | }
1205 | }
1206 | ).then(selecteds => {
1207 | if (typeof selecteds !== "undefined") {
1208 | for (let selected of selecteds) {
1209 | this.deleteBookmark(selected.bookmark);
1210 | }
1211 |
1212 | this.updateDecorations();
1213 | this.saveState();
1214 | this.treeViewRefreshCallback();
1215 | }
1216 |
1217 | if (!didNavigateBeforeClosing) {
1218 | return;
1219 | }
1220 |
1221 | if (
1222 | typeof currentDocument === "undefined"
1223 | || typeof currentSelection === "undefined"
1224 | || currentDocument === null
1225 | || currentSelection === null) {
1226 | return;
1227 | }
1228 |
1229 | vscode.window.showTextDocument(currentDocument, { preview: false }).then(
1230 | textEditor => {
1231 | try {
1232 | textEditor.selection = currentSelection;
1233 | textEditor.revealRange(new Range(currentSelection.start, currentSelection.end));
1234 | } catch (e) {
1235 | vscode.window.showWarningMessage("Failed to navigate to origin (1): " + e);
1236 | return;
1237 | }
1238 | },
1239 | rejectReason => {
1240 | vscode.window.showWarningMessage("Failed to navigate to origin (2): " + rejectReason.message);
1241 | }
1242 | );
1243 | });
1244 | }
1245 |
1246 | public actionToggleHideAll() {
1247 | this.setHideAll(!this.hideAll);
1248 | this.updateDecorations();
1249 | this.saveState();
1250 | }
1251 |
1252 | public actionToggleHideInactiveGroups() {
1253 | this.setHideInactiveGroups(!this.hideInactiveGroups);
1254 | this.updateDecorations();
1255 | this.saveState();
1256 | }
1257 |
1258 | public actionClearFailedJumpFlags() {
1259 | let clearedFlagCount = 0;
1260 |
1261 | for (let bookmark of this.bookmarks) {
1262 | if (bookmark.failedJump) {
1263 | bookmark.failedJump = false;
1264 | clearedFlagCount++;
1265 | }
1266 | }
1267 |
1268 | vscode.window.showInformationMessage("Cleared broken bookmark flags: " + clearedFlagCount);
1269 | this.saveState();
1270 | }
1271 |
1272 | public actionMoveBookmarksFromActiveGroup() {
1273 | let pickItems = this.groups.filter(
1274 | g => g !== this.activeGroup
1275 | ).map(
1276 | group => GroupPickItem.fromGroup(group, this.getTempGroupBookmarkList(group).length)
1277 | );
1278 |
1279 | if (pickItems.length === 0) {
1280 | vscode.window.showWarningMessage("There is no other group to move bookmarks into");
1281 | return;
1282 | }
1283 |
1284 | vscode.window.showQuickPick(
1285 | pickItems,
1286 | {
1287 | canPickMany: false,
1288 | matchOnDescription: false,
1289 | placeHolder: "select destination group to move bookmarks into"
1290 | }
1291 | ).then(selected => {
1292 | if (typeof selected !== "undefined") {
1293 | this.moveBookmarksBetween(this.activeGroup, selected.group);
1294 | }
1295 | });
1296 | }
1297 |
1298 | private moveBookmarksBetween(src: Group, dst: Group) {
1299 | let pickItems = this.getTempGroupBookmarkList(src).map(
1300 | bookmark => BookmarkPickItem.fromBookmark(bookmark, false)
1301 | );
1302 |
1303 | vscode.window.showQuickPick(
1304 | pickItems,
1305 | {
1306 | canPickMany: true,
1307 | matchOnDescription: false,
1308 | placeHolder: "move bookmarks from " + src.name + " into " + dst.name,
1309 | ignoreFocusOut: true,
1310 | }
1311 | ).then(selecteds => {
1312 | if (typeof selecteds !== "undefined") {
1313 | for (let selected of selecteds) {
1314 | let oldBookmark = selected.bookmark;
1315 |
1316 | this.deleteBookmark(oldBookmark);
1317 |
1318 | let newBookmark = new Bookmark(
1319 | oldBookmark.fsPath,
1320 | oldBookmark.lineNumber,
1321 | oldBookmark.characterNumber,
1322 | oldBookmark.label,
1323 | oldBookmark.lineText,
1324 | dst,
1325 | this.decorationFactory
1326 | );
1327 |
1328 | this.addNewDecoratedBookmark(newBookmark);
1329 |
1330 | this.tempDocumentDecorations.delete(newBookmark.fsPath);
1331 | this.tempDocumentBookmarks.delete(newBookmark.fsPath);
1332 | this.tempGroupBookmarks.delete(newBookmark.group);
1333 | }
1334 |
1335 | this.bookmarks.sort(Bookmark.sortByLocation);
1336 |
1337 | this.saveState();
1338 | this.updateDecorations();
1339 | this.treeViewRefreshCallback();
1340 | }
1341 | });
1342 | }
1343 |
1344 | public readSettings() {
1345 | let defaultDefaultShape = "bookmark";
1346 |
1347 | let config = vscode.workspace.getConfiguration(this.configRoot);
1348 |
1349 | if (config.has(this.configKeyColors)) {
1350 | try {
1351 | let configColors = (config.get(this.configKeyColors) as Array>);
1352 | this.colors = new Map();
1353 | for (let [index, value] of configColors) {
1354 | this.colors.set(index, this.decorationFactory.normalizeColorFormat(value));
1355 | }
1356 | } catch (e) {
1357 | vscode.window.showWarningMessage("Error reading bookmark color setting");
1358 | }
1359 | }
1360 |
1361 | if (config.has(this.configKeyUnicodeMarkers)) {
1362 | try {
1363 | let configMarkers = (config.get(this.configKeyUnicodeMarkers) as Array>);
1364 | this.unicodeMarkers = new Map();
1365 | for (let [index, value] of configMarkers) {
1366 | this.unicodeMarkers.set(index, value);
1367 | }
1368 | } catch (e) {
1369 | vscode.window.showWarningMessage("Error reading bookmark unicode marker setting");
1370 | }
1371 | }
1372 |
1373 | if (config.has(this.configKeyDefaultShape)) {
1374 | let configDefaultShape = (config.get(this.configKeyDefaultShape) as string) ?? "";
1375 | if (this.shapes.has(configDefaultShape)) {
1376 | this.defaultShape = configDefaultShape;
1377 | } else {
1378 | vscode.window.showWarningMessage("Error reading bookmark default shape setting, using default");
1379 | this.defaultShape = defaultDefaultShape;
1380 | }
1381 | } else {
1382 | this.defaultShape = defaultDefaultShape;
1383 | }
1384 |
1385 | let configOverviewRulerLane = (config.get(this.configOverviewRulerLane) as string) ?? "center";
1386 | let previousOverviewRulerLane = this.decorationFactory.overviewRulerLane;
1387 | let newOverviewRulerLane: OverviewRulerLane | undefined;
1388 | switch (configOverviewRulerLane) {
1389 | case "center": newOverviewRulerLane = OverviewRulerLane.Center; break;
1390 | case "full": newOverviewRulerLane = OverviewRulerLane.Full; break;
1391 | case "left": newOverviewRulerLane = OverviewRulerLane.Left; break;
1392 | case "right": newOverviewRulerLane = OverviewRulerLane.Right; break;
1393 | default:
1394 | newOverviewRulerLane = undefined;
1395 | }
1396 |
1397 | let newLineEndLabelType = (config.get(this.configLineEndLabelType) as string) ?? "bordered";
1398 | let previousLineEndLabelType = this.decorationFactory.lineEndLabelType;
1399 |
1400 | if (
1401 | (typeof previousOverviewRulerLane === "undefined") !== (typeof newOverviewRulerLane === "undefined")
1402 | || previousOverviewRulerLane !== newOverviewRulerLane
1403 | || (typeof previousLineEndLabelType === "undefined") !== (typeof newLineEndLabelType === "undefined")
1404 | || previousLineEndLabelType !== newLineEndLabelType
1405 | ) {
1406 | this.decorationFactory.overviewRulerLane = newOverviewRulerLane;
1407 | this.decorationFactory.lineEndLabelType = newLineEndLabelType;
1408 | this.groups.forEach(group => group.redoDecorations());
1409 | this.bookmarks.forEach(bookmark => bookmark.initDecoration());
1410 | }
1411 | }
1412 |
1413 | public async onFilesRenamed(fileRenamedEvent: FileRenameEvent) {
1414 | let changedFiles = new Map();
1415 |
1416 | for (let rename of fileRenamedEvent.files) {
1417 | let stat = await vscode.workspace.fs.stat(rename.newUri);
1418 | let oldFsPath = rename.oldUri.fsPath;
1419 | let newFsPath = rename.newUri.fsPath;
1420 |
1421 | if ((stat.type & vscode.FileType.Directory) > 0) {
1422 | for (let bookmark of this.bookmarks) {
1423 | if (bookmark.fsPath.startsWith(oldFsPath)) {
1424 | let originalBookmarkFsPath = bookmark.fsPath;
1425 | bookmark.fsPath = newFsPath + bookmark.fsPath.substring(oldFsPath.length);
1426 | changedFiles.set(originalBookmarkFsPath, true);
1427 | changedFiles.set(bookmark.fsPath, true);
1428 | }
1429 | }
1430 | } else {
1431 | for (let bookmark of this.bookmarks) {
1432 | if (bookmark.fsPath === oldFsPath) {
1433 | bookmark.fsPath = newFsPath;
1434 | changedFiles.set(oldFsPath, true);
1435 | changedFiles.set(newFsPath, true);
1436 | }
1437 | }
1438 | }
1439 | }
1440 |
1441 | for (let [changedFile, b] of changedFiles) {
1442 | this.tempDocumentBookmarks.delete(changedFile);
1443 | this.tempDocumentDecorations.delete(changedFile);
1444 | }
1445 |
1446 | if (changedFiles.size > 0) {
1447 | this.saveState();
1448 | this.updateDecorations();
1449 | this.treeViewRefreshCallback();
1450 | }
1451 | }
1452 |
1453 | public async onFilesDeleted(fileDeleteEvent: FileDeleteEvent) {
1454 | for (let uri of fileDeleteEvent.files) {
1455 | let deletedFsPath = uri.fsPath;
1456 |
1457 | let changesWereMade = false;
1458 | for (let bookmark of this.bookmarks) {
1459 | if (bookmark.fsPath === deletedFsPath) {
1460 | this.deleteBookmark(bookmark);
1461 | changesWereMade = true;
1462 | }
1463 | }
1464 |
1465 | if (changesWereMade) {
1466 | this.saveState();
1467 | this.updateDecorations();
1468 | this.treeViewRefreshCallback();
1469 | }
1470 | }
1471 | }
1472 |
1473 | private updateStatusBar() {
1474 | this.statusBarItem.text = "$(bookmark) "
1475 | + this.activeGroup.name
1476 | + ": "
1477 | + this.getTempGroupBookmarkList(this.activeGroup).length;
1478 |
1479 | let hideStatus = "";
1480 | if (this.hideAll) {
1481 | hideStatus = ", all hidden";
1482 | } else if (this.hideInactiveGroups) {
1483 | hideStatus = ", inactive groups hidden";
1484 | } else {
1485 | hideStatus = ", all visible";
1486 | }
1487 | this.statusBarItem.tooltip = this.groups.length + " group(s)" + hideStatus;
1488 | }
1489 |
1490 | private restoreSavedState() {
1491 | this.hideInactiveGroups = this.ctx.workspaceState.get(this.savedHideInactiveGroupsKey) ?? false;
1492 |
1493 | this.hideAll = this.ctx.workspaceState.get(this.savedHideAllKey) ?? false;
1494 |
1495 | let activeGroupName: string = this.ctx.workspaceState.get(this.savedActiveGroupKey) ?? this.defaultGroupName;
1496 |
1497 | let serializedGroups: Array | undefined = this.ctx.workspaceState.get(this.savedGroupsKey);
1498 | this.groups = new Array();
1499 | if (typeof serializedGroups !== "undefined") {
1500 | try {
1501 | for (let sg of serializedGroups) {
1502 | this.addNewGroup(Group.fromSerializableGroup(sg, this.decorationFactory));
1503 | }
1504 |
1505 | this.groups.sort(Group.sortByName);
1506 | } catch (e) {
1507 | vscode.window.showErrorMessage("Restoring bookmark groups failed (" + e + ")");
1508 | }
1509 | }
1510 |
1511 | let serializedBookmarks: Array | undefined
1512 | = this.ctx.workspaceState.get(this.savedBookmarksKey);
1513 | this.bookmarks = new Array();
1514 | if (typeof serializedBookmarks !== "undefined") {
1515 | try {
1516 | for (let sb of serializedBookmarks) {
1517 | let bookmark = Bookmark.fromSerializableBookMark(sb, this.getGroupByName.bind(this), this.decorationFactory);
1518 | this.addNewDecoratedBookmark(bookmark);
1519 | }
1520 |
1521 | this.bookmarks.sort(Bookmark.sortByLocation);
1522 | } catch (e) {
1523 | vscode.window.showErrorMessage("Restoring bookmarks failed (" + e + ")");
1524 | }
1525 | }
1526 |
1527 | this.resetTempLists();
1528 | this.activateGroup(activeGroupName);
1529 | }
1530 |
1531 | private addNewGroup(group: Group) {
1532 | group.onGroupDecorationUpdated(this.handleGroupDecorationUpdated.bind(this));
1533 | group.onGroupDecorationSwitched(this.handleGroupDecorationSwitched.bind(this));
1534 | group.onDecorationRemoved(this.handleDecorationRemoved.bind(this));
1535 | group.initDecorations();
1536 | this.groups.push(group);
1537 | }
1538 |
1539 | private addNewDecoratedBookmark(bookmark: Bookmark) {
1540 | bookmark.onBookmarkDecorationUpdated(this.handleBookmarkDecorationUpdated.bind(this));
1541 | bookmark.onDecorationRemoved(this.handleDecorationRemoved.bind(this));
1542 | bookmark.initDecoration();
1543 | this.bookmarks.push(bookmark);
1544 | }
1545 |
1546 | private activateGroup(name: string) {
1547 | let newActiveGroup = this.ensureGroup(name);
1548 | if (newActiveGroup === this.activeGroup) {
1549 | return;
1550 | }
1551 |
1552 | this.activeGroup.setIsActive(false);
1553 | this.activeGroup = newActiveGroup;
1554 | newActiveGroup.setIsActive(true);
1555 |
1556 | this.setGroupVisibilities();
1557 | this.tempDocumentDecorations.clear();
1558 | }
1559 |
1560 | private setGroupVisibilities() {
1561 | this.groups.forEach(group => {
1562 | group.setIsVisible(!this.hideAll && (!this.hideInactiveGroups || group.isActive));
1563 | });
1564 | }
1565 |
1566 | private ensureGroup(name: string): Group {
1567 | let group = this.groups.find(
1568 | (group) => {
1569 | return group.name === name;
1570 | });
1571 |
1572 | if (typeof group !== "undefined") {
1573 | return group;
1574 | }
1575 |
1576 | group = new Group(name, this.getLeastUsedColor(), this.defaultShape, name, this.decorationFactory);
1577 | this.addNewGroup(group);
1578 | this.groups.sort(Group.sortByName);
1579 |
1580 | return group;
1581 | }
1582 |
1583 | private getLeastUsedColor(): string {
1584 | if (this.colors.size < 1) {
1585 | return this.fallbackColor;
1586 | }
1587 |
1588 | let usages = new Map();
1589 |
1590 | for (let [index, color] of this.colors) {
1591 | usages.set(color, 0);
1592 | }
1593 |
1594 | for (let group of this.groups) {
1595 | let groupColor = group.getColor();
1596 | if (usages.has(groupColor)) {
1597 | usages.set(groupColor, (usages.get(groupColor) ?? 0) + 1);
1598 | }
1599 | }
1600 |
1601 | let minUsage = Number.MAX_SAFE_INTEGER;
1602 | let leastUsedColor = "";
1603 |
1604 | for (let [key, value] of usages) {
1605 | if (minUsage > value) {
1606 | minUsage = value;
1607 | leastUsedColor = key;
1608 | }
1609 | }
1610 |
1611 | return leastUsedColor;
1612 | }
1613 |
1614 | private setHideInactiveGroups(hideInactiveGroups: boolean) {
1615 | if (this.hideInactiveGroups === hideInactiveGroups) {
1616 | return;
1617 | }
1618 |
1619 | this.hideInactiveGroups = hideInactiveGroups;
1620 |
1621 | this.setGroupVisibilities();
1622 |
1623 | this.tempDocumentDecorations.clear();
1624 | }
1625 |
1626 | private setHideAll(hideAll: boolean) {
1627 | if (this.hideAll === hideAll) {
1628 | return;
1629 | }
1630 |
1631 | this.hideAll = hideAll;
1632 |
1633 | this.setGroupVisibilities();
1634 |
1635 | this.tempDocumentDecorations.clear();
1636 | }
1637 |
1638 | private getNlCount(text: string) {
1639 | let nlCount: number = 0;
1640 | for (let c of text) {
1641 | nlCount += (c === "\n") ? 1 : 0;
1642 | }
1643 | return nlCount;
1644 | }
1645 |
1646 | private getFirstLine(text: string): string {
1647 | let firstNewLinePos = text.indexOf("\n");
1648 | if (firstNewLinePos < 0) {
1649 | return text;
1650 | }
1651 |
1652 | return text.substring(0, firstNewLinePos + 1);
1653 | }
1654 |
1655 | private getLastLine(text: string): string {
1656 | let lastNewLinePos = text.lastIndexOf("\n");
1657 | if (lastNewLinePos < 0) {
1658 | return text;
1659 | }
1660 |
1661 | return text.substring(lastNewLinePos + 1);
1662 | }
1663 |
1664 | public jumpToBookmark(bookmark: Bookmark, preview: boolean = false) {
1665 | vscode.window.showTextDocument(vscode.Uri.file(bookmark.fsPath), { preview: preview, preserveFocus: preview }).then(
1666 | textEditor => {
1667 | try {
1668 | let range = new Range(
1669 | bookmark.lineNumber,
1670 | bookmark.characterNumber,
1671 | bookmark.lineNumber,
1672 | bookmark.characterNumber
1673 | );
1674 | textEditor.selection = new vscode.Selection(range.start, range.start);
1675 | textEditor.revealRange(range);
1676 | } catch (e) {
1677 | bookmark.failedJump = true;
1678 | vscode.window.showWarningMessage("Failed to navigate to bookmark (3): " + e);
1679 | return;
1680 | }
1681 | bookmark.failedJump = false;
1682 | },
1683 | rejectReason => {
1684 | bookmark.failedJump = true;
1685 | vscode.window.showWarningMessage("Failed to navigate to bookmark (2): " + rejectReason.message);
1686 | }
1687 | );
1688 | }
1689 |
1690 | public getNearestActiveBookmarkInFile(textEditor: TextEditor, group: Group | null): Bookmark | null {
1691 | if (textEditor.selections.length === 0) {
1692 | return null;
1693 | }
1694 |
1695 | let fsPath = textEditor.document.uri.fsPath;
1696 | let lineNumber = textEditor.selection.start.line;
1697 |
1698 | let nearestBeforeLine = -1;
1699 | let nearestBefore: Bookmark | null = null;
1700 | let nearestAfterline = Number.MAX_SAFE_INTEGER;
1701 | let nearestAfter: Bookmark | null = null;
1702 |
1703 | this.getTempDocumentBookmarkList(fsPath)
1704 | .filter(g => (group === null || g.group === group))
1705 | .forEach(bookmark => {
1706 | if (bookmark.lineNumber > nearestBeforeLine && bookmark.lineNumber <= lineNumber) {
1707 | nearestBeforeLine = bookmark.lineNumber;
1708 | nearestBefore = bookmark;
1709 | }
1710 |
1711 | if (bookmark.lineNumber < nearestAfterline && bookmark.lineNumber >= lineNumber) {
1712 | nearestAfterline = bookmark.lineNumber;
1713 | nearestAfter = bookmark;
1714 | }
1715 | });
1716 |
1717 | if (nearestBefore === null && nearestAfter === null) {
1718 | return null;
1719 | }
1720 |
1721 | if (nearestBefore !== null && nearestAfter !== null) {
1722 | if (lineNumber - nearestBeforeLine < nearestAfterline - lineNumber) {
1723 | return nearestBefore;
1724 | }
1725 |
1726 | return nearestAfter;
1727 | }
1728 |
1729 | if (nearestBefore !== null) {
1730 | return nearestBefore;
1731 | }
1732 |
1733 | return nearestAfter;
1734 | }
1735 | }
--------------------------------------------------------------------------------