From 81f91b5c2f3af5158ea214b379e727825e1fdfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leandro=20Sim=C3=B5es?= Date: Wed, 1 Nov 2023 12:36:36 +0100 Subject: [PATCH] feat(): add pagination to the URLs browser --- .eslintrc | 1 + .vscodeignore | 10 +- CHANGELOG.md | 9 + LICENSE.txt | 21 ++ README.md | 2 +- docs/browser-icon-black.svg | 2 - docs/browser-icon-white.svg | 2 - docs/file-icon-black.svg | 4 - docs/file-icon-white.svg | 4 - package.json | 34 +-- src/assets/css/style.css | 68 ++++- src/assets/index.html | 26 +- src/assets/js/script.js | 385 ++++++++++++++++---------- src/extension.ts | 5 +- src/services/treeview/models/index.ts | 42 ++- src/services/urls/index.ts | 136 +++++++-- src/services/urls/interfaces/index.ts | 4 + src/services/urls/models/index.ts | 7 +- src/services/webview/enums/index.ts | 2 + src/services/webview/index.ts | 268 +++++++++--------- 20 files changed, 677 insertions(+), 355 deletions(-) create mode 100644 LICENSE.txt delete mode 100644 docs/browser-icon-black.svg delete mode 100644 docs/browser-icon-white.svg delete mode 100644 docs/file-icon-black.svg delete mode 100644 docs/file-icon-white.svg diff --git a/.eslintrc b/.eslintrc index 767764b..1fc9ee3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,6 +27,7 @@ "max-classes-per-file": 0, "class-methods-use-this": 0, "no-underscore-dangle": 0, + "@typescript-eslint/no-this-alias": 0, "@typescript-eslint/ban-ts-ignore": 0, "@typescript-eslint/interface-name-prefix": 0, "@typescript-eslint/explicit-module-boundary-types": 0, diff --git a/.vscodeignore b/.vscodeignore index 29c7b05..1d1552c 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,9 +1,9 @@ -.vscode/** -.vscode-test/** -out/test/** -src/** +.github/ +.vscode/ +.vscode-test/ +src/ +node_modules/ .gitignore -vsc-extension-quickstart.md **/tsconfig.json **/.eslintrc.json **/*.map diff --git a/CHANGELOG.md b/CHANGELOG.md index b2efd05..0871309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - You can see more about what's going on [here](https://github.com/leandrosimoes/project-urls-manager-vscode-extension/issues), even open a new issue with your sugestions/errors. +## [1.3.0] +- Now the URLs browser has pagination that shows only 6 URLs per page. You can change the page by clicking on the pagination buttons at the bottom of the page. + +## [1.2.6] +- Fixed problem where no URLs were found when some ignored pattern URLs exists. + +## [1.2.5] +- Now the tree view shows two icon buttons, one to open the URL in the browser, and another to open the URL source file with the cursor exactly where the URL was found. + ## [1.2.4] - Docs improvements diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7a01131 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Leandro Simões + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 55be956..fd4a182 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This is what the Project URLs Manager looks like when open: * **URL Address**: Click at the URL address to open on your browser * **URL Description**: Add a quick description of the URL * **Status Bar/Open Button**: See how many URLs were found on your project and click to open the manager window -* **Treview**: Pannels that shows all URLs separated by status. You can double-click them to open on the browser. +* **Treview**: Pannels that shows all URLs separated by status. Each item has two buttons, one to open the URL in the browser, and another to open the file source of the URL with the cursor positioned exactly where the URL was found. ## Extension Settings diff --git a/docs/browser-icon-black.svg b/docs/browser-icon-black.svg deleted file mode 100644 index 3d10439..0000000 --- a/docs/browser-icon-black.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/docs/browser-icon-white.svg b/docs/browser-icon-white.svg deleted file mode 100644 index fc7d045..0000000 --- a/docs/browser-icon-white.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/docs/file-icon-black.svg b/docs/file-icon-black.svg deleted file mode 100644 index b03e8e6..0000000 --- a/docs/file-icon-black.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/docs/file-icon-white.svg b/docs/file-icon-white.svg deleted file mode 100644 index 72eb23a..0000000 --- a/docs/file-icon-white.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/package.json b/package.json index 4c9d668..33634c2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,18 @@ "name": "project-urls-manager", "displayName": "Project URLs Manager", "description": "Manage all urls of your project in one place", - "version": "1.2.6", + "version": "1.3.0", + "scripts": { + "dev": "npm-run-all clean compile copy-assets watch", + "prod": "npm-run-all clean lint:fix compile copy-assets", + "vscode:prepublish": "npm run prod", + "compile": "tsc --project tsconfig.json", + "lint": "tsc --project tsconfig.json --noEmit && eslint src --ext ts", + "lint:fix": "tsc --project tsconfig.json --noEmit && eslint src --ext ts --fix", + "watch": "tsc --project tsconfig.json --watch", + "copy-assets": "ts-node copy-assets.ts", + "clean": "rimraf out/*" + }, "icon": "docs/icon-256.png", "publisher": "Leandro", "galleryBanner": { @@ -55,19 +66,13 @@ { "command": "urlList.openInBrowser", "title": "See in browswer", - "icon": { - "light": "docs/browser-icon-black.svg", - "dark": "docs/browser-icon-white.svg" - }, + "icon": "$(browser)", "when": "view == starredList || view == normalList || view == ignoredList" }, { "command": "urlList.openURLFile", "title": "See in file", - "icon": { - "light": "docs/file-icon-black.svg", - "dark": "docs/file-icon-white.svg" - }, + "icon": "$(file-symlink-file)", "when": "view == starredList || view == normalList || view == ignoredList" } ], @@ -125,17 +130,6 @@ ] } }, - "scripts": { - "dev": "npm-run-all clean compile copy-assets watch", - "prod": "npm-run-all clean lint:fix compile copy-assets", - "vscode:prepublish": "npm run prod", - "compile": "tsc --project tsconfig.json", - "lint": "tsc --project tsconfig.json --noEmit && eslint src --ext ts", - "lint:fix": "tsc --project tsconfig.json --noEmit && eslint src --ext ts --fix", - "watch": "tsc --project tsconfig.json --watch", - "copy-assets": "ts-node copy-assets.ts", - "clean": "rimraf out/*" - }, "devDependencies": { "@types/glob": "^7.2.0", "@types/node": "^18.0.0", diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 056459e..9503e00 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -20,6 +20,10 @@ body { } } +i.rotate-horizontal { + transform: rotate(180deg); +} + i.spinning { animation: pam-spin .5s linear infinite; } @@ -34,8 +38,7 @@ a { text-decoration: none; } -a#help-button, -a#github-button { +a#help-button { margin: 10px 10px 10px 0; } @@ -117,7 +120,7 @@ main { z-index: 9999; } -html:not(.show-ignored) .toggle-ignore-control .toggle { +.toggle-ignore-control .toggle:not(.show-ignored) { right: 5px; } @@ -226,6 +229,65 @@ html:not(.show-ignored) .toggle-ignore-control .toggle { cursor: pointer; } +#pagination { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-top: 20px; +} + +#pagination i { + font-size: 20px; + margin: 0 5px; + cursor: pointer; + opacity: 0.9; +} + +#pagination i:hover { + opacity: 1; +} + +#pagination div.pages { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +#pagination div.pages div { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + margin: 0 5px; + color: var(--vscode-editor-foreground); + background-color: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-text-input-border); + cursor: pointer; + opacity: 0.9; +} + +#pagination div.pages div:hover { + opacity: 1; + text-decoration: underline; +} + +#pagination div.pages div.active { + background-color: var(--vscode-editor-foreground); + color: var(--vscode-editor-background); + opacity: 1; +} + +footer { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + /* SCROLL */ ::-webkit-scrollbar { width: 2px; diff --git a/src/assets/index.html b/src/assets/index.html index 6a7bbd8..83cbd4a 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -1,5 +1,5 @@ - + @@ -15,14 +15,15 @@

 {{TITLE}}

-
- - +
+ +
@@ -69,9 +70,20 @@

 {{TITLE}}

- Made with by Leandro Simões + {{SCRIPTS}} + \ No newline at end of file diff --git a/src/assets/js/script.js b/src/assets/js/script.js index fc1fd9d..f7ce719 100644 --- a/src/assets/js/script.js +++ b/src/assets/js/script.js @@ -1,209 +1,314 @@ -document.addEventListener('readystatechange', () => { - const ActionTypes = { - URL: 'URL', - URL_ICON: 'URL_ICON', - ICON: 'ICON', - COPY: 'COPY', - IGNORE: 'IGNORE', - RESTORE: 'RESTORE', - START_LOADING: 'START_LOADING', - STOP_LOADING: 'STOP_LOADING', - SAVE_URL_DESCRIPTION: 'SAVE_URL_DESCRIPTION', - TOGGLE_SHOW_IGNORED: 'TOGGLE_SHOW_IGNORED', - STAR: 'STAR', - UNSTAR: 'UNSTAR', - } - - function pamViewModel() { - const vscode = acquireVsCodeApi() - - // observables - this.isLoading = ko.observable(true) - this.urls = ko.observable([]) - this.searchText = ko.observable('') - this.delayChangeSearchInputCallbackOptions = { - delay: 1000, // miliseconds - callback: (value) => { - window.pam.model.urls().forEach((url) => { - url.show(!value || url.href.indexOf(value) > -1) - }) - }, - } - this.delayChangeURLDescriptionInputCallbackOptions = { - delay: 1000, // miliseconds - callback: (value, data) => { - if (!data) { - return - } - - this.saveURLDescription(data) - }, - } +const ActionTypes = { + URL: 'URL', + URL_ICON: 'URL_ICON', + ICON: 'ICON', + COPY: 'COPY', + IGNORE: 'IGNORE', + RESTORE: 'RESTORE', + START_LOADING: 'START_LOADING', + STOP_LOADING: 'STOP_LOADING', + SAVE_URL_DESCRIPTION: 'SAVE_URL_DESCRIPTION', + TOGGLE_SHOW_IGNORED: 'TOGGLE_SHOW_IGNORED', + STAR: 'STAR', + UNSTAR: 'UNSTAR', + CHANGE_PAGE: 'CHANGE_PAGE', + SEARCH: 'SEARCH', +} - // subscribers - this.urls.subscribe(() => { - setTimeout(() => { - const tareas = document.querySelectorAll('.url-description-input') - if (tareas && tareas.length > 0) { - tareas.forEach((tarea) => - this.onChangeTextarea(null, { - currentTarget: tarea, - }) - ) - } - }, 100) - }) - - // computeds - this.ShowNoURLsMessage = ko.computed(() => { - return this.urls().filter((url) => url.show()).length === 0 && !this.isLoading() - }) - this.ShowURLsList = ko.computed(() => { - return this.urls().filter((url) => url.show()).length > 0 && !this.isLoading() - }) - - // functions - this.saveURLDescription = (url) => { - if (!url) { - return - } - - vscode.postMessage({ - type: ActionTypes.SAVE_URL_DESCRIPTION, - url: ko.mapping.toJS(url), - }) - } - this.copyToClipboard = (url) => { - if (!url) { - return - } +function viewModel() { + const vscode = acquireVsCodeApi() + // observables + this.isShowingIgnored = ko.observable( + document.querySelector('.toggle').classList.contains('show-ignored') + ) + this.currentPage = ko.observable(1) + this.totalPages = ko.observable(1) + this.isLoading = ko.observable(true) + this.urls = ko.observable([]) + this.pages = ko.observable([]) + this.searchText = ko.observable( + document.querySelector('#search-input').value || '' + ) + this.delayChangeSearchInputCallbackOptions = { + delay: 1000, // miliseconds + callback: (value) => { vscode.postMessage({ - type: ActionTypes.COPY, - url: ko.mapping.toJS(url), + type: ActionTypes.SEARCH, + isShowingIgnored: this.isShowingIgnored(), + currentPage: 1, + searchText: value, }) - } - this.ignore = (url) => { - if (!url) { + }, + } + this.delayChangeURLDescriptionInputCallbackOptions = { + delay: 1000, // miliseconds + callback: (_, data) => { + if (!data) { return } - vscode.postMessage({ - type: ActionTypes.IGNORE, - url: ko.mapping.toJS(url), - }) - } - this.restore = (url) => { - if (!url) { - return - } + this.saveURLDescription(data) + }, + } - vscode.postMessage({ - type: ActionTypes.RESTORE, - url: ko.mapping.toJS(url), - }) - } - this.star = (url) => { - if (!url) { - return + // subscribers + this.urls.subscribe(() => { + setTimeout(() => { + const textareas = document.querySelectorAll( + '.url-description-input' + ) + if (textareas && textareas.length > 0) { + textareas.forEach((tarea) => + this.onChangeTextarea(null, { + currentTarget: tarea, + }) + ) } + }, 100) + }) + this.debounceTimeout = null + this.currentPage.subscribe(() => { + clearTimeout(this.debounceTimeout) - vscode.postMessage({ - type: ActionTypes.STAR, - url: ko.mapping.toJS(url), - }) - } - this.unstar = (url) => { - if (!url) { - return - } + this.debounceTimeout = setTimeout(() => { + this.changePage() + }, 500) + }) + this.isShowingIgnored.subscribe((isShowingIgnored) => { + clearTimeout(this.debounceTimeout) - vscode.postMessage({ - type: ActionTypes.UNSTAR, - url: ko.mapping.toJS(url), - }) - } - this.toggleShowIgnore = () => { + this.debounceTimeout = setTimeout(() => { vscode.postMessage({ type: ActionTypes.TOGGLE_SHOW_IGNORED, + isShowingIgnored, + currentPage: 1, + searchText: this.searchText(), }) - } - this.onChangeTextarea = (data, event) => { - const tarea = event.currentTarget - const diference = (tarea.offsetHeight - tarea.scrollHeight) * -1 - - if (diference > 0) { - tarea.style.height = `${tarea.offsetHeight + diference}px` - tarea.style.minHeight = `${tarea.offsetHeight + diference}px` - tarea.style.maxHeight = `${tarea.offsetHeight + diference}px` - } + }, 500) + }) + + // computeds + this.ShowNoURLsMessage = ko.computed(() => { + return ( + this.urls().filter((url) => url.show()).length === 0 && + !this.isLoading() + ) + }) + this.ShowURLsList = ko.computed(() => { + return ( + this.urls().filter((url) => url.show()).length > 0 && + !this.isLoading() + ) + }) + this.ShowPagination = ko.computed(() => { + return this.pages().length > 1 && !this.isLoading() + }) + + // functions + this.changePage = () => { + vscode.postMessage({ + type: ActionTypes.CHANGE_PAGE, + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.nextPage = () => { + if (this.currentPage() >= this.totalPages()) return + + this.currentPage(this.currentPage() + 1) + } + this.firstPage = () => { + this.currentPage(1) + } + this.lastPage = () => { + this.currentPage(this.totalPages()) + } + this.nextPage = () => { + if (this.currentPage() >= this.totalPages()) return + + this.currentPage(this.currentPage() + 1) + } + this.selectPage = (page) => { + if (!page) return + + this.currentPage(page) + } + this.prevPage = () => { + if (this.currentPage() <= 1) return + + this.currentPage(this.currentPage() - 1) + } + this.saveURLDescription = (url) => { + if (!url) return + + vscode.postMessage({ + type: ActionTypes.SAVE_URL_DESCRIPTION, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.copyToClipboard = (url) => { + if (!url) return - return true + vscode.postMessage({ + type: ActionTypes.COPY, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.ignore = (url) => { + if (!url) return + + vscode.postMessage({ + type: ActionTypes.IGNORE, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.restore = (url) => { + if (!url) return + + vscode.postMessage({ + type: ActionTypes.RESTORE, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.star = (url) => { + if (!url) return + + vscode.postMessage({ + type: ActionTypes.STAR, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.unstar = (url) => { + if (!url) return + + vscode.postMessage({ + type: ActionTypes.UNSTAR, + url: ko.mapping.toJS(url), + isShowingIgnored: this.isShowingIgnored(), + currentPage: this.currentPage(), + searchText: this.searchText(), + }) + } + this.toggleShowIgnore = (isShowingIgnored) => { + this.isShowingIgnored(isShowingIgnored) + } + this.onChangeTextarea = (data, event) => { + const tarea = event.currentTarget + const diference = (tarea.offsetHeight - tarea.scrollHeight) * -1 + + if (diference > 0) { + tarea.style.height = `${tarea.offsetHeight + diference}px` + tarea.style.minHeight = `${tarea.offsetHeight + diference}px` + tarea.style.maxHeight = `${tarea.offsetHeight + diference}px` } + + return true } +} +document.addEventListener('readystatechange', () => { if (document.readyState === 'complete') { let DELAY_CHANGE_INPUT_TIMEOUT ko.bindingHandlers.delayChangeInputCallback = { - init: (element, valueAccessor, allBindings, viewModel, bindingContext) => { + init: ( + element, + valueAccessor, + allBindings, + viewModel, + bindingContext + ) => { const { delay = 1000, callback } = valueAccessor() element.addEventListener('keyup', (event) => { clearTimeout(DELAY_CHANGE_INPUT_TIMEOUT) DELAY_CHANGE_INPUT_TIMEOUT = setTimeout(() => { - if (callback) callback(event.target.value || '', bindingContext.$data) + if (callback) + callback( + event.target.value || '', + bindingContext.$data + ) }, delay) }) }, } - window.pam = { - model: new pamViewModel(), + window.view_model = { + model: new viewModel(), } - ko.applyBindings(window.pam.model) + ko.applyBindings(window.view_model.model) window.addEventListener('message', (event) => { - const { type, urls } = event.data + const { type, urls = [], pages = [], totalPages = 1 } = event.data let lastDomain = '' switch (type) { case ActionTypes.START_LOADING: - window.pam.model.isLoading(true) + window.view_model.model.isLoading(true) break case ActionTypes.STOP_LOADING: - window.pam.model.isLoading(false) + window.view_model.model.isLoading(false) break case ActionTypes.URL: lastDomain = '' - window.pam.model.urls( + window.view_model.model.urls( urls.map((url) => { url.hasFavicon = ko.observable(url.hasFavicon) url.favicon = ko.observable(url.favicon) url.show = ko.observable(true) - url.showDomain = ko.observable(lastDomain !== url.host) + url.showDomain = ko.observable( + lastDomain !== url.host + ) url.description = ko.observable(url.description) url.isStarred = !!url.isStarred lastDomain = url.host return url }) ) + window.view_model.model.pages(pages) + window.view_model.model.totalPages(totalPages) + + if (pages.length === 1) { + window.view_model.model.currentPage(1) + } break case ActionTypes.URL_ICON: - window.pam.model.urls().forEach((url) => { - const foundURL = urls.find((u) => u.baseURL === url.baseURL) + window.view_model.model.urls().forEach((url) => { + const foundURL = urls.find( + (u) => u.baseURL === url.baseURL + ) - if (foundURL && foundURL.hasFavicon && foundURL.favicon !== url.favicon()) { + if ( + foundURL && + foundURL.hasFavicon && + foundURL.favicon !== url.favicon() + ) { url.favicon(foundURL.favicon) url.hasFavicon(true) } }) - break default: diff --git a/src/extension.ts b/src/extension.ts index 1c951e5..59aee1d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,7 +9,10 @@ import { getInstance, openWebview } from './services/webview' import { setupTreeViews } from './services/treeview' export function activate(context: vscode.ExtensionContext) { - if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + if ( + !vscode.workspace.workspaceFolders || + vscode.workspace.workspaceFolders.length === 0 + ) { return } diff --git a/src/services/treeview/models/index.ts b/src/services/treeview/models/index.ts index 9d551ce..44e4b87 100644 --- a/src/services/treeview/models/index.ts +++ b/src/services/treeview/models/index.ts @@ -16,14 +16,13 @@ export class ProjectURLsTreeItem extends vscode.TreeItem { super(label, collapsibleState) } - iconPath = - !this.contextValueId - ? new vscode.ThemeIcon('globe') - : new vscode.ThemeIcon('link') + iconPath = !this.contextValueId + ? new vscode.ThemeIcon('globe') + : new vscode.ThemeIcon('link') contextValue = this.contextValueId - tooltip = this.sourceFilePath + tooltip = this.sourceFilePath ? `${this.sourceFilePath}:${this.sourceFileLineNumber}:${this.sourceFileColumnNumber}` : this.label } @@ -66,20 +65,40 @@ export class ProjectURLsTreeViewDataProvider ].filter((h) => !!h) return hosts.map( - (h) => new ProjectURLsTreeItem(h, vscode.TreeItemCollapsibleState.Collapsed, '', -1, -1, undefined) + (h) => + new ProjectURLsTreeItem( + h, + vscode.TreeItemCollapsibleState.Collapsed, + '', + -1, + -1, + undefined + ) ) } return urls .filter((url) => url.host === host.replace(`${url.protocol}//`, '')) - .map((url) => new ProjectURLsTreeItem(url.href, vscode.TreeItemCollapsibleState.None, url.filePath, url.lineNumber, url.columnNumber, 'child')) + .map( + (url) => + new ProjectURLsTreeItem( + url.href, + vscode.TreeItemCollapsibleState.None, + url.filePath, + url.lineNumber, + url.columnNumber, + 'child' + ) + ) } getTreeItem(element: ProjectURLsTreeItem): ProjectURLsTreeItem { return element } - getChildren(element?: ProjectURLsTreeItem | undefined): Thenable { + getChildren( + element?: ProjectURLsTreeItem | undefined + ): Thenable { if (!element) { return Promise.resolve(this._mapURLItems(this._urls)) } @@ -100,7 +119,12 @@ export class ProjectURLsTreeView { async updateTreviewData(forceSync = false) { logger.log({ message: `Start updating ${this._type} TreeView ...` }) - const urls = await getURLs(forceSync, this._type === EProjectURLsTreeViewType.IGNORED) + const { urls } = await getURLs( + forceSync, + this._type === EProjectURLsTreeViewType.IGNORED, + -1, + '' + ) let treeDataProvider: ProjectURLsTreeViewDataProvider | undefined diff --git a/src/services/urls/index.ts b/src/services/urls/index.ts index 6deddca..a5eb994 100644 --- a/src/services/urls/index.ts +++ b/src/services/urls/index.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode' import { readdirSync, readFileSync, statSync } from 'fs' import { extname, join } from 'path' -import { IURL } from './interfaces' +import { IPage, IURL } from './interfaces' import URL from './models' import { logger } from '../logger' @@ -40,7 +40,10 @@ async function searchForWorkspaceURLs(rootPath = vscode.workspace.rootPath) { if (!stat.isDirectory()) { const fileExtension = extname(file) - if (extensionsList.length > 0 && extensionsList.indexOf(fileExtension) === -1) { + if ( + extensionsList.length > 0 && + extensionsList.indexOf(fileExtension) === -1 + ) { logger.log({ message: `File extension ignored: '${file}'` }) continue } @@ -53,14 +56,19 @@ async function searchForWorkspaceURLs(rootPath = vscode.workspace.rootPath) { const urlsFound = line.match( /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g ) - + if (urlsFound && urlsFound.length > 0) { for (const url of urlsFound) { const columnNumber = line.indexOf(url) const href = cleanURL(url) - const urlInstance = new URL(href, filePath, lineNumber, columnNumber).url + const urlInstance = new URL( + href, + filePath, + lineNumber, + columnNumber + ).url urlInstance.hasFavicon = false - + if ( urlInstance && urlInstance.host && @@ -69,10 +77,14 @@ async function searchForWorkspaceURLs(rootPath = vscode.workspace.rootPath) { URLS.push(urlInstance) } } - - logger.log({ message: `${urlsFound.length} URL(s) found in "${filePath}".` }) + + logger.log({ + message: `${urlsFound.length} URL(s) found in "${filePath}".`, + }) } else { - logger.log({ message: `No URL found in "${filePath}".` }) + logger.log({ + message: `No URL found in "${filePath}".`, + }) } } } else { @@ -82,7 +94,9 @@ async function searchForWorkspaceURLs(rootPath = vscode.workspace.rootPath) { return URLS } catch (error) { - logger.log({ message: `searchForWorkspaceURLs ERROR: ${error.message}` }) + logger.log({ + message: `searchForWorkspaceURLs ERROR: ${error.message}`, + }) return URLS } } @@ -92,13 +106,17 @@ export const syncURLs = async (showIgnored: boolean) => { const context = getContext() if (!context) { - logger.log({ message: `0 URL(s) found`, shouldSetStatusBarMessage: true }) + logger.log({ + message: `0 URL(s) found`, + shouldSetStatusBarMessage: true, + }) return } URLS = [] - let existentURLs: IURL[] = context.workspaceState.get('urls') || [] + let existentURLs: IURL[] = + context.workspaceState.get('urls') || [] logger.log({ message: 'Syncing Project URLs ...', @@ -110,7 +128,13 @@ export const syncURLs = async (showIgnored: boolean) => { // ADD URL TO THE existentURLs IF NOT ALREADY EXISTS for (const urlFound of URLS) { - const existent = existentURLs.find((ex) => ex.href === urlFound.href && ex.lineNumber === urlFound.lineNumber && ex.columnNumber === urlFound.columnNumber && ex.filePath === urlFound.filePath) + const existent = existentURLs.find( + (ex) => + ex.href === urlFound.href && + ex.lineNumber === urlFound.lineNumber && + ex.columnNumber === urlFound.columnNumber && + ex.filePath === urlFound.filePath + ) if (!existent && urlFound.host) { existentURLs.push(urlFound) } @@ -118,9 +142,17 @@ export const syncURLs = async (showIgnored: boolean) => { // REMOVE FROM existentURLs URLs THAT WAS NOT FOUND IN FILES ANYMORE for (const existent of existentURLs) { - const urlFound = URLS.find((ex) => ex.href === existent.href && ex.lineNumber === existent.lineNumber && ex.columnNumber === existent.columnNumber && ex.filePath === existent.filePath) + const urlFound = URLS.find( + (ex) => + ex.href === existent.href && + ex.lineNumber === existent.lineNumber && + ex.columnNumber === existent.columnNumber && + ex.filePath === existent.filePath + ) if (!urlFound) { - existentURLs = existentURLs.filter((ex) => ex.href !== existent.href) + existentURLs = existentURLs.filter( + (ex) => ex.href !== existent.href + ) } } @@ -137,11 +169,16 @@ export const syncURLs = async (showIgnored: boolean) => { } } -export const getURLs = async (forceSync = false, showIgnored: boolean): Promise => { +export const getURLs = async ( + forceSync = false, + showIgnored: boolean, + currentPage: number, + searchText: string +): Promise<{ urls: IURL[]; pages: IPage[], totalPages: number }> => { const context = getContext() if (!context) { - return URLS + return { urls: URLS, pages: [], totalPages: 0 } } if (forceSync) { @@ -151,14 +188,18 @@ export const getURLs = async (forceSync = false, showIgnored: boolean): Promise< try { const existentURLs = context.workspaceState.get('urls') || [] - const starredURLs = (existentURLs.filter((ex) => ex.isStarred) || []).sort((a, b) => { + const starredURLs = ( + existentURLs.filter((ex) => ex.isStarred) || [] + ).sort((a, b) => { if (!a.host || !b.host) { return 1 } return a.host >= b.host ? 1 : -1 }) - const notStarredURLs = (existentURLs.filter((ex) => !ex.isStarred) || []).sort((a, b) => { + const notStarredURLs = ( + existentURLs.filter((ex) => !ex.isStarred) || [] + ).sort((a, b) => { if (!a.host || !b.host) { return 1 } @@ -166,10 +207,42 @@ export const getURLs = async (forceSync = false, showIgnored: boolean): Promise< return a.host >= b.host ? 1 : -1 }) - return [...starredURLs, ...notStarredURLs].filter((ex) => showIgnored || !ex.isIgnored) + const fileredURLs = [...starredURLs, ...notStarredURLs].filter( + (ex) => + (showIgnored || !ex.isIgnored) && + (!searchText || ex.href.includes(searchText)) + ) + + // for treeview should show all URLs + if (currentPage === -1) { + return { urls: fileredURLs, pages: [], totalPages: 0 } + } + + const QTY_PER_PAGE = 6 + + let pages = [ + ...Array(Math.ceil(fileredURLs.length / QTY_PER_PAGE)).keys(), + ] + + const totalPages = pages.length + + if (pages.length > 5 && currentPage > 3) { + pages = pages.splice(currentPage - 3, 5) + } else if (pages.length > 5) { + pages = pages.splice(0, 5) + } + + return { + urls: fileredURLs.splice( + (currentPage - 1) * QTY_PER_PAGE, + QTY_PER_PAGE + ), + pages: pages.map((item) => ({ number: item + 1 })), + totalPages, + } } catch (error) { logger.log({ message: `getURLs ERROR: ${error.message}` }) - return [] + return { urls: [], pages: [], totalPages: 0 } } } @@ -181,7 +254,8 @@ export const saveURLDescription = async (url: IURL) => { } try { - const urlsFound: IURL[] = context.workspaceState.get('urls') || [] + const urlsFound: IURL[] = + context.workspaceState.get('urls') || [] if (urlsFound.length <= 0) { return @@ -207,7 +281,8 @@ export const restoreURLFromIgnoreList = async (url: IURL) => { } try { - const existentURLs: IURL[] = context.workspaceState.get('urls') || [] + const existentURLs: IURL[] = + context.workspaceState.get('urls') || [] for (const existent of existentURLs) { if (existent.href === url.href) { @@ -217,7 +292,9 @@ export const restoreURLFromIgnoreList = async (url: IURL) => { context.workspaceState.update('urls', existentURLs) } catch (error) { - logger.log({ message: `restoreURLFromIgnoreList ERROR: ${error.message}` }) + logger.log({ + message: `restoreURLFromIgnoreList ERROR: ${error.message}`, + }) } } @@ -229,7 +306,8 @@ export const addURLToIgnoreList = async (url: IURL) => { } try { - const existentURLs: IURL[] = context.workspaceState.get('urls') || [] + const existentURLs: IURL[] = + context.workspaceState.get('urls') || [] for (const existent of existentURLs) { if (existent.href === url.href) { @@ -252,7 +330,8 @@ export const restoreURLFromStarredList = async (url: IURL) => { } try { - const existentURLs: IURL[] = context.workspaceState.get('urls') || [] + const existentURLs: IURL[] = + context.workspaceState.get('urls') || [] for (const existent of existentURLs) { if (existent.href === url.href) { @@ -262,7 +341,9 @@ export const restoreURLFromStarredList = async (url: IURL) => { context.workspaceState.update('urls', existentURLs) } catch (error) { - logger.log({ message: `restoreURLFromIgnoreList ERROR: ${error.message}` }) + logger.log({ + message: `restoreURLFromIgnoreList ERROR: ${error.message}`, + }) } } @@ -274,7 +355,8 @@ export const addURLToStarredList = async (url: IURL) => { } try { - const existentURLs: IURL[] = context.workspaceState.get('urls') || [] + const existentURLs: IURL[] = + context.workspaceState.get('urls') || [] for (const existent of existentURLs) { if (existent.href === url.href) { diff --git a/src/services/urls/interfaces/index.ts b/src/services/urls/interfaces/index.ts index ec36f51..78c1972 100644 --- a/src/services/urls/interfaces/index.ts +++ b/src/services/urls/interfaces/index.ts @@ -11,3 +11,7 @@ export interface IURL extends Url { lineNumber: number columnNumber: number } + +export interface IPage { + number: number +} \ No newline at end of file diff --git a/src/services/urls/models/index.ts b/src/services/urls/models/index.ts index d57cb3b..71ddb47 100644 --- a/src/services/urls/models/index.ts +++ b/src/services/urls/models/index.ts @@ -4,7 +4,12 @@ import { IURL } from '../interfaces' export default class URL { private _url: IURL - constructor(url: string, filePath: string, lineNumber: number, columnNumber: number) { + constructor( + url: string, + filePath: string, + lineNumber: number, + columnNumber: number + ) { /* eslint-disable-next-line no-underscore-dangle */ this._url = { ...parse(url), diff --git a/src/services/webview/enums/index.ts b/src/services/webview/enums/index.ts index 7ca6688..25d8758 100644 --- a/src/services/webview/enums/index.ts +++ b/src/services/webview/enums/index.ts @@ -12,6 +12,8 @@ export enum EActionTypes { TOGGLE_SHOW_IGNORED = 'TOGGLE_SHOW_IGNORED', STAR = 'STAR', UNSTAR = 'UNSTAR', + CHANGE_PAGE = 'CHANGE_PAGE', + SEARCH = 'SEARCH', } export default EActionTypes diff --git a/src/services/webview/index.ts b/src/services/webview/index.ts index bf86a04..4779a10 100644 --- a/src/services/webview/index.ts +++ b/src/services/webview/index.ts @@ -92,7 +92,11 @@ const getScriptsToInject = async (): Promise => { logger.log({ message: 'Injecting script ...' }) const context = getContext() - const order = ['knockout.min.js', 'knockout.mapping.min.js', 'script.js'] + const order = [ + 'knockout.min.js', + 'knockout.mapping.min.js', + 'script.js', + ] if (!context) { return undefined @@ -130,7 +134,11 @@ const getScriptsToInject = async (): Promise => { } } -const prepareHTML = async (html: string, shouldShowIgnored: boolean) => { +const prepareHTML = async ( + html: string, + shouldShowIgnored: boolean, + searchText: string +) => { try { const context = getContext() @@ -140,7 +148,12 @@ const prepareHTML = async (html: string, shouldShowIgnored: boolean) => { logger.log({ message: 'Preparing HTML ...' }) - html = html.replace(/{{SHOW_IGNORED}}/g, shouldShowIgnored ? ' show-ignored' : '') + html = html.replace( + /{{SHOW_IGNORED}}/g, + shouldShowIgnored ? ' show-ignored' : '' + ) + + html = html.replace(/{{SEARCH_TEXT}}/g, searchText || '') const scripts = await getScriptsToInject() @@ -166,7 +179,11 @@ const prepareHTML = async (html: string, shouldShowIgnored: boolean) => { } } -export const getHTML = async (force = false, shouldShowIgnored: boolean) => { +export const getHTML = async ( + force = false, + shouldShowIgnoredStored: boolean, + searchText: string +) => { try { if (!force) { return HTML @@ -185,7 +202,11 @@ export const getHTML = async (force = false, shouldShowIgnored: boolean) => { const htmlFilePath = join(assetsPaths.root, 'index.html') let htmlFileContent = readFileSync(htmlFilePath).toString() - htmlFileContent = await prepareHTML(htmlFileContent, shouldShowIgnored) + htmlFileContent = await prepareHTML( + htmlFileContent, + shouldShowIgnoredStored, + searchText + ) HTML = htmlFileContent @@ -196,7 +217,12 @@ export const getHTML = async (force = false, shouldShowIgnored: boolean) => { } } -const sendURLs = async (forceSync: boolean, shouldShowIgnored: boolean) => { +const sendURLs = async ( + forceSync: boolean, + shouldShowIgnored: boolean, + currentPage: number, + searchText: string +) => { try { const context = getContext() @@ -208,17 +234,31 @@ const sendURLs = async (forceSync: boolean, shouldShowIgnored: boolean) => { const assetsPaths = getAssetsPaths() - const urls = await getURLs(forceSync, shouldShowIgnored) - const fallbackFaviconPath = vscode.Uri.file(join(assetsPaths.img, 'fallback-favicon.png')) + const { urls, pages, totalPages } = await getURLs( + forceSync, + shouldShowIgnored, + currentPage, + searchText + ) + const fallbackFaviconPath = vscode.Uri.file( + join(assetsPaths.img, 'fallback-favicon.png') + ) for (const url of urls) { if (!url.hasFavicon && WEBVIEW_PANNEL) { - url.favicon = WEBVIEW_PANNEL.webview.asWebviewUri(fallbackFaviconPath).toString() + url.favicon = WEBVIEW_PANNEL.webview + .asWebviewUri(fallbackFaviconPath) + .toString() url.hasFavicon = false } } - WEBVIEW_PANNEL.webview.postMessage({ urls, type: EActionTypes.URL }) + WEBVIEW_PANNEL.webview.postMessage({ + urls, + pages, + totalPages, + type: EActionTypes.URL, + }) } catch (error) { logger.log({ message: `sendURLs ERROR: ${error.message}` }) } @@ -243,8 +283,12 @@ const sendFavicons = async () => { for (const url of existentURLs) { try { if (!url.hasFavicon) { - const favicon: IFavicon = await pageIcon(`${url.protocol}//${url.hostname}`) - url.favicon = `data:${favicon.mime};base64, ${favicon.data.toString('base64')}` + const favicon: IFavicon = await pageIcon( + `${url.protocol}//${url.hostname}` + ) + url.favicon = `data:${ + favicon.mime + };base64, ${favicon.data.toString('base64')}` url.hasFavicon = true } } catch (error) {} @@ -268,6 +312,21 @@ const updateTreeviews = async () => { treeviews.IGNORED_TREEVIEW.updateTreviewData() } +const reloadWebView = async ( + shouldShowIgnoredStored: boolean, + currentPage: number, + searchText: string +) => { + const html = await getHTML(true, shouldShowIgnoredStored, searchText) + if (html && WEBVIEW_PANNEL) { + WEBVIEW_PANNEL.webview.html = html + } + await sendURLs(true, shouldShowIgnoredStored, currentPage, searchText) + await stopLoading() + await updateTreeviews() + await sendFavicons() +} + export const openWebview = async (ignoreFocus?: boolean) => { const context = getContext() @@ -275,7 +334,8 @@ export const openWebview = async (ignoreFocus?: boolean) => { return } - const shouldShowIgnored = context.workspaceState.get('shouldShowIgnored') || false + const shouldShowIgnored = + context.workspaceState.get('shouldShowIgnored') || false const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined @@ -300,136 +360,86 @@ export const openWebview = async (ignoreFocus?: boolean) => { WEBVIEW_PANNEL = undefined }) - WEBVIEW_PANNEL.webview.onDidReceiveMessage((message: any) => { - const currentContext = getContext() - - if (!currentContext) { - return - } - - const shouldShowIgnoredStored = - currentContext.workspaceState.get('shouldShowIgnored') || false - const { url } = message - - switch (message.type) { - case EActionTypes.COPY: - if (url.href) { - vscode.env.clipboard.writeText(url.href) - vscode.window.showInformationMessage(`'${url.href}' copied to clipboard`) - } - - break + WEBVIEW_PANNEL.webview.onDidReceiveMessage( + async (message: { + type: EActionTypes + url: IURL + currentPage: number + isShowingIgnored: boolean + searchText: string + }) => { + const currentContext = getContext() + + if (!currentContext) { + return + } - case EActionTypes.STAR: - ;(async () => { - await addURLToStarredList(url) - const html = await getHTML(true, shouldShowIgnoredStored) - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html + let shouldReloadWebView = false + + const { url, isShowingIgnored, currentPage, searchText } = + message + + switch (message.type) { + case EActionTypes.SEARCH: + case EActionTypes.CHANGE_PAGE: + shouldReloadWebView = true + break + case EActionTypes.COPY: + if (url.href) { + vscode.env.clipboard.writeText(url.href) + vscode.window.showInformationMessage( + `'${url.href}' copied to clipboard` + ) } + break - await sendURLs(true, shouldShowIgnoredStored) - await stopLoading() - await updateTreeviews() - await sendFavicons() - })() - break + case EActionTypes.STAR: + shouldReloadWebView = true + await addURLToStarredList(url) + break - case EActionTypes.UNSTAR: - ;(async () => { + case EActionTypes.UNSTAR: + shouldReloadWebView = true await restoreURLFromStarredList(url) - const html = await getHTML(true, shouldShowIgnoredStored) - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html - } + break - await sendURLs(true, shouldShowIgnoredStored) - await stopLoading() - await updateTreeviews() - await sendFavicons() - })() - break - - case EActionTypes.IGNORE: - ;(async () => { + case EActionTypes.IGNORE: + shouldReloadWebView = true await addURLToIgnoreList(url) - const html = await getHTML(true, shouldShowIgnoredStored) - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html - } - - await sendURLs(true, shouldShowIgnoredStored) - await stopLoading() - await updateTreeviews() - await sendFavicons() - })() - break + break - case EActionTypes.RESTORE: - ;(async () => { + case EActionTypes.RESTORE: + shouldReloadWebView = true await restoreURLFromIgnoreList(url) - const html = await getHTML(true, shouldShowIgnoredStored) - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html - } - - await sendURLs(true, shouldShowIgnoredStored) - await stopLoading() - await updateTreeviews() - await sendFavicons() - })() - break + break - case EActionTypes.SAVE_URL_DESCRIPTION: - ;(async () => { + case EActionTypes.SAVE_URL_DESCRIPTION: + shouldReloadWebView = true await saveURLDescription(url) - const html = await getHTML(true, shouldShowIgnoredStored) - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html - } - - await sendURLs(true, shouldShowIgnoredStored) - await stopLoading() - await sendFavicons() - })() - break + break + + case EActionTypes.TOGGLE_SHOW_IGNORED: + shouldReloadWebView = true + currentContext.workspaceState.update( + 'shouldShowIgnored', + isShowingIgnored + ) + break + + default: + break + } - case EActionTypes.TOGGLE_SHOW_IGNORED: - currentContext.workspaceState.update( - 'shouldShowIgnored', - !shouldShowIgnoredStored + if (shouldReloadWebView) { + await reloadWebView( + isShowingIgnored, + currentPage, + searchText ) - ;(async () => { - const html = await getHTML(true, !shouldShowIgnoredStored) - - if (html && WEBVIEW_PANNEL) { - WEBVIEW_PANNEL.webview.html = html - } - - await sendURLs(true, !shouldShowIgnoredStored) - await stopLoading() - await updateTreeviews() - await sendFavicons() - })() - break - - default: - break + } } - }) - } - - const viewHtml = await getHTML(true, shouldShowIgnored) - - if (!viewHtml) { - logger.log({ message: `No HTML string found` }) - return + ) } - WEBVIEW_PANNEL.webview.html = viewHtml - - await startLoading() - await sendURLs(true, shouldShowIgnored) - await stopLoading() - await sendFavicons() + await reloadWebView(shouldShowIgnored, 1, '') }