├── .npmrc ├── icon.png ├── .gitignore ├── demo └── path-autocomplete.gif ├── .vscodeignore ├── .vscode ├── extensions.json ├── cSpell.json ├── settings.json ├── tasks.json └── launch.json ├── src ├── features │ ├── Normalize.ts │ ├── FileInfo.ts │ ├── FsUtils.ts │ ├── PathConfiguration.ts │ └── PathAutocompleteProvider.ts ├── extension.ts └── util │ ├── process.ts │ ├── platform.ts │ └── path.ts ├── tsconfig.json ├── .prettierrc ├── LICENSE ├── .eslintrc.js ├── CHANGELOG.md ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry="https://registry.npmjs.org/" -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihai-vlc/path-autocomplete/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | package-lock.json 4 | tmp 5 | *.vsix 6 | dist 7 | -------------------------------------------------------------------------------- /demo/path-autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihai-vlc/path-autocomplete/HEAD/demo/path-autocomplete.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | tmp/** 6 | src/** 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | demo 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "eamodio.gitlens", 5 | "esbenp.prettier-vscode", 6 | "streetsidesoftware.code-spell-checker" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [], 6 | "words": ["minimatch"], 7 | "ignoreWords": [], 8 | "import": [] 9 | } 10 | -------------------------------------------------------------------------------- /src/features/Normalize.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | const normalize = require('normalize-path'); 3 | 4 | export function normalizeForBrowser(filePath) { 5 | // we only need to do the normalization if we are in a browser environment 6 | if (os.platform().indexOf('browser') === -1) { 7 | return filePath; 8 | } 9 | 10 | return normalize(filePath); 11 | } 12 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode'; 2 | import { PathAutocomplete } from './features/PathAutocompleteProvider'; 3 | 4 | export function activate(context: vscode.ExtensionContext) { 5 | const selector: vscode.DocumentSelector = [ 6 | { 7 | pattern: '**', 8 | }, 9 | ]; 10 | context.subscriptions.push( 11 | vscode.languages.registerCompletionItemProvider(selector, new PathAutocomplete(), '/', '\\'), 12 | ); 13 | } 14 | 15 | // this method is called when your extension is deactivated 16 | // export function deactivate() {} 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "out": true, // set this to false to include "out" folder in search results 5 | "**/*.lock": true 6 | }, 7 | "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version 8 | "[javascript][typescript][typescriptreact][json][jsonc][yaml][markdown]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "editor.formatOnSave": true 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2021", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | "rootDir": ".", 8 | 9 | /* Module Resolution Options */ 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | 14 | /* Experimental Options */ 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | 18 | /* Advanced Options */ 19 | "forceConsistentCasingInFileNames": true, 20 | "skipLibCheck": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "endOfLine": "auto", 7 | "printWidth": 105, 8 | "overrides": [ 9 | { 10 | "files": "*.svg", 11 | "options": { 12 | "parser": "html" 13 | } 14 | }, 15 | { 16 | "files": "*.md", 17 | "options": { 18 | "tabWidth": 2 19 | } 20 | }, 21 | { 22 | "files": "*.yaml", 23 | "options": { 24 | "tabWidth": 4 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/features/FileInfo.ts: -------------------------------------------------------------------------------- 1 | import { basename } from '../util/path'; 2 | export type FileType = 'dir' | 'file'; 3 | 4 | export class FileInfo { 5 | private type: FileType; 6 | name: string; 7 | path: string; 8 | 9 | /** 10 | * Extracts the needed information about the provider file path. 11 | * 12 | * @throws Error if the path is invalid or you don't have permissions to it 13 | */ 14 | constructor(path: string, type: FileType) { 15 | this.name = basename(path); 16 | this.path = path; 17 | this.type = type; 18 | } 19 | 20 | get isDirectory() { 21 | return this.type === 'dir'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/features/FsUtils.ts: -------------------------------------------------------------------------------- 1 | import vs from 'vscode'; 2 | 3 | export async function pathExists(localPath: string): Promise { 4 | try { 5 | await vs.workspace.fs.stat(vs.Uri.file(localPath)); 6 | return true; 7 | } catch (e) { 8 | return false; 9 | } 10 | } 11 | 12 | export async function isDirectory(filePath: string): Promise { 13 | try { 14 | const stat = await vs.workspace.fs.stat(vs.Uri.file(filePath)); 15 | return stat.type === vs.FileType.Directory; 16 | } catch (e) { 17 | return false; 18 | } 19 | } 20 | 21 | export async function readDirectory(filePath: string) { 22 | return vs.workspace.fs.readDirectory(vs.Uri.file(filePath)); 23 | } 24 | -------------------------------------------------------------------------------- /.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 | "type": "npm", 21 | "script": "watch-web", 22 | "group": "build", 23 | "isBackground": true, 24 | "problemMatcher": [ 25 | "$ts-webpack-watch" 26 | ] 27 | }, 28 | { 29 | "label": "My Task", 30 | "type": "shell", 31 | "command": "echo ${relativeFileDirname}", 32 | "problemMatcher": [] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mihai Ionut Vilcu 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. -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | const WARN = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | env: { 7 | es2021: true, 8 | node: true, 9 | }, 10 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 13, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['@typescript-eslint'], 17 | rules: { 18 | '@typescript-eslint/no-unused-vars': OFF, 19 | '@typescript-eslint/no-explicit-any': OFF, 20 | '@typescript-eslint/no-var-requires': OFF, 21 | 22 | 'no-empty': OFF, 23 | 24 | eqeqeq: [ERROR, 'smart'], 25 | radix: ERROR, 26 | 27 | 'block-scoped-var': ERROR, 28 | 'consistent-this': ERROR, 29 | 'default-case-last': ERROR, 30 | 'default-case': ERROR, 31 | 'dot-notation': ERROR, 32 | 'func-name-matching': ERROR, 33 | 'guard-for-in': ERROR, 34 | 'max-lines': [ERROR, { max: 2048 }], 35 | 'max-nested-callbacks': ERROR, 36 | 'max-params': [ERROR, { max: 10 }], 37 | 'new-cap': ERROR, 38 | 'no-invalid-this': ERROR, 39 | 'no-unused-expressions': ERROR, 40 | 'no-use-before-define': [ERROR, { functions: false }], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Run for native VS Code", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceRoot}/out/**/*.js" 16 | ], 17 | "skipFiles": [ 18 | "/**", 19 | "**/node_modules/**", 20 | "**/resources/app/out/vs/**" 21 | ], 22 | "preLaunchTask": "npm: watch" 23 | }, 24 | { 25 | "name": "Run Web Extension in VS Code", 26 | "type": "extensionHost", 27 | "debugWebWorkerHost": true, 28 | "request": "launch", 29 | "args": [ 30 | "--extensionDevelopmentPath=${workspaceFolder}", 31 | "--extensionDevelopmentKind=web" 32 | ], 33 | "outFiles": [ 34 | "${workspaceFolder}/dist/web/**/*.js" 35 | ], 36 | "preLaunchTask": "npm: watch-web" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /src/util/process.ts: -------------------------------------------------------------------------------- 1 | import { globals, INodeProcess, isMacintosh, isWindows } from './platform'; 2 | 3 | let safeProcess: Omit & { arch: string | undefined }; 4 | declare const process: INodeProcess; 5 | 6 | // Native sandbox environment 7 | if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { 8 | const sandboxProcess: INodeProcess = globals.vscode.process; 9 | safeProcess = { 10 | get platform() { 11 | return sandboxProcess.platform; 12 | }, 13 | get arch() { 14 | return sandboxProcess.arch; 15 | }, 16 | get env() { 17 | return sandboxProcess.env; 18 | }, 19 | cwd() { 20 | return sandboxProcess.cwd(); 21 | }, 22 | }; 23 | } 24 | 25 | // Native node.js environment 26 | else if (typeof process !== 'undefined') { 27 | safeProcess = { 28 | get platform() { 29 | return process.platform; 30 | }, 31 | get arch() { 32 | return process.arch; 33 | }, 34 | get env() { 35 | return process.env; 36 | }, 37 | cwd() { 38 | return process.env.VSCODE_CWD || process.cwd(); 39 | }, 40 | }; 41 | } 42 | 43 | // Web environment 44 | else { 45 | safeProcess = { 46 | // Supported 47 | get platform() { 48 | return isWindows ? 'win32' : isMacintosh ? 'darwin' : 'linux'; 49 | }, 50 | get arch() { 51 | return undefined; /* arch is undefined in web */ 52 | }, 53 | 54 | // Unsupported 55 | get env() { 56 | return {}; 57 | }, 58 | cwd() { 59 | return '/'; 60 | }, 61 | }; 62 | } 63 | 64 | /** 65 | * Provides safe access to the `cwd` property in node.js, sandboxed or web 66 | * environments. 67 | * 68 | * Note: in web, this property is hardcoded to be `/`. 69 | */ 70 | export const cwd = safeProcess.cwd; 71 | 72 | /** 73 | * Provides safe access to the `env` property in node.js, sandboxed or web 74 | * environments. 75 | * 76 | * Note: in web, this property is hardcoded to be `{}`. 77 | */ 78 | export const env = safeProcess.env; 79 | 80 | /** 81 | * Provides safe access to the `platform` property in node.js, sandboxed or web 82 | * environments. 83 | */ 84 | export const platform = safeProcess.platform; 85 | 86 | /** 87 | * Provides safe access to the `arch` method in node.js, sandboxed or web 88 | * environments. 89 | * Note: `arch` is `undefined` in web 90 | */ 91 | export const arch = safeProcess.arch; 92 | -------------------------------------------------------------------------------- /src/features/PathConfiguration.ts: -------------------------------------------------------------------------------- 1 | import vs from 'vscode'; 2 | import * as process from '../util/process'; 3 | import * as path from '../util/path'; 4 | 5 | interface PathConfigurationValues { 6 | withExtension?: boolean; 7 | withExtensionOnImport?: boolean; 8 | excludedItems?: { 9 | [key: string]: { 10 | when: string; 11 | isDir?: boolean; 12 | context?: string; 13 | }; 14 | }; 15 | pathMappings?: [ 16 | { 17 | [key: string]: string; 18 | }, 19 | ]; 20 | transformations?: [ 21 | { 22 | type: string; 23 | parameters?: Array; 24 | when?: { 25 | fileName?: string; 26 | path?: string; 27 | }; 28 | }, 29 | ]; 30 | triggerOutsideStrings?: boolean; 31 | disableUpOneFolder?: boolean; 32 | enableFolderTrailingSlash?: boolean; 33 | pathSeparators?: string; 34 | homeDirectory?: string; 35 | workspaceFolderPath?: string; 36 | workspaceRootPath?: string; 37 | useBackslash?: boolean; 38 | useSingleBackslash?: boolean; 39 | ignoredFilesPattern?: string; 40 | ignoredPrefixes?: Array; 41 | fileDirname?: string; 42 | relativeFileDirname?: string; 43 | } 44 | 45 | export default class PathConfiguration { 46 | static configuration = new PathConfiguration(); 47 | 48 | readonly data: PathConfigurationValues; 49 | 50 | private constructor() { 51 | this.data = {}; 52 | this.update(); 53 | } 54 | 55 | update(fileUri?: vs.Uri, languageId?: string) { 56 | const codeConfiguration = vs.workspace.getConfiguration('path-autocomplete', { 57 | uri: fileUri, 58 | languageId: languageId, 59 | }); 60 | 61 | this.data.withExtension = codeConfiguration.get('includeExtension'); 62 | this.data.withExtensionOnImport = codeConfiguration.get('extensionOnImport'); 63 | this.data.excludedItems = codeConfiguration.get('excludedItems'); 64 | this.data.pathMappings = codeConfiguration.get('pathMappings'); 65 | this.data.pathSeparators = codeConfiguration.get('pathSeparators'); 66 | this.data.transformations = codeConfiguration.get('transformations'); 67 | this.data.triggerOutsideStrings = codeConfiguration.get('triggerOutsideStrings'); 68 | this.data.disableUpOneFolder = codeConfiguration.get('disableUpOneFolder'); 69 | this.data.useBackslash = codeConfiguration.get('useBackslash'); 70 | this.data.useSingleBackslash = codeConfiguration.get('useSingleBackslash'); 71 | this.data.enableFolderTrailingSlash = codeConfiguration.get('enableFolderTrailingSlash'); 72 | this.data.ignoredFilesPattern = codeConfiguration.get('ignoredFilesPattern'); 73 | this.data.ignoredPrefixes = codeConfiguration.get('ignoredPrefixes'); 74 | this.data.homeDirectory = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; 75 | 76 | const workspaceRootFolder = vs.workspace.workspaceFolders 77 | ? vs.workspace.workspaceFolders[0] 78 | : null; 79 | let workspaceFolder = workspaceRootFolder; 80 | 81 | if (fileUri) { 82 | workspaceFolder = vs.workspace.getWorkspaceFolder(fileUri); 83 | const dirName = path.dirname(fileUri.fsPath); 84 | this.data.fileDirname = dirName; 85 | 86 | if (workspaceFolder) { 87 | this.data.relativeFileDirname = 88 | path.relative(workspaceFolder.uri.fsPath, dirName) || '.'; 89 | } 90 | } 91 | 92 | this.data.workspaceFolderPath = workspaceFolder && workspaceFolder.uri.fsPath; 93 | this.data.workspaceRootPath = workspaceRootFolder && workspaceRootFolder.uri.fsPath; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Path Autocomplete Change Log 2 | 3 | #### 1.25.0 4 | 5 | - Adds support for conditional path mappings 6 | 7 | ```jsonc 8 | "path-autocomplete.pathMappings": { 9 | "$root": { 10 | "conditions": [ 11 | { 12 | "when": "**/packages/math/**", 13 | "value": "${folder}/packages/math" 14 | }, 15 | { 16 | "when": "**/packages/ui/**", 17 | "value": "${folder}/packages/ui" 18 | } 19 | ] 20 | } 21 | }, 22 | ``` 23 | 24 | #### 1.24.0 25 | 26 | - Adds support for regex modifiers on the replace transformation. Fixes #118 27 | - Adds support for the `inputReplace` transformation type. Fixes #118 28 | 29 | #### 1.23.0 30 | 31 | - Adds support for language specific configurations. Fixes #123 32 | 33 | #### 1.22.0 34 | 35 | - Removes the need for the trailing slash in the path mappings folders. Fixes #111 36 | - Removes dependency on the native path and process node modules 37 | - Adds support for the relativeFileDirname variable in the path mappings. Fixes #94 38 | 39 | #### 1.21.0 40 | 41 | - Adds support for the `useSingleBackslash` preference. Fixes #57 42 | - Implements the auto detection of the `useBackslash` flag, fixes #112 43 | - Updates the activation events to use `onStartupFinished`, fixes #114 44 | 45 | #### 1.20.1 46 | 47 | - Normalizes the path when using the VS Code for Web from Windows 48 | 49 | #### 1.20.0 50 | 51 | - Adds support for VS Code for Web 52 | 53 | #### 1.19.1 54 | 55 | - Fixes the extension loading on remote ssh #108 56 | 57 | #### 1.19.0 58 | 59 | - Updates the completion label to be consistent with insertText 60 | - Fix: asar file is recogonized as directory 61 | - Upgrade dependencies 62 | 63 | #### 1.18.0 64 | 65 | 1. integrate some useful front end tools: eslint, prettier 66 | 2. optimize dotfiles, settings.json, launch.json 67 | 3. fix #100 68 | 4. remove npm package suggestion because VSCode has builtin support 69 | 5. code style and performance optimizations 70 | 71 | #### 1.17.0 72 | 73 | Adds support for partial paths. 74 | Previously the completions were only generated if the path inserted by the user 75 | was a valid folder on the disk. 76 | Starting with this version partial paths are suppored as well. 77 | Examples: 78 | 79 | ``` 80 | ./tmp/folder1/ -- generates suggetions 81 | ./tmp/fol -- generates suggetions for ./tmp/ and filters out items that don't start with fol 82 | ``` 83 | 84 | This feature fixes: #87 85 | 86 | #### 1.16.0 87 | 88 | Added new option `path-autocomplete.disableUpOneFolder`. Fixes #89 89 | By default it's set to `true`. 90 | 91 | #### 1.15.0 92 | 93 | Added new rules for the excludedItems option. 94 | Stating with this version we can now do things like: 95 | 96 | ``` 97 | "path-autocomplete.excludedItems": { 98 | "**": { "when": "**", "isDir": true }, // always ignore `folder` suggestions 99 | "**/*.js": { "when": "**", "context": "import.*" }, // ignore .js file suggestions in all files when the current line matches the regex from the `context` 100 | } 101 | ``` 102 | 103 | #### 1.14.0 104 | 105 | Added new option `path-autocomplete.ignoredPrefixes`. Fixes #81 106 | 107 | #### 1.13.6 108 | 109 | Moved the change log from the readme file to the `CHANGELOG.md` file. 110 | 111 | #### 1.13.5 112 | 113 | resolve #72 - include `require` in the "extensionOnImport" preference 114 | 115 | #### 1.13.3 116 | 117 | Fixes the completion items for json files. Fixes #47 118 | 119 | #### 1.13.2 120 | 121 | Fixes the mapping conflict with the node modules. Fixes #30. 122 | 123 | #### 1.13.1 124 | 125 | Fixes the mapping of keys with the same prefix. 126 | 127 | #### 1.13.0 128 | 129 | Adds the `path-autocomplete.ignoredFilesPattern` option to disable the extension on certain file types. 130 | Example configuration: 131 | 132 | ``` 133 | "path-autocomplete.ignoredFilesPattern": "**/*.{css,scss}" 134 | ``` 135 | 136 | #### 1.12.0 137 | 138 | Adds the `path-autocomplete.useBackslash` option to enable the use of `\\` for windows paths. 139 | 140 | #### 1.11.0 141 | 142 | Adds the `path-autocomplete.pathSeparators` option to control the separators when 143 | inserting the path outside strings. 144 | 145 | #### 1.10.0 146 | 147 | - Updates the behavior of `extensionOnImport` to be taken into account only on import statements line. 148 | - Adds the `path-autocomplete.includeExtension` option to control the extension on standard paths. (#45) 149 | - Fixes the completion kind for folders and files (#43) 150 | - Adds support for merging multiple folders in the path mappings configuration 151 | 152 | ``` 153 | "path-autocomplete.pathMappings": { 154 | "$root": ["${folder}/p1/src", "${folder}/p2/src"] 155 | } 156 | ``` 157 | 158 | #### 1.9.0 159 | 160 | - Adds `{` and `[` as separators for the current path 161 | 162 | #### 1.8.1 163 | 164 | - Fixes the handing of the path outside strings for markdown links `[](./)` 165 | 166 | #### 1.8.0 167 | 168 | - Added support for multi root vscode folders via the `${folder}` variable in pathMappings 169 | 170 | #### 1.7.0 171 | 172 | - Adds support for redefining the root folder via the pathMappings with the `$root` 173 | special key. 174 | 175 | #### 1.6.0 176 | 177 | - Adds the `path-autocomplete.enableFolderTrailingSlash` option 178 | 179 | #### 1.5.0 180 | 181 | - Adds support for path autocomplete outside strings. 182 | Available via `path-autocomplete.triggerOutsideStrings` 183 | - Improves the support for node_modules lookup. #15 184 | 185 | #### 1.4.0 186 | 187 | - Adds support for custom transformation 188 | 189 | #### 1.3.0 190 | 191 | - Adds support for custom user mappings 192 | 193 | #### 1.2.1 194 | 195 | - Fixes the extension trimming for folders. Fixes #6 196 | 197 | #### 1.2.0 198 | 199 | - Adds support for the trailing slash functionality. Fixes #5 200 | - Adds support for path autocomplete inside backticks. Fixes #3 201 | 202 | #### 1.1.0 203 | 204 | - Added option to exclude files 205 | 206 | #### 1.0.2 207 | 208 | - Initial release 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path-autocomplete", 3 | "displayName": "Path Autocomplete", 4 | "description": "Provides path completion for visual studio code and VS Code for Web.", 5 | "version": "1.25.0", 6 | "publisher": "ionutvmi", 7 | "icon": "icon.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mihai-vlc/path-autocomplete" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/mihai-vlc/path-autocomplete/issues", 14 | "email": "mihai.vlc11@gmail.com" 15 | }, 16 | "engines": { 17 | "vscode": "^1.69.0" 18 | }, 19 | "keywords": [ 20 | "path", 21 | "suggestion", 22 | "autocomplete", 23 | "intellisense", 24 | "alias" 25 | ], 26 | "categories": [ 27 | "Other" 28 | ], 29 | "activationEvents": [ 30 | "onStartupFinished" 31 | ], 32 | "main": "./out/src/extension", 33 | "browser": "./out/web/extension.js", 34 | "scripts": { 35 | "vscode:prepublish": "npm run compile && npm run compile-web", 36 | "compile": "tsc -p ./", 37 | "watch": "tsc -watch -p ./", 38 | "type-check": "tsc -p ./tsconfig.json --noEmit", 39 | "lint": "eslint -c .eslintrc.js --ext .ts", 40 | "compile-web": "webpack --config ./build/web-extension.webpack.config.js", 41 | "watch-web": "webpack --watch --config ./build/web-extension.webpack.config.js" 42 | }, 43 | "dependencies": { 44 | "minimatch": "5.0.1", 45 | "normalize-path": "3.0.0" 46 | }, 47 | "devDependencies": { 48 | "@types/minimatch": "3.0.5", 49 | "@types/node": "18.6.2", 50 | "@types/vscode": "1.69.0", 51 | "@typescript-eslint/eslint-plugin": "5.31.0", 52 | "@typescript-eslint/parser": "5.31.0", 53 | "eslint": "8.20.0", 54 | "eslint-config-prettier": "8.5.0", 55 | "os-browserify": "0.3.0", 56 | "prettier": "2.5.1", 57 | "ts-loader": "9.3.1", 58 | "typescript": "4.7.4", 59 | "webpack": "5.94.0", 60 | "webpack-cli": "4.10.0" 61 | }, 62 | "contributes": { 63 | "configuration": { 64 | "type": "object", 65 | "title": "path-autocomplete", 66 | "properties": { 67 | "path-autocomplete.extensionOnImport": { 68 | "type": "boolean", 69 | "default": false, 70 | "description": "Adds the extension when inserting file on import or require statements.", 71 | "scope": "language-overridable" 72 | }, 73 | "path-autocomplete.includeExtension": { 74 | "type": "boolean", 75 | "default": true, 76 | "description": "Adds the extension when inserting file names.", 77 | "scope": "language-overridable" 78 | }, 79 | "path-autocomplete.excludedItems": { 80 | "type": "object", 81 | "default": {}, 82 | "description": "Allows you to exclude certain files from the suggestions.", 83 | "scope": "language-overridable" 84 | }, 85 | "path-autocomplete.pathMappings": { 86 | "type": "object", 87 | "default": {}, 88 | "description": "Defines custom mappings for the autocomplete paths.", 89 | "scope": "language-overridable" 90 | }, 91 | "path-autocomplete.pathSeparators": { 92 | "type": "string", 93 | "default": " \t({[", 94 | "description": "Defines the separators for support outside string.", 95 | "scope": "language-overridable" 96 | }, 97 | "path-autocomplete.transformations": { 98 | "type": "array", 99 | "default": [], 100 | "description": "Custom transformations applied to the inserted text.", 101 | "scope": "language-overridable" 102 | }, 103 | "path-autocomplete.triggerOutsideStrings": { 104 | "type": "boolean", 105 | "default": false, 106 | "description": "Enables path autocompletion outside strings.", 107 | "scope": "language-overridable" 108 | }, 109 | "path-autocomplete.disableUpOneFolder": { 110 | "type": "boolean", 111 | "default": true, 112 | "description": "Disabled the .. option in the recommendations.", 113 | "scope": "language-overridable" 114 | }, 115 | "path-autocomplete.useBackslash": { 116 | "type": "boolean", 117 | "default": false, 118 | "description": "If enabled it will use backslash (\\) as a path separator.", 119 | "scope": "language-overridable" 120 | }, 121 | "path-autocomplete.useSingleBackslash": { 122 | "type": "boolean", 123 | "default": false, 124 | "description": "If enabled it will insert a single backslash (\\) even inside quoted strings", 125 | "scope": "language-overridable" 126 | }, 127 | "path-autocomplete.enableFolderTrailingSlash": { 128 | "type": "boolean", 129 | "default": true, 130 | "description": "Enables the trailing slash on the folder path insertion.", 131 | "scope": "language-overridable" 132 | }, 133 | "path-autocomplete.ignoredFilesPattern": { 134 | "type": "string", 135 | "default": "", 136 | "description": "Glob patterns for disabling the path completion in the specified file types.", 137 | "scope": "language-overridable" 138 | }, 139 | "path-autocomplete.ignoredPrefixes": { 140 | "type": "array", 141 | "default": [], 142 | "description": "List of prefixes for which completions will be skipped.", 143 | "scope": "language-overridable" 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/util/platform.ts: -------------------------------------------------------------------------------- 1 | const LANGUAGE_DEFAULT = 'en'; 2 | 3 | let _isWindows = false; 4 | let _isMacintosh = false; 5 | let _isLinux = false; 6 | let _isLinuxSnap = false; 7 | let _isNative = false; 8 | let _isWeb = false; 9 | let _isElectron = false; 10 | let _isIOS = false; 11 | let _isCI = false; 12 | let _locale: string | undefined = undefined; 13 | let _language: string = LANGUAGE_DEFAULT; 14 | let _translationsConfigFile: string | undefined = undefined; 15 | let _userAgent: string | undefined = undefined; 16 | 17 | interface NLSConfig { 18 | locale: string; 19 | availableLanguages: { [key: string]: string }; 20 | _translationsConfigFile: string; 21 | } 22 | 23 | export interface IProcessEnvironment { 24 | [key: string]: string | undefined; 25 | } 26 | 27 | /** 28 | * This interface is intentionally not identical to node.js 29 | * process because it also works in sandboxed environments 30 | * where the process object is implemented differently. We 31 | * define the properties here that we need for `platform` 32 | * to work and nothing else. 33 | */ 34 | export interface INodeProcess { 35 | platform: string; 36 | arch: string; 37 | env: IProcessEnvironment; 38 | versions?: { 39 | electron?: string; 40 | }; 41 | type?: string; 42 | cwd: () => string; 43 | } 44 | 45 | declare const process: INodeProcess; 46 | declare const global: unknown; 47 | declare const self: unknown; 48 | 49 | export const globals: any = 50 | typeof self === 'object' ? self : typeof global === 'object' ? global : {}; 51 | 52 | let nodeProcess: INodeProcess | undefined = undefined; 53 | if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') { 54 | // Native environment (sandboxed) 55 | nodeProcess = globals.vscode.process; 56 | } else if (typeof process !== 'undefined') { 57 | // Native environment (non-sandboxed) 58 | nodeProcess = process; 59 | } 60 | 61 | const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string'; 62 | const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer'; 63 | 64 | interface INavigator { 65 | userAgent: string; 66 | maxTouchPoints?: number; 67 | } 68 | declare const navigator: INavigator; 69 | 70 | // Web environment 71 | if (typeof navigator === 'object' && !isElectronRenderer) { 72 | _userAgent = navigator.userAgent; 73 | _isWindows = _userAgent.indexOf('Windows') >= 0; 74 | _isMacintosh = _userAgent.indexOf('Macintosh') >= 0; 75 | _isIOS = 76 | (_userAgent.indexOf('Macintosh') >= 0 || 77 | _userAgent.indexOf('iPad') >= 0 || 78 | _userAgent.indexOf('iPhone') >= 0) && 79 | !!navigator.maxTouchPoints && 80 | navigator.maxTouchPoints > 0; 81 | _isLinux = _userAgent.indexOf('Linux') >= 0; 82 | _isWeb = true; 83 | 84 | _locale = LANGUAGE_DEFAULT; 85 | 86 | _language = _locale; 87 | } 88 | 89 | // Native environment 90 | else if (typeof nodeProcess === 'object') { 91 | _isWindows = nodeProcess.platform === 'win32'; 92 | _isMacintosh = nodeProcess.platform === 'darwin'; 93 | _isLinux = nodeProcess.platform === 'linux'; 94 | _isLinuxSnap = _isLinux && !!nodeProcess.env.SNAP && !!nodeProcess.env.SNAP_REVISION; 95 | _isElectron = isElectronProcess; 96 | _isCI = !!nodeProcess.env.CI || !!nodeProcess.env.BUILD_ARTIFACTSTAGINGDIRECTORY; 97 | _locale = LANGUAGE_DEFAULT; 98 | _language = LANGUAGE_DEFAULT; 99 | const rawNlsConfig = nodeProcess.env.VSCODE_NLS_CONFIG; 100 | if (rawNlsConfig) { 101 | try { 102 | const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); 103 | const resolved = nlsConfig.availableLanguages['*']; 104 | _locale = nlsConfig.locale; 105 | // VSCode's default language is 'en' 106 | _language = resolved ? resolved : LANGUAGE_DEFAULT; 107 | _translationsConfigFile = nlsConfig._translationsConfigFile; 108 | } catch (e) {} 109 | } 110 | _isNative = true; 111 | } 112 | 113 | // Unknown environment 114 | else { 115 | console.error('Unable to resolve platform.'); 116 | } 117 | 118 | export const enum Platform { 119 | Web, 120 | Mac, 121 | Linux, 122 | Windows, 123 | } 124 | export function PlatformToString(platform: Platform) { 125 | switch (platform) { 126 | case Platform.Web: 127 | return 'Web'; 128 | case Platform.Mac: 129 | return 'Mac'; 130 | case Platform.Linux: 131 | return 'Linux'; 132 | case Platform.Windows: 133 | return 'Windows'; 134 | default: 135 | return ''; 136 | } 137 | } 138 | 139 | let _platform: Platform = Platform.Web; 140 | if (_isMacintosh) { 141 | _platform = Platform.Mac; 142 | } else if (_isWindows) { 143 | _platform = Platform.Windows; 144 | } else if (_isLinux) { 145 | _platform = Platform.Linux; 146 | } 147 | 148 | export const isWindows = _isWindows; 149 | export const isMacintosh = _isMacintosh; 150 | export const isLinux = _isLinux; 151 | export const isLinuxSnap = _isLinuxSnap; 152 | export const isNative = _isNative; 153 | export const isElectron = _isElectron; 154 | export const isWeb = _isWeb; 155 | export const isWebWorker = _isWeb && typeof globals.importScripts === 'function'; 156 | export const isIOS = _isIOS; 157 | /** 158 | * Whether we run inside a CI environment, such as 159 | * GH actions or Azure Pipelines. 160 | */ 161 | export const isCI = _isCI; 162 | export const platform = _platform; 163 | export const userAgent = _userAgent; 164 | 165 | /** 166 | * The language used for the user interface. The format of 167 | * the string is all lower case (e.g. zh-tw for Traditional 168 | * Chinese) 169 | */ 170 | export const language = _language; 171 | 172 | // eslint-disable-next-line @typescript-eslint/no-namespace 173 | export namespace Language { 174 | export function value(): string { 175 | return language; 176 | } 177 | 178 | export function isDefaultVariant(): boolean { 179 | if (language.length === 2) { 180 | return language === 'en'; 181 | } else if (language.length >= 3) { 182 | return language[0] === 'e' && language[1] === 'n' && language[2] === '-'; 183 | } else { 184 | return false; 185 | } 186 | } 187 | 188 | export function isDefault(): boolean { 189 | return language === 'en'; 190 | } 191 | } 192 | 193 | /** 194 | * The OS locale or the locale specified by --locale. The format of 195 | * the string is all lower case (e.g. zh-tw for Traditional 196 | * Chinese). The UI is not necessarily shown in the provided locale. 197 | */ 198 | export const locale = _locale; 199 | 200 | /** 201 | * The translations that are available through language packs. 202 | */ 203 | export const translationsConfigFile = _translationsConfigFile; 204 | 205 | export const setTimeout0IsFaster = 206 | typeof globals.postMessage === 'function' && !globals.importScripts; 207 | 208 | /** 209 | * See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-. 210 | * 211 | * Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay 212 | * that browsers set when the nesting level is > 5. 213 | */ 214 | export const setTimeout0 = (() => { 215 | if (setTimeout0IsFaster) { 216 | interface IQueueElement { 217 | id: number; 218 | callback: () => void; 219 | } 220 | const pending: IQueueElement[] = []; 221 | globals.addEventListener('message', (e: MessageEvent) => { 222 | if (e.data && e.data.vscodeScheduleAsyncWork) { 223 | for (let i = 0, len = pending.length; i < len; i++) { 224 | const candidate = pending[i]; 225 | if (candidate.id === e.data.vscodeScheduleAsyncWork) { 226 | pending.splice(i, 1); 227 | candidate.callback(); 228 | return; 229 | } 230 | } 231 | } 232 | }); 233 | let lastId = 0; 234 | return (callback: () => void) => { 235 | const myId = ++lastId; 236 | pending.push({ 237 | id: myId, 238 | callback: callback, 239 | }); 240 | globals.postMessage({ vscodeScheduleAsyncWork: myId }, '*'); 241 | }; 242 | } 243 | return (callback: () => void) => setTimeout(callback); 244 | })(); 245 | 246 | export const enum OperatingSystem { 247 | Windows = 1, 248 | Macintosh = 2, 249 | Linux = 3, 250 | } 251 | export const OS = 252 | _isMacintosh || _isIOS 253 | ? OperatingSystem.Macintosh 254 | : _isWindows 255 | ? OperatingSystem.Windows 256 | : OperatingSystem.Linux; 257 | 258 | let _isLittleEndian = true; 259 | let _isLittleEndianComputed = false; 260 | export function isLittleEndian(): boolean { 261 | if (!_isLittleEndianComputed) { 262 | _isLittleEndianComputed = true; 263 | const test = new Uint8Array(2); 264 | test[0] = 1; 265 | test[1] = 2; 266 | const view = new Uint16Array(test.buffer); 267 | _isLittleEndian = view[0] === (2 << 8) + 1; 268 | } 269 | return _isLittleEndian; 270 | } 271 | 272 | export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0); 273 | export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0); 274 | export const isSafari = !!(!isChrome && userAgent && userAgent.indexOf('Safari') >= 0); 275 | export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0); 276 | export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0); 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Path Autocomplete for Visual Studio Code 2 | 3 | Provides path completion for visual studio code. 4 | 5 | demo gif 6 | 7 | ## Features 8 | 9 | - it supports relative paths (starting with ./) 10 | - it supports absolute path to the workspace (starting with /) 11 | - it supports absolute path to the file system (starts with: C:) 12 | - it supports paths relative to the user folder (starts with ~) 13 | - it supports partial paths (./tmp/fol will suggest ./tmp/folder1 if it exists) 14 | - it supports items exclusions via the `path-autocomplete.excludedItems` option 15 | - it supports npm packages (starting with a-z and not relative to disk) 16 | - it supports automatic suggestion after selecting a folder 17 | - it supports custom mappings via the `path-autocomplete.pathMappings` option 18 | - it supports conditional path mappings that apply to certain sub-folders only 19 | - it supports custom transformations to the inserted text via the `path-autocomplete.transformations` 20 | - it supports Windows paths with the `path-autocomplete.useBackslash` 21 | - it supports VS Code for Web (including on Windows) 22 | - it supports language specific configurations 23 | 24 | ## Installation 25 | 26 | You can install it from the [marketplace](https://marketplace.visualstudio.com/items?itemName=ionutvmi.path-autocomplete). 27 | `ext install path-autocomplete` 28 | 29 | ## Options 30 | 31 | - `path-autocomplete.extensionOnImport` - boolean If true it will append the extension as well when inserting the file name on `import` or `require` statements. 32 | - `path-autocomplete.includeExtension` - boolean If true it will append the extension as well when inserting the file name. 33 | - `path-autocomplete.excludedItems` 34 | This option allows you to exclude certain files from the suggestions. 35 | 36 | ```jsonc 37 | "path-autocomplete.excludedItems": { 38 | "**/*.js": { "when": "**/*.ts" }, // ignore js files if i'm inside a ts file 39 | "**/*.map": { "when": "**" }, // always ignore *.map files 40 | "**/{.git,node_modules}": { "when": "**" }, // always ignore .git and node_modules folders 41 | "**": { "when": "**", "isDir": true }, // always ignore `folder` suggestions 42 | "**/*.ts": { "when": "**", "context": "import.*" }, // ignore .ts file suggestions in all files when the current line matches the regex from the `context` 43 | } 44 | ``` 45 | 46 | [minimatch](https://www.npmjs.com/package/minimatch) is used to check if the files match the pattern. 47 | 48 | - `path-autocomplete.pathMappings` 49 | Useful for defining aliases for absolute or relative paths. 50 | 51 | ```jsonc 52 | "path-autocomplete.pathMappings": { 53 | "/test": "${folder}/src/Actions/test", // alias for /test 54 | "/": "${folder}/src", // the absolute root folder is now /src, 55 | "$root": "${folder}/src", // the relative root folder is now /src 56 | // or multiple folders for one mapping 57 | "$root": ["${folder}/p1/src", "${folder}/p2/src"] // the root is now relative to both p1/src and p2/src 58 | } 59 | ``` 60 | 61 | In monorepos the following setup could be used: 62 | 63 | ```jsonc 64 | "path-autocomplete.pathMappings": { 65 | "$root": { 66 | "conditions": [ 67 | { 68 | "when": "**/packages/math/**", 69 | "value": "${folder}/packages/math" 70 | }, 71 | { 72 | "when": "**/packages/ui/**", 73 | "value": "${folder}/packages/ui" 74 | } 75 | ] 76 | } 77 | }, 78 | ``` 79 | 80 | Supported variables: 81 | | Name | Description | 82 | |------|-------------| 83 | | ${home} | User home folder | 84 | | ${folder} | The root folder of the current file | 85 | | ${workspace} | The root folder of the current workspace | 86 | | ${fileDirname} | The directory of the current file | 87 | | ${relativeFileDirname} | The current opened file's dirname relative to workspaceFolder | 88 | 89 | - `path-autocomplete.pathSeparators` - string Lists the separators used for extracting the inserted path when used outside strings. 90 | The default value is: ` \t({[` 91 | 92 | - `path-autocomplete.transformations` 93 | List of custom transformation applied to the inserted text. 94 | Example: replace `_` with an empty string when selecting a SCSS partial file. 95 | 96 | ```jsonc 97 | "path-autocomplete.transformations": [ 98 | { 99 | "type": "replace", 100 | "parameters": ["^_", ""], 101 | "when": { 102 | "fileName": "\\.scss$" 103 | } 104 | }, 105 | 106 | // replace spaces with %20 107 | { 108 | "type": "replace", 109 | "parameters": [ " ", "%20", "g" ], 110 | "when": { 111 | "path": ".*routes" 112 | } 113 | }, 114 | 115 | // replace %20 with spaces when reading the already inserted path 116 | { 117 | "type": "inputReplace", 118 | "parameters": [ "%20", " ", "g" ], 119 | "when": { 120 | "path": ".*routes" 121 | } 122 | }, 123 | 124 | // useful if extensionOnImport is true 125 | { 126 | "type": "replace", 127 | "parameters": [ 128 | "\\.\\w+$", 129 | "" 130 | ], 131 | "when": { 132 | "fileName": "\\.(ts|tsx|js|jsx)$" 133 | } 134 | } 135 | ], 136 | ``` 137 | 138 | Supported transformation: 139 | 140 | - `replace` - Performs a string replace on the selected item text. 141 | Parameters: 142 | 143 | - `regex` - a regex pattern 144 | - `replaceString` - the replacement string 145 | - `modifiers` - modifiers passed to the RegExp constructor 146 | 147 | - `inputReplace` - Performs a string replace on the input path. 148 | Parameters: 149 | 150 | - `regex` - a regex pattern 151 | - `replaceString` - the replacement string 152 | - `modifiers` - modifiers passed to the RegExp constructor 153 | 154 | The `fileName` and `path` can be used for filtering the items/instances where the transformation should be applied. 155 | 156 | For the `replace` transformation considering we selected `/home/mihai/a.txt`: 157 | 158 | - `fileName` - regex applied to the basename of the selected suggestion `a.txt` 159 | - `path` - regex applied to the the full path of the selected suggestion `/home/mihai/a.txt` 160 | 161 | For the `inputReplace` transformation considering that what we typed so far is `/home/mihai`: 162 | 163 | - `path` - regex applied to the path inserted so far `/home/mihai` 164 | 165 | - `path-autocomplete.triggerOutsideStrings` boolean - if true it will trigger the autocomplete outside of quotes 166 | - `path-autocomplete.enableFolderTrailingSlash` boolean - if true it will add a slash after the insertion of a folder path that will trigger the autocompletion. 167 | - `path-autocomplete.disableUpOneFolder` boolean - disables the up one folder (..) element from the completion list. 168 | - `path-autocomplete.useBackslash` boolean - if true it will use `\\` when iserting the paths. 169 | - `path-autocomplete.useSingleBackslash` boolean - If enabled it will insert a single backslash (\\) even inside quoted strings. 170 | - `path-autocomplete.ignoredFilesPattern` - string - Glob patterns for disabling the path completion in the specified file types. Example: "\*_/_.{css,scss}" 171 | - `path-autocomplete.ignoredPrefixes` array - list of ignored prefixes to disable suggestions 172 | on certain preceeding words/characters. 173 | Example: 174 | ```js 175 | "path-autocomplete.ignoredPrefixes": [ 176 | "//" // type double slash and no suggesstions will be displayed 177 | ] 178 | ``` 179 | - 180 | 181 | ## Language specific configurations 182 | 183 | All settings can be overwritten by language specific configurations. 184 | 185 | ```jsonc 186 | "path-autocomplete.extensionOnImport": false, 187 | "[typescript]": { 188 | "path-autocomplete.extensionOnImport": false, 189 | }, 190 | ``` 191 | 192 | ## Configure VSCode to recognize path aliases 193 | 194 | VSCode doesn't automatically recognize path aliases so you cannot alt+click to open files. To fix this you need to create `jsconfig.json` or `tsconfig.json` to the root of your project and define your alises. An example configuration: 195 | 196 | ``` 197 | { 198 | "compilerOptions": { 199 | "target": "esnext", // define to your liking 200 | "baseUrl": "./", 201 | "paths": { 202 | "test/*": ["src/actions/test"], 203 | "assets/*": ["src/assets"] 204 | } 205 | }, 206 | "exclude": ["node_modules"] // Optional 207 | } 208 | ``` 209 | 210 | ## Tips 211 | 212 | - if you want to use this in markdown or simple text files you need to enable `path-autocomplete.triggerOutsideStrings` 213 | 214 | - `./` for relative paths 215 | 216 | > If `./` doesn't work properly, add this to `keybindings.json`: `{ "key": ".", "command": "" }`. Refer to https://github.com/ChristianKohler/PathIntellisense/issues/9 217 | 218 | - When I use aliases I can't jump to imported file on Ctrl + Click 219 | > This is controlled by the compiler options in jsconfig.json. You can create the JSON file in your project root and add paths for your aliases. 220 | > jsconfig.json Reference 221 | > https://code.visualstudio.com/docs/languages/jsconfig#_using-webpack-aliases 222 | - if you have issues with duplicate suggestions please use the `path-autocomplete.ignoredFilesPattern` option to disable the path autocomplete in certain file types 223 | - if you need more fine grained control for handing duplicate items you can use the `path-autocomplete.excludedItems` option as follows: 224 | 225 | ```jsonc 226 | // disable all suggestions in HTML files, when the current line contains the href or src attributes 227 | "path-autocomplete.excludedItems": { 228 | "**": { 229 | "when": "**/*.html", 230 | "context": "(src|href)=.*" 231 | } 232 | }, 233 | 234 | // for js and typescript you can disable the vscode suggestions using the following options 235 | "javascript.suggest.paths": false, 236 | "typescript.suggest.paths": false 237 | ``` 238 | 239 | ## Release notes 240 | 241 | The release notes are available in the [CHANGELOG.md](CHANGELOG.md) file. 242 | 243 | ## Author 244 | 245 | Mihai Ionut Vilcu 246 | 247 | - [github/mihai-vlc](https://github.com/mihai-vlc) 248 | - [twitter/mihai_vlc](http://twitter.com/mihai_vlc) 249 | 250 | ## Credits 251 | 252 | This extension is based on [path-intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) 253 | -------------------------------------------------------------------------------- /src/features/PathAutocompleteProvider.ts: -------------------------------------------------------------------------------- 1 | import vs from 'vscode'; 2 | import minimatch from 'minimatch'; 3 | import { FileInfo } from './FileInfo'; 4 | import PathConfiguration from './PathConfiguration'; 5 | import { isDirectory, pathExists, readDirectory } from './FsUtils'; 6 | import { normalizeForBrowser } from './Normalize'; 7 | 8 | import * as path from '../util/path'; 9 | 10 | interface MappingItem { 11 | currentDir: string; 12 | insertedPath: string; 13 | } 14 | 15 | const configuration = PathConfiguration.configuration; 16 | 17 | export class PathAutocomplete implements vs.CompletionItemProvider { 18 | private currentFile: string; 19 | private currentLine: string; 20 | private currentPosition: number; 21 | private namePrefix: string; 22 | 23 | async provideCompletionItems( 24 | document: vs.TextDocument, 25 | position: vs.Position, 26 | _token: vs.CancellationToken, 27 | ): Promise { 28 | configuration.update(document.uri, document.languageId); 29 | 30 | this.currentFile = normalizeForBrowser(document.fileName); 31 | const currentLine = document.getText(document.lineAt(position).range); 32 | this.currentLine = currentLine; 33 | this.currentPosition = position.character; 34 | this.namePrefix = this.getNamePrefix(); 35 | 36 | if (!this.shouldProvide()) { 37 | return []; 38 | } 39 | 40 | const useBackslash = this.shouldUseBackslash(); 41 | 42 | const foldersPath = await this.getFoldersPath(this.currentFile, currentLine, position.character); 43 | 44 | if (foldersPath.length === 0) { 45 | return []; 46 | } 47 | 48 | const folderItems = await this.getFolderItems(foldersPath); 49 | 50 | // build the list of the completion items 51 | const result = folderItems.filter(this.filter, this).map((file) => { 52 | const insertText = this.getInsertText(file); 53 | const completion = new vs.CompletionItem(insertText); 54 | 55 | // correct suggestion Item icon, ref issue#100 56 | completion.detail = file.path; 57 | completion.insertText = insertText; 58 | 59 | // show folders before files 60 | if (file.isDirectory) { 61 | if (useBackslash) { 62 | completion.label += '\\'; 63 | } else { 64 | completion.label += '/'; 65 | } 66 | 67 | if (configuration.data.enableFolderTrailingSlash) { 68 | let commandText = '/'; 69 | 70 | if (useBackslash) { 71 | if (this.shouldUseSingleBackslash()) { 72 | commandText = '\\'; 73 | } else { 74 | commandText = this.isInsideQuotes() ? '\\\\' : '\\'; 75 | } 76 | } 77 | 78 | completion.command = { 79 | command: 'default:type', 80 | title: 'triggerSuggest', 81 | arguments: [ 82 | { 83 | text: commandText, 84 | }, 85 | ], 86 | }; 87 | } 88 | 89 | completion.sortText = 'd'; 90 | completion.kind = vs.CompletionItemKind.Folder; 91 | } else { 92 | completion.sortText = 'f'; 93 | completion.kind = vs.CompletionItemKind.File; 94 | } 95 | 96 | // this is deprecated but still needed for the completion to work 97 | // in json files 98 | completion.textEdit = new vs.TextEdit( 99 | new vs.Range(position, position), 100 | completion.insertText, 101 | ); 102 | 103 | return completion; 104 | }); 105 | 106 | // add the `up one folder` item 107 | if (!configuration.data.disableUpOneFolder) { 108 | result.unshift(new vs.CompletionItem('..')); 109 | } 110 | 111 | return result; 112 | } 113 | 114 | /** 115 | * Determines if the current completion item should use backshash or forward slash. 116 | */ 117 | shouldUseBackslash(): boolean { 118 | if (configuration.data.useBackslash) { 119 | return true; 120 | } 121 | 122 | const userPath = this.getUserPath(this.currentLine, this.currentPosition); 123 | const pathParts = userPath.split(/\\|\//); 124 | const backslashParts = userPath.split('\\'); 125 | 126 | // check if backslash is the path separator for the current path 127 | if (userPath.indexOf('\\') > -1 && pathParts.length === backslashParts.length) { 128 | return true; 129 | } 130 | 131 | return false; 132 | } 133 | 134 | /** 135 | * Determines if a single backslash should be used in the current string 136 | */ 137 | shouldUseSingleBackslash() { 138 | if (configuration.data.useSingleBackslash) { 139 | return true; 140 | } 141 | 142 | // attempt to detect raw strings that don't need double backslash 143 | // example: r"C:\work" 144 | if (this.isInsideQuotes()) { 145 | const currentLine = this.currentLine; 146 | const position = this.currentPosition; 147 | 148 | for (let i = position; i > 0; i--) { 149 | const c = currentLine[i]; 150 | const isQuote = c === '"' || c === "'" || c === '`'; 151 | if (i > 0 && isQuote && currentLine[i - 1] === 'r') { 152 | return true; 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | } 159 | 160 | /** 161 | * Gets the name prefix for the completion item. 162 | * This is used when the path that the user typed so far 163 | * contains part of the file/folder name 164 | * Examples: 165 | * /folder/Fi => complete path is /folder/File => will return Fi 166 | * /folder/subfo => complete path is /folder/subfolder => will return subfo 167 | */ 168 | getNamePrefix(): string { 169 | const userPath = this.getUserPath(this.currentLine, this.currentPosition); 170 | if (userPath.endsWith('/') || userPath.endsWith('\\')) { 171 | return ''; 172 | } 173 | 174 | return path.basename(userPath); 175 | } 176 | 177 | /** 178 | * Determines if the file extension should be included in the selected options when 179 | * the selection is made. 180 | */ 181 | isExtensionEnabled(): boolean { 182 | if (this.currentLine.match(/require|import/)) { 183 | return configuration.data.withExtensionOnImport; 184 | } 185 | 186 | return configuration.data.withExtension; 187 | } 188 | 189 | getInsertText(file: FileInfo): string { 190 | let insertText = ''; 191 | 192 | if (this.isExtensionEnabled() || file.isDirectory) { 193 | insertText = file.name; 194 | } else { 195 | // remove the extension 196 | insertText = path.basename(file.name, path.extname(file.name)); 197 | } 198 | 199 | if ( 200 | !this.namePrefix && 201 | this.shouldUseBackslash() && 202 | this.isInsideQuotes() && 203 | !this.shouldUseSingleBackslash() 204 | ) { 205 | // determine if we should insert an additional backslash 206 | if (this.currentLine[this.currentPosition - 2] !== '\\') { 207 | insertText = '\\' + insertText; 208 | } 209 | } 210 | 211 | // apply the transformations 212 | configuration.data.transformations.forEach((transform) => { 213 | const fileNameRegex = 214 | transform.when && transform.when.fileName && new RegExp(transform.when.fileName); 215 | if (fileNameRegex && !file.name.match(fileNameRegex)) { 216 | return; 217 | } 218 | const pathRegex = transform.when && transform.when.path && new RegExp(transform.when.path); 219 | if (pathRegex && !file.path.match(pathRegex)) { 220 | return; 221 | } 222 | 223 | const parameters = transform.parameters || []; 224 | if (transform.type === 'replace' && parameters[0]) { 225 | insertText = String.prototype.replace.call( 226 | insertText, 227 | new RegExp(parameters[0], parameters[2]), 228 | parameters[1], 229 | ); 230 | } 231 | }); 232 | 233 | if (this.namePrefix) { 234 | insertText = insertText.substring(this.namePrefix.length); 235 | } 236 | 237 | return insertText; 238 | } 239 | 240 | /** 241 | * Builds a list of the available files and folders from the provided path. 242 | */ 243 | async getFolderItems(foldersPath: string[]): Promise { 244 | const getFileInfoPromises = foldersPath.map(async (folderPath) => { 245 | const fileTuples = await readDirectory(folderPath); 246 | return Promise.all( 247 | fileTuples.map(async (fileTuple) => { 248 | const filePath = path.join(folderPath, fileTuple[0]); 249 | try { 250 | const isDir = fileTuple[1] === vs.FileType.Directory; 251 | return new FileInfo(filePath, isDir ? 'dir' : 'file'); 252 | } catch (err) { 253 | // silently ignore permissions errors 254 | console.error(err); 255 | } 256 | }), 257 | ); 258 | }); 259 | const fileInfosArray = await Promise.all(getFileInfoPromises); 260 | return fileInfosArray.flat().filter((record) => { 261 | // in case of a file permission error we need to keep only valid 262 | // FileInfo objects in the result 263 | return Boolean(record); 264 | }); 265 | } 266 | 267 | /** 268 | * Builds the current folder path based on the current file and the path from 269 | * the current line. 270 | * 271 | */ 272 | async getFoldersPath( 273 | fileName: string, 274 | currentLine: string, 275 | currentPosition: number, 276 | ): Promise { 277 | const userPath = this.getUserPath(currentLine, currentPosition); 278 | const mappingResult = this.applyMapping(userPath); 279 | const promises = mappingResult.items 280 | .map((item) => { 281 | const insertedPath = item.insertedPath; 282 | const currentDir = item.currentDir || this.getCurrentDirectory(fileName, insertedPath); 283 | 284 | // relative to the disk 285 | if (insertedPath.match(/^[a-z]:/i)) { 286 | return [insertedPath]; 287 | } 288 | 289 | // user folder 290 | if (insertedPath.startsWith('~')) { 291 | return [path.join(configuration.data.homeDirectory, insertedPath.substring(1))]; 292 | } 293 | 294 | return [path.join(currentDir, insertedPath)]; 295 | }) 296 | // merge the resulted path 297 | .flat() 298 | // keep only folders 299 | .map(async (folderPath) => { 300 | if (!folderPath.endsWith('/') && !folderPath.endsWith('\\')) { 301 | const isDirPath = await isDirectory(folderPath); 302 | if (!isDirPath) { 303 | folderPath = path.dirname(folderPath); 304 | } 305 | } 306 | 307 | const item = { 308 | folderPath, 309 | valid: true, 310 | }; 311 | 312 | if (!(await pathExists(item.folderPath)) || !(await isDirectory(item.folderPath))) { 313 | item.valid = false; 314 | } 315 | 316 | return item; 317 | }); 318 | 319 | const items = await Promise.all(promises); 320 | const foldersPath = []; 321 | for (const item of items) { 322 | if (item.valid) { 323 | foldersPath.push(item.folderPath); 324 | } 325 | } 326 | return foldersPath; 327 | } 328 | 329 | /** 330 | * Retrieves the path inserted by the user. This is taken based on the last quote or last white space character. 331 | * 332 | * @param currentLine The current line of the cursor. 333 | * @param currentPosition The current position of the cursor. 334 | */ 335 | getUserPath(currentLine: string, currentPosition: number): string { 336 | let lastQuote = -1; 337 | let lastSeparator = -1; 338 | const pathSeparators = configuration.data.pathSeparators.split(''); 339 | 340 | for (let i = 0; i < currentPosition; i++) { 341 | const c = currentLine[i]; 342 | 343 | // skip next character if escaped 344 | if (c === '\\') { 345 | i++; 346 | continue; 347 | } 348 | 349 | // handle separators for support outside strings 350 | if (pathSeparators.indexOf(c) > -1) { 351 | lastSeparator = i; 352 | continue; 353 | } 354 | 355 | // handle quotes 356 | if (c === "'" || c === '"' || c === '`') { 357 | lastQuote = i; 358 | } 359 | } 360 | 361 | const startPosition = lastQuote !== -1 ? lastQuote : lastSeparator; 362 | let userPath = currentLine.substring(startPosition + 1, currentPosition); 363 | 364 | // apply the transformations 365 | configuration.data.transformations.forEach((transform) => { 366 | const pathRegex = transform.when && transform.when.path && new RegExp(transform.when.path); 367 | if (pathRegex && !userPath.match(pathRegex)) { 368 | return; 369 | } 370 | 371 | const parameters = transform.parameters || []; 372 | if (transform.type === 'inputReplace' && parameters[0]) { 373 | userPath = String.prototype.replace.call( 374 | userPath, 375 | new RegExp(parameters[0], parameters[2]), 376 | parameters[1], 377 | ); 378 | } 379 | }); 380 | 381 | return userPath; 382 | } 383 | 384 | /** 385 | * Returns the current working directory 386 | */ 387 | getCurrentDirectory(fileName: string, insertedPath: string): string { 388 | let currentDir = path.parse(fileName).dir || '/'; 389 | const workspacePath = configuration.data.workspaceFolderPath; 390 | 391 | // based on the project root 392 | if (insertedPath.startsWith('/') && workspacePath) { 393 | currentDir = workspacePath; 394 | } 395 | 396 | return path.resolve(currentDir); 397 | } 398 | 399 | /** 400 | * Applies the folder mappings based on the user configurations 401 | */ 402 | applyMapping(insertedPath: string): { items: MappingItem[] } { 403 | const workspaceFolderPath = configuration.data.workspaceFolderPath; 404 | const workspaceRootPath = configuration.data.workspaceRootPath; 405 | const items = []; 406 | 407 | Object.keys(configuration.data.pathMappings || {}) 408 | // if insertedPath is '@view/' 409 | // and mappings is [{key: '@', ...}, {key: '@view', ...}] 410 | // and it will match '@' and return wrong items { currentDir: 'xxx', insertedPath: 'view/'} 411 | // solution : Sort keys by matching longest prefix, and it will match key(@view) first 412 | .sort((key1, key2) => { 413 | const f1 = insertedPath.startsWith(key1) ? key1.length : 0; 414 | const f2 = insertedPath.startsWith(key2) ? key2.length : 0; 415 | return f2 - f1; 416 | }) 417 | .filter((key) => { 418 | const candidate = configuration.data.pathMappings[key]; 419 | if (typeof candidate == 'string' || Array.isArray(candidate)) { 420 | return true; 421 | } 422 | 423 | if (typeof candidate == 'object' && Array.isArray(candidate.conditions)) { 424 | /* 425 | "conditions": [ 426 | { 427 | "when": "packages/math/**", 428 | "value": "${folder}/packages/math" 429 | } 430 | ] 431 | */ 432 | return candidate.conditions.some((condition) => { 433 | return minimatch(this.currentFile, condition.when); 434 | }); 435 | } 436 | 437 | return false; 438 | }) 439 | .map((key) => { 440 | let candidatePaths = configuration.data.pathMappings[key]; 441 | 442 | // normalize candidate paths 443 | if (typeof candidatePaths == 'object' && Array.isArray(candidatePaths.conditions)) { 444 | const matchingCandidate = candidatePaths.conditions.find((condition) => 445 | minimatch(this.currentFile, condition.when), 446 | ); 447 | candidatePaths = matchingCandidate.value; 448 | } 449 | 450 | if (typeof candidatePaths == 'string') { 451 | candidatePaths = [candidatePaths]; 452 | } 453 | 454 | return candidatePaths.map((candidatePath) => { 455 | if (workspaceRootPath) { 456 | candidatePath = candidatePath.replace('${workspace}', workspaceRootPath); 457 | } 458 | 459 | if (workspaceFolderPath) { 460 | candidatePath = candidatePath.replace('${folder}', workspaceFolderPath); 461 | } 462 | 463 | candidatePath = candidatePath.replace('${home}', configuration.data.homeDirectory); 464 | 465 | if (configuration.data.fileDirname) { 466 | candidatePath = candidatePath.replace( 467 | '${fileDirname}', 468 | configuration.data.fileDirname, 469 | ); 470 | } 471 | 472 | if (configuration.data.relativeFileDirname) { 473 | candidatePath = candidatePath.replace( 474 | '${relativeFileDirname}', 475 | configuration.data.relativeFileDirname, 476 | ); 477 | } 478 | 479 | return { 480 | key: key, 481 | path: candidatePath, 482 | }; 483 | }); 484 | }) 485 | .some((mappings) => { 486 | let found = false; 487 | 488 | mappings.forEach((mapping) => { 489 | if ( 490 | insertedPath.startsWith(mapping.key) || 491 | (mapping.key === '$root' && !insertedPath.startsWith('.')) 492 | ) { 493 | items.push({ 494 | currentDir: mapping.path, 495 | insertedPath: insertedPath.replace(mapping.key, ''), 496 | }); 497 | found = true; 498 | } 499 | }); 500 | 501 | // stop after the first mapping found 502 | return found; 503 | }); 504 | 505 | // no mapping was found, use the raw path inserted by the user 506 | if (items.length === 0) { 507 | items.push({ 508 | currentDir: '', 509 | insertedPath, 510 | }); 511 | } 512 | return { items }; 513 | } 514 | 515 | /** 516 | * Determine if we should provide path completion. 517 | */ 518 | shouldProvide() { 519 | if ( 520 | configuration.data.ignoredFilesPattern && 521 | minimatch(this.currentFile, configuration.data.ignoredFilesPattern) 522 | ) { 523 | return false; 524 | } 525 | 526 | if (this.isIgnoredPrefix()) { 527 | return false; 528 | } 529 | 530 | if (configuration.data.triggerOutsideStrings) { 531 | return true; 532 | } 533 | 534 | return this.isInsideQuotes(); 535 | } 536 | 537 | /** 538 | * Determines if the prefix of the path is in the ignored list 539 | */ 540 | isIgnoredPrefix() { 541 | const ignoredPrefixes = configuration.data.ignoredPrefixes; 542 | 543 | if (!ignoredPrefixes || ignoredPrefixes.length === 0) { 544 | return false; 545 | } 546 | 547 | return ignoredPrefixes.some((prefix) => { 548 | const currentLine = this.currentLine; 549 | const position = this.currentPosition; 550 | 551 | if (prefix.length > currentLine.length) { 552 | return false; 553 | } 554 | 555 | const candidate = currentLine.substring(position - prefix.length, position); 556 | 557 | if (prefix === candidate) { 558 | return true; 559 | } 560 | 561 | return false; 562 | }); 563 | } 564 | 565 | /** 566 | * Determines if the cursor is inside quotes. 567 | */ 568 | isInsideQuotes(): boolean { 569 | const currentLine = this.currentLine; 570 | const position = this.currentPosition; 571 | const quotes = { 572 | single: 0, 573 | double: 0, 574 | backtick: 0, 575 | }; 576 | 577 | // check if we are inside quotes 578 | for (let i = 0; i < position; i++) { 579 | if (currentLine.charAt(i) === "'" && currentLine.charAt(i - 1) !== '\\') { 580 | quotes.single += quotes.single > 0 ? -1 : 1; 581 | } 582 | 583 | if (currentLine.charAt(i) === '"' && currentLine.charAt(i - 1) !== '\\') { 584 | quotes.double += quotes.double > 0 ? -1 : 1; 585 | } 586 | 587 | if (currentLine.charAt(i) === '`' && currentLine.charAt(i - 1) !== '\\') { 588 | quotes.backtick += quotes.backtick > 0 ? -1 : 1; 589 | } 590 | } 591 | 592 | return !!(quotes.single || quotes.double || quotes.backtick); 593 | } 594 | 595 | /** 596 | * Filter for the suggested items 597 | */ 598 | filter(suggestionFile: FileInfo) { 599 | // no options configured 600 | if (!configuration.data.excludedItems || typeof configuration.data.excludedItems != 'object') { 601 | return true; 602 | } 603 | 604 | // keep only the records that match the name prefix inserted by the user 605 | if (this.namePrefix && suggestionFile.name.indexOf(this.namePrefix) !== 0) { 606 | return false; 607 | } 608 | 609 | const currentFile = this.currentFile; 610 | const currentLine = this.currentLine; 611 | 612 | return Object.entries(configuration.data.excludedItems).every(([item, exclusion]) => { 613 | // check the local file name pattern 614 | if (!minimatch(currentFile, exclusion.when)) { 615 | return true; 616 | } 617 | 618 | if (!minimatch(suggestionFile.path, item)) { 619 | return true; 620 | } 621 | 622 | // check the local line context 623 | if (exclusion.context) { 624 | const contextRegex = new RegExp(exclusion.context); 625 | if (!contextRegex.test(currentLine)) { 626 | return true; 627 | } 628 | } 629 | 630 | // exclude folders from the results 631 | if (exclusion.isDir && !suggestionFile.isDirectory) { 632 | return true; 633 | } 634 | 635 | return false; 636 | }); 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /src/util/path.ts: -------------------------------------------------------------------------------- 1 | import * as process from './process'; 2 | 3 | const CHAR_UPPERCASE_A = 65; /* A */ 4 | const CHAR_LOWERCASE_A = 97; /* a */ 5 | const CHAR_UPPERCASE_Z = 90; /* Z */ 6 | const CHAR_LOWERCASE_Z = 122; /* z */ 7 | const CHAR_DOT = 46; /* . */ 8 | const CHAR_FORWARD_SLASH = 47; /* / */ 9 | const CHAR_BACKWARD_SLASH = 92; /* \ */ 10 | const CHAR_COLON = 58; /* : */ 11 | const CHAR_QUESTION_MARK = 63; /* ? */ 12 | 13 | class ErrorInvalidArgType extends Error { 14 | code: 'ERR_INVALID_ARG_TYPE'; 15 | constructor(name: string, expected: string, actual: unknown) { 16 | // determiner: 'must be' or 'must not be' 17 | let determiner; 18 | if (typeof expected === 'string' && expected.indexOf('not ') === 0) { 19 | determiner = 'must not be'; 20 | expected = expected.replace(/^not /, ''); 21 | } else { 22 | determiner = 'must be'; 23 | } 24 | 25 | const type = name.indexOf('.') !== -1 ? 'property' : 'argument'; 26 | let msg = `The "${name}" ${type} ${determiner} of type ${expected}`; 27 | 28 | msg += `. Received type ${typeof actual}`; 29 | super(msg); 30 | 31 | this.code = 'ERR_INVALID_ARG_TYPE'; 32 | } 33 | } 34 | 35 | function validateString(value: string, name: string) { 36 | if (typeof value !== 'string') { 37 | throw new ErrorInvalidArgType(name, 'string', value); 38 | } 39 | } 40 | 41 | function isPathSeparator(code: number | undefined) { 42 | return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; 43 | } 44 | 45 | function isPosixPathSeparator(code: number | undefined) { 46 | return code === CHAR_FORWARD_SLASH; 47 | } 48 | 49 | function isWindowsDeviceRoot(code: number) { 50 | return ( 51 | (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) || 52 | (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) 53 | ); 54 | } 55 | 56 | // Resolves . and .. elements in a path with directory names 57 | function normalizeString( 58 | path: string, 59 | allowAboveRoot: boolean, 60 | separator: string, 61 | isPathSeparator: (code?: number) => boolean, 62 | ) { 63 | let res = ''; 64 | let lastSegmentLength = 0; 65 | let lastSlash = -1; 66 | let dots = 0; 67 | let code = 0; 68 | for (let i = 0; i <= path.length; ++i) { 69 | if (i < path.length) { 70 | code = path.charCodeAt(i); 71 | } else if (isPathSeparator(code)) { 72 | break; 73 | } else { 74 | code = CHAR_FORWARD_SLASH; 75 | } 76 | 77 | if (isPathSeparator(code)) { 78 | if (lastSlash === i - 1 || dots === 1) { 79 | // NOOP 80 | } else if (dots === 2) { 81 | if ( 82 | res.length < 2 || 83 | lastSegmentLength !== 2 || 84 | res.charCodeAt(res.length - 1) !== CHAR_DOT || 85 | res.charCodeAt(res.length - 2) !== CHAR_DOT 86 | ) { 87 | if (res.length > 2) { 88 | const lastSlashIndex = res.lastIndexOf(separator); 89 | if (lastSlashIndex === -1) { 90 | res = ''; 91 | lastSegmentLength = 0; 92 | } else { 93 | res = res.slice(0, lastSlashIndex); 94 | lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); 95 | } 96 | lastSlash = i; 97 | dots = 0; 98 | continue; 99 | } else if (res.length !== 0) { 100 | res = ''; 101 | lastSegmentLength = 0; 102 | lastSlash = i; 103 | dots = 0; 104 | continue; 105 | } 106 | } 107 | if (allowAboveRoot) { 108 | res += res.length > 0 ? `${separator}..` : '..'; 109 | lastSegmentLength = 2; 110 | } 111 | } else { 112 | if (res.length > 0) { 113 | res += `${separator}${path.slice(lastSlash + 1, i)}`; 114 | } else { 115 | res = path.slice(lastSlash + 1, i); 116 | } 117 | lastSegmentLength = i - lastSlash - 1; 118 | } 119 | lastSlash = i; 120 | dots = 0; 121 | } else if (code === CHAR_DOT && dots !== -1) { 122 | ++dots; 123 | } else { 124 | dots = -1; 125 | } 126 | } 127 | return res; 128 | } 129 | 130 | export interface ParsedPath { 131 | root: string; 132 | dir: string; 133 | base: string; 134 | ext: string; 135 | name: string; 136 | } 137 | 138 | function _format(sep: string, pathObject: ParsedPath) { 139 | if (pathObject === null || typeof pathObject !== 'object') { 140 | throw new ErrorInvalidArgType('pathObject', 'Object', pathObject); 141 | } 142 | const dir = pathObject.dir || pathObject.root; 143 | const base = pathObject.base || `${pathObject.name || ''}${pathObject.ext || ''}`; 144 | if (!dir) { 145 | return base; 146 | } 147 | return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`; 148 | } 149 | 150 | export interface IPath { 151 | normalize(path: string): string; 152 | isAbsolute(path: string): boolean; 153 | join(...paths: string[]): string; 154 | resolve(...pathSegments: string[]): string; 155 | relative(from: string, to: string): string; 156 | dirname(path: string): string; 157 | basename(path: string, ext?: string): string; 158 | extname(path: string): string; 159 | format(pathObject: ParsedPath): string; 160 | parse(path: string): ParsedPath; 161 | toNamespacedPath(path: string): string; 162 | sep: '\\' | '/'; 163 | delimiter: string; 164 | win32: IPath | null; 165 | posix: IPath | null; 166 | } 167 | 168 | export const win32: IPath = { 169 | // path.resolve([from ...], to) 170 | resolve(...pathSegments: string[]): string { 171 | let resolvedDevice = ''; 172 | let resolvedTail = ''; 173 | let resolvedAbsolute = false; 174 | 175 | for (let i = pathSegments.length - 1; i >= -1; i--) { 176 | let path; 177 | if (i >= 0) { 178 | path = pathSegments[i]; 179 | validateString(path, 'path'); 180 | 181 | // Skip empty entries 182 | if (path.length === 0) { 183 | continue; 184 | } 185 | } else if (resolvedDevice.length === 0) { 186 | path = process.cwd(); 187 | } else { 188 | // Windows has the concept of drive-specific current working 189 | // directories. If we've resolved a drive letter but not yet an 190 | // absolute path, get cwd for that drive, or the process cwd if 191 | // the drive cwd is not available. We're sure the device is not 192 | // a UNC path at this points, because UNC paths are always absolute. 193 | path = process.env[`=${resolvedDevice}`] || process.cwd(); 194 | 195 | // Verify that a cwd was found and that it actually points 196 | // to our drive. If not, default to the drive's root. 197 | if ( 198 | path === undefined || 199 | (path.slice(0, 2).toLowerCase() !== resolvedDevice.toLowerCase() && 200 | path.charCodeAt(2) === CHAR_BACKWARD_SLASH) 201 | ) { 202 | path = `${resolvedDevice}\\`; 203 | } 204 | } 205 | 206 | const len = path.length; 207 | let rootEnd = 0; 208 | let device = ''; 209 | let isAbsolute = false; 210 | const code = path.charCodeAt(0); 211 | 212 | // Try to match a root 213 | if (len === 1) { 214 | if (isPathSeparator(code)) { 215 | // `path` contains just a path separator 216 | rootEnd = 1; 217 | isAbsolute = true; 218 | } 219 | } else if (isPathSeparator(code)) { 220 | // Possible UNC root 221 | 222 | // If we started with a separator, we know we at least have an 223 | // absolute path of some kind (UNC or otherwise) 224 | isAbsolute = true; 225 | 226 | if (isPathSeparator(path.charCodeAt(1))) { 227 | // Matched double path separator at beginning 228 | let j = 2; 229 | let last = j; 230 | // Match 1 or more non-path separators 231 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 232 | j++; 233 | } 234 | if (j < len && j !== last) { 235 | const firstPart = path.slice(last, j); 236 | // Matched! 237 | last = j; 238 | // Match 1 or more path separators 239 | while (j < len && isPathSeparator(path.charCodeAt(j))) { 240 | j++; 241 | } 242 | if (j < len && j !== last) { 243 | // Matched! 244 | last = j; 245 | // Match 1 or more non-path separators 246 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 247 | j++; 248 | } 249 | if (j === len || j !== last) { 250 | // We matched a UNC root 251 | device = `\\\\${firstPart}\\${path.slice(last, j)}`; 252 | rootEnd = j; 253 | } 254 | } 255 | } 256 | } else { 257 | rootEnd = 1; 258 | } 259 | } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) { 260 | // Possible device root 261 | device = path.slice(0, 2); 262 | rootEnd = 2; 263 | if (len > 2 && isPathSeparator(path.charCodeAt(2))) { 264 | // Treat separator following drive name as an absolute path 265 | // indicator 266 | isAbsolute = true; 267 | rootEnd = 3; 268 | } 269 | } 270 | 271 | if (device.length > 0) { 272 | if (resolvedDevice.length > 0) { 273 | if (device.toLowerCase() !== resolvedDevice.toLowerCase()) { 274 | // This path points to another device so it is not applicable 275 | continue; 276 | } 277 | } else { 278 | resolvedDevice = device; 279 | } 280 | } 281 | 282 | if (resolvedAbsolute) { 283 | if (resolvedDevice.length > 0) { 284 | break; 285 | } 286 | } else { 287 | resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`; 288 | resolvedAbsolute = isAbsolute; 289 | if (isAbsolute && resolvedDevice.length > 0) { 290 | break; 291 | } 292 | } 293 | } 294 | 295 | // At this point the path should be resolved to a full absolute path, 296 | // but handle relative paths to be safe (might happen when process.cwd() 297 | // fails) 298 | 299 | // Normalize the tail path 300 | resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute, '\\', isPathSeparator); 301 | 302 | return resolvedAbsolute 303 | ? `${resolvedDevice}\\${resolvedTail}` 304 | : `${resolvedDevice}${resolvedTail}` || '.'; 305 | }, 306 | 307 | normalize(path: string): string { 308 | validateString(path, 'path'); 309 | const len = path.length; 310 | if (len === 0) { 311 | return '.'; 312 | } 313 | let rootEnd = 0; 314 | let device; 315 | let isAbsolute = false; 316 | const code = path.charCodeAt(0); 317 | 318 | // Try to match a root 319 | if (len === 1) { 320 | // `path` contains just a single char, exit early to avoid 321 | // unnecessary work 322 | return isPosixPathSeparator(code) ? '\\' : path; 323 | } 324 | if (isPathSeparator(code)) { 325 | // Possible UNC root 326 | 327 | // If we started with a separator, we know we at least have an absolute 328 | // path of some kind (UNC or otherwise) 329 | isAbsolute = true; 330 | 331 | if (isPathSeparator(path.charCodeAt(1))) { 332 | // Matched double path separator at beginning 333 | let j = 2; 334 | let last = j; 335 | // Match 1 or more non-path separators 336 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 337 | j++; 338 | } 339 | if (j < len && j !== last) { 340 | const firstPart = path.slice(last, j); 341 | // Matched! 342 | last = j; 343 | // Match 1 or more path separators 344 | while (j < len && isPathSeparator(path.charCodeAt(j))) { 345 | j++; 346 | } 347 | if (j < len && j !== last) { 348 | // Matched! 349 | last = j; 350 | // Match 1 or more non-path separators 351 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 352 | j++; 353 | } 354 | if (j === len) { 355 | // We matched a UNC root only 356 | // Return the normalized version of the UNC root since there 357 | // is nothing left to process 358 | return `\\\\${firstPart}\\${path.slice(last)}\\`; 359 | } 360 | if (j !== last) { 361 | // We matched a UNC root with leftovers 362 | device = `\\\\${firstPart}\\${path.slice(last, j)}`; 363 | rootEnd = j; 364 | } 365 | } 366 | } 367 | } else { 368 | rootEnd = 1; 369 | } 370 | } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) { 371 | // Possible device root 372 | device = path.slice(0, 2); 373 | rootEnd = 2; 374 | if (len > 2 && isPathSeparator(path.charCodeAt(2))) { 375 | // Treat separator following drive name as an absolute path 376 | // indicator 377 | isAbsolute = true; 378 | rootEnd = 3; 379 | } 380 | } 381 | 382 | let tail = 383 | rootEnd < len 384 | ? normalizeString(path.slice(rootEnd), !isAbsolute, '\\', isPathSeparator) 385 | : ''; 386 | if (tail.length === 0 && !isAbsolute) { 387 | tail = '.'; 388 | } 389 | if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) { 390 | tail += '\\'; 391 | } 392 | if (device === undefined) { 393 | return isAbsolute ? `\\${tail}` : tail; 394 | } 395 | return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`; 396 | }, 397 | 398 | isAbsolute(path: string): boolean { 399 | validateString(path, 'path'); 400 | const len = path.length; 401 | if (len === 0) { 402 | return false; 403 | } 404 | 405 | const code = path.charCodeAt(0); 406 | return ( 407 | isPathSeparator(code) || 408 | // Possible device root 409 | (len > 2 && 410 | isWindowsDeviceRoot(code) && 411 | path.charCodeAt(1) === CHAR_COLON && 412 | isPathSeparator(path.charCodeAt(2))) 413 | ); 414 | }, 415 | 416 | join(...paths: string[]): string { 417 | if (paths.length === 0) { 418 | return '.'; 419 | } 420 | 421 | let joined; 422 | let firstPart: string | undefined; 423 | for (let i = 0; i < paths.length; ++i) { 424 | const arg = paths[i]; 425 | validateString(arg, 'path'); 426 | if (arg.length > 0) { 427 | if (joined === undefined) { 428 | joined = firstPart = arg; 429 | } else { 430 | joined += `\\${arg}`; 431 | } 432 | } 433 | } 434 | 435 | if (joined === undefined) { 436 | return '.'; 437 | } 438 | 439 | // Make sure that the joined path doesn't start with two slashes, because 440 | // normalize() will mistake it for a UNC path then. 441 | // 442 | // This step is skipped when it is very clear that the user actually 443 | // intended to point at a UNC path. This is assumed when the first 444 | // non-empty string arguments starts with exactly two slashes followed by 445 | // at least one more non-slash character. 446 | // 447 | // Note that for normalize() to treat a path as a UNC path it needs to 448 | // have at least 2 components, so we don't filter for that here. 449 | // This means that the user can use join to construct UNC paths from 450 | // a server name and a share name; for example: 451 | // path.join('//server', 'share') -> '\\\\server\\share\\') 452 | let needsReplace = true; 453 | let slashCount = 0; 454 | if (typeof firstPart === 'string' && isPathSeparator(firstPart.charCodeAt(0))) { 455 | ++slashCount; 456 | const firstLen = firstPart.length; 457 | if (firstLen > 1 && isPathSeparator(firstPart.charCodeAt(1))) { 458 | ++slashCount; 459 | if (firstLen > 2) { 460 | if (isPathSeparator(firstPart.charCodeAt(2))) { 461 | ++slashCount; 462 | } else { 463 | // We matched a UNC path in the first part 464 | needsReplace = false; 465 | } 466 | } 467 | } 468 | } 469 | if (needsReplace) { 470 | // Find any more consecutive slashes we need to replace 471 | while (slashCount < joined.length && isPathSeparator(joined.charCodeAt(slashCount))) { 472 | slashCount++; 473 | } 474 | 475 | // Replace the slashes if needed 476 | if (slashCount >= 2) { 477 | joined = `\\${joined.slice(slashCount)}`; 478 | } 479 | } 480 | 481 | return win32.normalize(joined); 482 | }, 483 | 484 | // It will solve the relative path from `from` to `to`, for instance: 485 | // from = 'C:\\orandea\\test\\aaa' 486 | // to = 'C:\\orandea\\impl\\bbb' 487 | // The output of the function should be: '..\\..\\impl\\bbb' 488 | relative(from: string, to: string): string { 489 | validateString(from, 'from'); 490 | validateString(to, 'to'); 491 | 492 | if (from === to) { 493 | return ''; 494 | } 495 | 496 | const fromOrig = win32.resolve(from); 497 | const toOrig = win32.resolve(to); 498 | 499 | if (fromOrig === toOrig) { 500 | return ''; 501 | } 502 | 503 | from = fromOrig.toLowerCase(); 504 | to = toOrig.toLowerCase(); 505 | 506 | if (from === to) { 507 | return ''; 508 | } 509 | 510 | // Trim any leading backslashes 511 | let fromStart = 0; 512 | while (fromStart < from.length && from.charCodeAt(fromStart) === CHAR_BACKWARD_SLASH) { 513 | fromStart++; 514 | } 515 | // Trim trailing backslashes (applicable to UNC paths only) 516 | let fromEnd = from.length; 517 | while (fromEnd - 1 > fromStart && from.charCodeAt(fromEnd - 1) === CHAR_BACKWARD_SLASH) { 518 | fromEnd--; 519 | } 520 | const fromLen = fromEnd - fromStart; 521 | 522 | // Trim any leading backslashes 523 | let toStart = 0; 524 | while (toStart < to.length && to.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) { 525 | toStart++; 526 | } 527 | // Trim trailing backslashes (applicable to UNC paths only) 528 | let toEnd = to.length; 529 | while (toEnd - 1 > toStart && to.charCodeAt(toEnd - 1) === CHAR_BACKWARD_SLASH) { 530 | toEnd--; 531 | } 532 | const toLen = toEnd - toStart; 533 | 534 | // Compare paths to find the longest common path from root 535 | const length = fromLen < toLen ? fromLen : toLen; 536 | let lastCommonSep = -1; 537 | let i = 0; 538 | for (; i < length; i++) { 539 | const fromCode = from.charCodeAt(fromStart + i); 540 | if (fromCode !== to.charCodeAt(toStart + i)) { 541 | break; 542 | } else if (fromCode === CHAR_BACKWARD_SLASH) { 543 | lastCommonSep = i; 544 | } 545 | } 546 | 547 | // We found a mismatch before the first common path separator was seen, so 548 | // return the original `to`. 549 | if (i !== length) { 550 | if (lastCommonSep === -1) { 551 | return toOrig; 552 | } 553 | } else { 554 | if (toLen > length) { 555 | if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) { 556 | // We get here if `from` is the exact base path for `to`. 557 | // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' 558 | return toOrig.slice(toStart + i + 1); 559 | } 560 | if (i === 2) { 561 | // We get here if `from` is the device root. 562 | // For example: from='C:\\'; to='C:\\foo' 563 | return toOrig.slice(toStart + i); 564 | } 565 | } 566 | if (fromLen > length) { 567 | if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) { 568 | // We get here if `to` is the exact base path for `from`. 569 | // For example: from='C:\\foo\\bar'; to='C:\\foo' 570 | lastCommonSep = i; 571 | } else if (i === 2) { 572 | // We get here if `to` is the device root. 573 | // For example: from='C:\\foo\\bar'; to='C:\\' 574 | lastCommonSep = 3; 575 | } 576 | } 577 | if (lastCommonSep === -1) { 578 | lastCommonSep = 0; 579 | } 580 | } 581 | 582 | let out = ''; 583 | // Generate the relative path based on the path difference between `to` and 584 | // `from` 585 | for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { 586 | if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) { 587 | out += out.length === 0 ? '..' : '\\..'; 588 | } 589 | } 590 | 591 | toStart += lastCommonSep; 592 | 593 | // Lastly, append the rest of the destination (`to`) path that comes after 594 | // the common path parts 595 | if (out.length > 0) { 596 | return `${out}${toOrig.slice(toStart, toEnd)}`; 597 | } 598 | 599 | if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) { 600 | ++toStart; 601 | } 602 | 603 | return toOrig.slice(toStart, toEnd); 604 | }, 605 | 606 | toNamespacedPath(path: string): string { 607 | // Note: this will *probably* throw somewhere. 608 | if (typeof path !== 'string') { 609 | return path; 610 | } 611 | 612 | if (path.length === 0) { 613 | return ''; 614 | } 615 | 616 | const resolvedPath = win32.resolve(path); 617 | 618 | if (resolvedPath.length <= 2) { 619 | return path; 620 | } 621 | 622 | if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) { 623 | // Possible UNC root 624 | if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) { 625 | const code = resolvedPath.charCodeAt(2); 626 | if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) { 627 | // Matched non-long UNC root, convert the path to a long UNC path 628 | return `\\\\?\\UNC\\${resolvedPath.slice(2)}`; 629 | } 630 | } 631 | } else if ( 632 | isWindowsDeviceRoot(resolvedPath.charCodeAt(0)) && 633 | resolvedPath.charCodeAt(1) === CHAR_COLON && 634 | resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH 635 | ) { 636 | // Matched device root, convert the path to a long UNC path 637 | return `\\\\?\\${resolvedPath}`; 638 | } 639 | 640 | return path; 641 | }, 642 | 643 | dirname(path: string): string { 644 | validateString(path, 'path'); 645 | const len = path.length; 646 | if (len === 0) { 647 | return '.'; 648 | } 649 | let rootEnd = -1; 650 | let offset = 0; 651 | const code = path.charCodeAt(0); 652 | 653 | if (len === 1) { 654 | // `path` contains just a path separator, exit early to avoid 655 | // unnecessary work or a dot. 656 | return isPathSeparator(code) ? path : '.'; 657 | } 658 | 659 | // Try to match a root 660 | if (isPathSeparator(code)) { 661 | // Possible UNC root 662 | 663 | rootEnd = offset = 1; 664 | 665 | if (isPathSeparator(path.charCodeAt(1))) { 666 | // Matched double path separator at beginning 667 | let j = 2; 668 | let last = j; 669 | // Match 1 or more non-path separators 670 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 671 | j++; 672 | } 673 | if (j < len && j !== last) { 674 | // Matched! 675 | last = j; 676 | // Match 1 or more path separators 677 | while (j < len && isPathSeparator(path.charCodeAt(j))) { 678 | j++; 679 | } 680 | if (j < len && j !== last) { 681 | // Matched! 682 | last = j; 683 | // Match 1 or more non-path separators 684 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 685 | j++; 686 | } 687 | if (j === len) { 688 | // We matched a UNC root only 689 | return path; 690 | } 691 | if (j !== last) { 692 | // We matched a UNC root with leftovers 693 | 694 | // Offset by 1 to include the separator after the UNC root to 695 | // treat it as a "normal root" on top of a (UNC) root 696 | rootEnd = offset = j + 1; 697 | } 698 | } 699 | } 700 | } 701 | // Possible device root 702 | } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) { 703 | rootEnd = len > 2 && isPathSeparator(path.charCodeAt(2)) ? 3 : 2; 704 | offset = rootEnd; 705 | } 706 | 707 | let end = -1; 708 | let matchedSlash = true; 709 | for (let i = len - 1; i >= offset; --i) { 710 | if (isPathSeparator(path.charCodeAt(i))) { 711 | if (!matchedSlash) { 712 | end = i; 713 | break; 714 | } 715 | } else { 716 | // We saw the first non-path separator 717 | matchedSlash = false; 718 | } 719 | } 720 | 721 | if (end === -1) { 722 | if (rootEnd === -1) { 723 | return '.'; 724 | } 725 | 726 | end = rootEnd; 727 | } 728 | return path.slice(0, end); 729 | }, 730 | 731 | basename(path: string, ext?: string): string { 732 | if (ext !== undefined) { 733 | validateString(ext, 'ext'); 734 | } 735 | validateString(path, 'path'); 736 | let start = 0; 737 | let end = -1; 738 | let matchedSlash = true; 739 | let i; 740 | 741 | // Check for a drive letter prefix so as not to mistake the following 742 | // path separator as an extra separator at the end of the path that can be 743 | // disregarded 744 | if ( 745 | path.length >= 2 && 746 | isWindowsDeviceRoot(path.charCodeAt(0)) && 747 | path.charCodeAt(1) === CHAR_COLON 748 | ) { 749 | start = 2; 750 | } 751 | 752 | if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { 753 | if (ext === path) { 754 | return ''; 755 | } 756 | let extIdx = ext.length - 1; 757 | let firstNonSlashEnd = -1; 758 | for (i = path.length - 1; i >= start; --i) { 759 | const code = path.charCodeAt(i); 760 | if (isPathSeparator(code)) { 761 | // If we reached a path separator that was not part of a set of path 762 | // separators at the end of the string, stop now 763 | if (!matchedSlash) { 764 | start = i + 1; 765 | break; 766 | } 767 | } else { 768 | if (firstNonSlashEnd === -1) { 769 | // We saw the first non-path separator, remember this index in case 770 | // we need it if the extension ends up not matching 771 | matchedSlash = false; 772 | firstNonSlashEnd = i + 1; 773 | } 774 | if (extIdx >= 0) { 775 | // Try to match the explicit extension 776 | if (code === ext.charCodeAt(extIdx)) { 777 | if (--extIdx === -1) { 778 | // We matched the extension, so mark this as the end of our path 779 | // component 780 | end = i; 781 | } 782 | } else { 783 | // Extension does not match, so our result is the entire path 784 | // component 785 | extIdx = -1; 786 | end = firstNonSlashEnd; 787 | } 788 | } 789 | } 790 | } 791 | 792 | if (start === end) { 793 | end = firstNonSlashEnd; 794 | } else if (end === -1) { 795 | end = path.length; 796 | } 797 | return path.slice(start, end); 798 | } 799 | for (i = path.length - 1; i >= start; --i) { 800 | if (isPathSeparator(path.charCodeAt(i))) { 801 | // If we reached a path separator that was not part of a set of path 802 | // separators at the end of the string, stop now 803 | if (!matchedSlash) { 804 | start = i + 1; 805 | break; 806 | } 807 | } else if (end === -1) { 808 | // We saw the first non-path separator, mark this as the end of our 809 | // path component 810 | matchedSlash = false; 811 | end = i + 1; 812 | } 813 | } 814 | 815 | if (end === -1) { 816 | return ''; 817 | } 818 | return path.slice(start, end); 819 | }, 820 | 821 | extname(path: string): string { 822 | validateString(path, 'path'); 823 | let start = 0; 824 | let startDot = -1; 825 | let startPart = 0; 826 | let end = -1; 827 | let matchedSlash = true; 828 | // Track the state of characters (if any) we see before our first dot and 829 | // after any path separator we find 830 | let preDotState = 0; 831 | 832 | // Check for a drive letter prefix so as not to mistake the following 833 | // path separator as an extra separator at the end of the path that can be 834 | // disregarded 835 | 836 | if ( 837 | path.length >= 2 && 838 | path.charCodeAt(1) === CHAR_COLON && 839 | isWindowsDeviceRoot(path.charCodeAt(0)) 840 | ) { 841 | start = startPart = 2; 842 | } 843 | 844 | for (let i = path.length - 1; i >= start; --i) { 845 | const code = path.charCodeAt(i); 846 | if (isPathSeparator(code)) { 847 | // If we reached a path separator that was not part of a set of path 848 | // separators at the end of the string, stop now 849 | if (!matchedSlash) { 850 | startPart = i + 1; 851 | break; 852 | } 853 | continue; 854 | } 855 | if (end === -1) { 856 | // We saw the first non-path separator, mark this as the end of our 857 | // extension 858 | matchedSlash = false; 859 | end = i + 1; 860 | } 861 | if (code === CHAR_DOT) { 862 | // If this is our first dot, mark it as the start of our extension 863 | if (startDot === -1) { 864 | startDot = i; 865 | } else if (preDotState !== 1) { 866 | preDotState = 1; 867 | } 868 | } else if (startDot !== -1) { 869 | // We saw a non-dot and non-path separator before our dot, so we should 870 | // have a good chance at having a non-empty extension 871 | preDotState = -1; 872 | } 873 | } 874 | 875 | if ( 876 | startDot === -1 || 877 | end === -1 || 878 | // We saw a non-dot character immediately before the dot 879 | preDotState === 0 || 880 | // The (right-most) trimmed path component is exactly '..' 881 | (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) 882 | ) { 883 | return ''; 884 | } 885 | return path.slice(startDot, end); 886 | }, 887 | 888 | format: _format.bind(null, '\\'), 889 | 890 | parse(path) { 891 | validateString(path, 'path'); 892 | 893 | const ret = { root: '', dir: '', base: '', ext: '', name: '' }; 894 | if (path.length === 0) { 895 | return ret; 896 | } 897 | 898 | const len = path.length; 899 | let rootEnd = 0; 900 | let code = path.charCodeAt(0); 901 | 902 | if (len === 1) { 903 | if (isPathSeparator(code)) { 904 | // `path` contains just a path separator, exit early to avoid 905 | // unnecessary work 906 | ret.root = ret.dir = path; 907 | return ret; 908 | } 909 | ret.base = ret.name = path; 910 | return ret; 911 | } 912 | // Try to match a root 913 | if (isPathSeparator(code)) { 914 | // Possible UNC root 915 | 916 | rootEnd = 1; 917 | if (isPathSeparator(path.charCodeAt(1))) { 918 | // Matched double path separator at beginning 919 | let j = 2; 920 | let last = j; 921 | // Match 1 or more non-path separators 922 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 923 | j++; 924 | } 925 | if (j < len && j !== last) { 926 | // Matched! 927 | last = j; 928 | // Match 1 or more path separators 929 | while (j < len && isPathSeparator(path.charCodeAt(j))) { 930 | j++; 931 | } 932 | if (j < len && j !== last) { 933 | // Matched! 934 | last = j; 935 | // Match 1 or more non-path separators 936 | while (j < len && !isPathSeparator(path.charCodeAt(j))) { 937 | j++; 938 | } 939 | if (j === len) { 940 | // We matched a UNC root only 941 | rootEnd = j; 942 | } else if (j !== last) { 943 | // We matched a UNC root with leftovers 944 | rootEnd = j + 1; 945 | } 946 | } 947 | } 948 | } 949 | } else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) { 950 | // Possible device root 951 | if (len <= 2) { 952 | // `path` contains just a drive root, exit early to avoid 953 | // unnecessary work 954 | ret.root = ret.dir = path; 955 | return ret; 956 | } 957 | rootEnd = 2; 958 | if (isPathSeparator(path.charCodeAt(2))) { 959 | if (len === 3) { 960 | // `path` contains just a drive root, exit early to avoid 961 | // unnecessary work 962 | ret.root = ret.dir = path; 963 | return ret; 964 | } 965 | rootEnd = 3; 966 | } 967 | } 968 | if (rootEnd > 0) { 969 | ret.root = path.slice(0, rootEnd); 970 | } 971 | 972 | let startDot = -1; 973 | let startPart = rootEnd; 974 | let end = -1; 975 | let matchedSlash = true; 976 | let i = path.length - 1; 977 | 978 | // Track the state of characters (if any) we see before our first dot and 979 | // after any path separator we find 980 | let preDotState = 0; 981 | 982 | // Get non-dir info 983 | for (; i >= rootEnd; --i) { 984 | code = path.charCodeAt(i); 985 | if (isPathSeparator(code)) { 986 | // If we reached a path separator that was not part of a set of path 987 | // separators at the end of the string, stop now 988 | if (!matchedSlash) { 989 | startPart = i + 1; 990 | break; 991 | } 992 | continue; 993 | } 994 | if (end === -1) { 995 | // We saw the first non-path separator, mark this as the end of our 996 | // extension 997 | matchedSlash = false; 998 | end = i + 1; 999 | } 1000 | if (code === CHAR_DOT) { 1001 | // If this is our first dot, mark it as the start of our extension 1002 | if (startDot === -1) { 1003 | startDot = i; 1004 | } else if (preDotState !== 1) { 1005 | preDotState = 1; 1006 | } 1007 | } else if (startDot !== -1) { 1008 | // We saw a non-dot and non-path separator before our dot, so we should 1009 | // have a good chance at having a non-empty extension 1010 | preDotState = -1; 1011 | } 1012 | } 1013 | 1014 | if (end !== -1) { 1015 | if ( 1016 | startDot === -1 || 1017 | // We saw a non-dot character immediately before the dot 1018 | preDotState === 0 || 1019 | // The (right-most) trimmed path component is exactly '..' 1020 | (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) 1021 | ) { 1022 | ret.base = ret.name = path.slice(startPart, end); 1023 | } else { 1024 | ret.name = path.slice(startPart, startDot); 1025 | ret.base = path.slice(startPart, end); 1026 | ret.ext = path.slice(startDot, end); 1027 | } 1028 | } 1029 | 1030 | // If the directory is the root, use the entire root as the `dir` including 1031 | // the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the 1032 | // trailing slash (`C:\abc\def` -> `C:\abc`). 1033 | if (startPart > 0 && startPart !== rootEnd) { 1034 | ret.dir = path.slice(0, startPart - 1); 1035 | } else { 1036 | ret.dir = ret.root; 1037 | } 1038 | 1039 | return ret; 1040 | }, 1041 | 1042 | sep: '\\', 1043 | delimiter: ';', 1044 | win32: null, 1045 | posix: null, 1046 | }; 1047 | 1048 | export const posix: IPath = { 1049 | // path.resolve([from ...], to) 1050 | resolve(...pathSegments: string[]): string { 1051 | let resolvedPath = ''; 1052 | let resolvedAbsolute = false; 1053 | 1054 | for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) { 1055 | const path = i >= 0 ? pathSegments[i] : process.cwd(); 1056 | 1057 | validateString(path, 'path'); 1058 | 1059 | // Skip empty entries 1060 | if (path.length === 0) { 1061 | continue; 1062 | } 1063 | 1064 | resolvedPath = `${path}/${resolvedPath}`; 1065 | resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; 1066 | } 1067 | 1068 | // At this point the path should be resolved to a full absolute path, but 1069 | // handle relative paths to be safe (might happen when process.cwd() fails) 1070 | 1071 | // Normalize the path 1072 | resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/', isPosixPathSeparator); 1073 | 1074 | if (resolvedAbsolute) { 1075 | return `/${resolvedPath}`; 1076 | } 1077 | return resolvedPath.length > 0 ? resolvedPath : '.'; 1078 | }, 1079 | 1080 | normalize(path: string): string { 1081 | validateString(path, 'path'); 1082 | 1083 | if (path.length === 0) { 1084 | return '.'; 1085 | } 1086 | 1087 | const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; 1088 | const trailingSeparator = path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH; 1089 | 1090 | // Normalize the path 1091 | path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator); 1092 | 1093 | if (path.length === 0) { 1094 | if (isAbsolute) { 1095 | return '/'; 1096 | } 1097 | return trailingSeparator ? './' : '.'; 1098 | } 1099 | if (trailingSeparator) { 1100 | path += '/'; 1101 | } 1102 | 1103 | return isAbsolute ? `/${path}` : path; 1104 | }, 1105 | 1106 | isAbsolute(path: string): boolean { 1107 | validateString(path, 'path'); 1108 | return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH; 1109 | }, 1110 | 1111 | join(...paths: string[]): string { 1112 | if (paths.length === 0) { 1113 | return '.'; 1114 | } 1115 | let joined; 1116 | for (let i = 0; i < paths.length; ++i) { 1117 | const arg = paths[i]; 1118 | validateString(arg, 'path'); 1119 | if (arg.length > 0) { 1120 | if (joined === undefined) { 1121 | joined = arg; 1122 | } else { 1123 | joined += `/${arg}`; 1124 | } 1125 | } 1126 | } 1127 | if (joined === undefined) { 1128 | return '.'; 1129 | } 1130 | return posix.normalize(joined); 1131 | }, 1132 | 1133 | relative(from: string, to: string): string { 1134 | validateString(from, 'from'); 1135 | validateString(to, 'to'); 1136 | 1137 | if (from === to) { 1138 | return ''; 1139 | } 1140 | 1141 | // Trim leading forward slashes. 1142 | from = posix.resolve(from); 1143 | to = posix.resolve(to); 1144 | 1145 | if (from === to) { 1146 | return ''; 1147 | } 1148 | 1149 | const fromStart = 1; 1150 | const fromEnd = from.length; 1151 | const fromLen = fromEnd - fromStart; 1152 | const toStart = 1; 1153 | const toLen = to.length - toStart; 1154 | 1155 | // Compare paths to find the longest common path from root 1156 | const length = fromLen < toLen ? fromLen : toLen; 1157 | let lastCommonSep = -1; 1158 | let i = 0; 1159 | for (; i < length; i++) { 1160 | const fromCode = from.charCodeAt(fromStart + i); 1161 | if (fromCode !== to.charCodeAt(toStart + i)) { 1162 | break; 1163 | } else if (fromCode === CHAR_FORWARD_SLASH) { 1164 | lastCommonSep = i; 1165 | } 1166 | } 1167 | if (i === length) { 1168 | if (toLen > length) { 1169 | if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) { 1170 | // We get here if `from` is the exact base path for `to`. 1171 | // For example: from='/foo/bar'; to='/foo/bar/baz' 1172 | return to.slice(toStart + i + 1); 1173 | } 1174 | if (i === 0) { 1175 | // We get here if `from` is the root 1176 | // For example: from='/'; to='/foo' 1177 | return to.slice(toStart + i); 1178 | } 1179 | } else if (fromLen > length) { 1180 | if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) { 1181 | // We get here if `to` is the exact base path for `from`. 1182 | // For example: from='/foo/bar/baz'; to='/foo/bar' 1183 | lastCommonSep = i; 1184 | } else if (i === 0) { 1185 | // We get here if `to` is the root. 1186 | // For example: from='/foo/bar'; to='/' 1187 | lastCommonSep = 0; 1188 | } 1189 | } 1190 | } 1191 | 1192 | let out = ''; 1193 | // Generate the relative path based on the path difference between `to` 1194 | // and `from`. 1195 | for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { 1196 | if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) { 1197 | out += out.length === 0 ? '..' : '/..'; 1198 | } 1199 | } 1200 | 1201 | // Lastly, append the rest of the destination (`to`) path that comes after 1202 | // the common path parts. 1203 | return `${out}${to.slice(toStart + lastCommonSep)}`; 1204 | }, 1205 | 1206 | toNamespacedPath(path: string): string { 1207 | // Non-op on posix systems 1208 | return path; 1209 | }, 1210 | 1211 | dirname(path: string): string { 1212 | validateString(path, 'path'); 1213 | if (path.length === 0) { 1214 | return '.'; 1215 | } 1216 | const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH; 1217 | let end = -1; 1218 | let matchedSlash = true; 1219 | for (let i = path.length - 1; i >= 1; --i) { 1220 | if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { 1221 | if (!matchedSlash) { 1222 | end = i; 1223 | break; 1224 | } 1225 | } else { 1226 | // We saw the first non-path separator 1227 | matchedSlash = false; 1228 | } 1229 | } 1230 | 1231 | if (end === -1) { 1232 | return hasRoot ? '/' : '.'; 1233 | } 1234 | if (hasRoot && end === 1) { 1235 | return '//'; 1236 | } 1237 | return path.slice(0, end); 1238 | }, 1239 | 1240 | basename(path: string, ext?: string): string { 1241 | if (ext !== undefined) { 1242 | validateString(ext, 'ext'); 1243 | } 1244 | validateString(path, 'path'); 1245 | 1246 | let start = 0; 1247 | let end = -1; 1248 | let matchedSlash = true; 1249 | let i; 1250 | 1251 | if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { 1252 | if (ext === path) { 1253 | return ''; 1254 | } 1255 | let extIdx = ext.length - 1; 1256 | let firstNonSlashEnd = -1; 1257 | for (i = path.length - 1; i >= 0; --i) { 1258 | const code = path.charCodeAt(i); 1259 | if (code === CHAR_FORWARD_SLASH) { 1260 | // If we reached a path separator that was not part of a set of path 1261 | // separators at the end of the string, stop now 1262 | if (!matchedSlash) { 1263 | start = i + 1; 1264 | break; 1265 | } 1266 | } else { 1267 | if (firstNonSlashEnd === -1) { 1268 | // We saw the first non-path separator, remember this index in case 1269 | // we need it if the extension ends up not matching 1270 | matchedSlash = false; 1271 | firstNonSlashEnd = i + 1; 1272 | } 1273 | if (extIdx >= 0) { 1274 | // Try to match the explicit extension 1275 | if (code === ext.charCodeAt(extIdx)) { 1276 | if (--extIdx === -1) { 1277 | // We matched the extension, so mark this as the end of our path 1278 | // component 1279 | end = i; 1280 | } 1281 | } else { 1282 | // Extension does not match, so our result is the entire path 1283 | // component 1284 | extIdx = -1; 1285 | end = firstNonSlashEnd; 1286 | } 1287 | } 1288 | } 1289 | } 1290 | 1291 | if (start === end) { 1292 | end = firstNonSlashEnd; 1293 | } else if (end === -1) { 1294 | end = path.length; 1295 | } 1296 | return path.slice(start, end); 1297 | } 1298 | for (i = path.length - 1; i >= 0; --i) { 1299 | if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) { 1300 | // If we reached a path separator that was not part of a set of path 1301 | // separators at the end of the string, stop now 1302 | if (!matchedSlash) { 1303 | start = i + 1; 1304 | break; 1305 | } 1306 | } else if (end === -1) { 1307 | // We saw the first non-path separator, mark this as the end of our 1308 | // path component 1309 | matchedSlash = false; 1310 | end = i + 1; 1311 | } 1312 | } 1313 | 1314 | if (end === -1) { 1315 | return ''; 1316 | } 1317 | return path.slice(start, end); 1318 | }, 1319 | 1320 | extname(path: string): string { 1321 | validateString(path, 'path'); 1322 | let startDot = -1; 1323 | let startPart = 0; 1324 | let end = -1; 1325 | let matchedSlash = true; 1326 | // Track the state of characters (if any) we see before our first dot and 1327 | // after any path separator we find 1328 | let preDotState = 0; 1329 | for (let i = path.length - 1; i >= 0; --i) { 1330 | const code = path.charCodeAt(i); 1331 | if (code === CHAR_FORWARD_SLASH) { 1332 | // If we reached a path separator that was not part of a set of path 1333 | // separators at the end of the string, stop now 1334 | if (!matchedSlash) { 1335 | startPart = i + 1; 1336 | break; 1337 | } 1338 | continue; 1339 | } 1340 | if (end === -1) { 1341 | // We saw the first non-path separator, mark this as the end of our 1342 | // extension 1343 | matchedSlash = false; 1344 | end = i + 1; 1345 | } 1346 | if (code === CHAR_DOT) { 1347 | // If this is our first dot, mark it as the start of our extension 1348 | if (startDot === -1) { 1349 | startDot = i; 1350 | } else if (preDotState !== 1) { 1351 | preDotState = 1; 1352 | } 1353 | } else if (startDot !== -1) { 1354 | // We saw a non-dot and non-path separator before our dot, so we should 1355 | // have a good chance at having a non-empty extension 1356 | preDotState = -1; 1357 | } 1358 | } 1359 | 1360 | if ( 1361 | startDot === -1 || 1362 | end === -1 || 1363 | // We saw a non-dot character immediately before the dot 1364 | preDotState === 0 || 1365 | // The (right-most) trimmed path component is exactly '..' 1366 | (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) 1367 | ) { 1368 | return ''; 1369 | } 1370 | return path.slice(startDot, end); 1371 | }, 1372 | 1373 | format: _format.bind(null, '/'), 1374 | 1375 | parse(path: string): ParsedPath { 1376 | validateString(path, 'path'); 1377 | 1378 | const ret = { root: '', dir: '', base: '', ext: '', name: '' }; 1379 | if (path.length === 0) { 1380 | return ret; 1381 | } 1382 | const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH; 1383 | let start; 1384 | if (isAbsolute) { 1385 | ret.root = '/'; 1386 | start = 1; 1387 | } else { 1388 | start = 0; 1389 | } 1390 | let startDot = -1; 1391 | let startPart = 0; 1392 | let end = -1; 1393 | let matchedSlash = true; 1394 | let i = path.length - 1; 1395 | 1396 | // Track the state of characters (if any) we see before our first dot and 1397 | // after any path separator we find 1398 | let preDotState = 0; 1399 | 1400 | // Get non-dir info 1401 | for (; i >= start; --i) { 1402 | const code = path.charCodeAt(i); 1403 | if (code === CHAR_FORWARD_SLASH) { 1404 | // If we reached a path separator that was not part of a set of path 1405 | // separators at the end of the string, stop now 1406 | if (!matchedSlash) { 1407 | startPart = i + 1; 1408 | break; 1409 | } 1410 | continue; 1411 | } 1412 | if (end === -1) { 1413 | // We saw the first non-path separator, mark this as the end of our 1414 | // extension 1415 | matchedSlash = false; 1416 | end = i + 1; 1417 | } 1418 | if (code === CHAR_DOT) { 1419 | // If this is our first dot, mark it as the start of our extension 1420 | if (startDot === -1) { 1421 | startDot = i; 1422 | } else if (preDotState !== 1) { 1423 | preDotState = 1; 1424 | } 1425 | } else if (startDot !== -1) { 1426 | // We saw a non-dot and non-path separator before our dot, so we should 1427 | // have a good chance at having a non-empty extension 1428 | preDotState = -1; 1429 | } 1430 | } 1431 | 1432 | if (end !== -1) { 1433 | const start = startPart === 0 && isAbsolute ? 1 : startPart; 1434 | if ( 1435 | startDot === -1 || 1436 | // We saw a non-dot character immediately before the dot 1437 | preDotState === 0 || 1438 | // The (right-most) trimmed path component is exactly '..' 1439 | (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) 1440 | ) { 1441 | ret.base = ret.name = path.slice(start, end); 1442 | } else { 1443 | ret.name = path.slice(start, startDot); 1444 | ret.base = path.slice(start, end); 1445 | ret.ext = path.slice(startDot, end); 1446 | } 1447 | } 1448 | 1449 | if (startPart > 0) { 1450 | ret.dir = path.slice(0, startPart - 1); 1451 | } else if (isAbsolute) { 1452 | ret.dir = '/'; 1453 | } 1454 | 1455 | return ret; 1456 | }, 1457 | 1458 | sep: '/', 1459 | delimiter: ':', 1460 | win32: null, 1461 | posix: null, 1462 | }; 1463 | 1464 | posix.win32 = win32.win32 = win32; 1465 | posix.posix = win32.posix = posix; 1466 | 1467 | export const normalize = process.platform === 'win32' ? win32.normalize : posix.normalize; 1468 | export const isAbsolute = process.platform === 'win32' ? win32.isAbsolute : posix.isAbsolute; 1469 | export const join = process.platform === 'win32' ? win32.join : posix.join; 1470 | export const resolve = process.platform === 'win32' ? win32.resolve : posix.resolve; 1471 | export const relative = process.platform === 'win32' ? win32.relative : posix.relative; 1472 | export const dirname = process.platform === 'win32' ? win32.dirname : posix.dirname; 1473 | export const basename = process.platform === 'win32' ? win32.basename : posix.basename; 1474 | export const extname = process.platform === 'win32' ? win32.extname : posix.extname; 1475 | export const format = process.platform === 'win32' ? win32.format : posix.format; 1476 | export const parse = process.platform === 'win32' ? win32.parse : posix.parse; 1477 | export const toNamespacedPath = 1478 | process.platform === 'win32' ? win32.toNamespacedPath : posix.toNamespacedPath; 1479 | export const sep = process.platform === 'win32' ? win32.sep : posix.sep; 1480 | export const delimiter = process.platform === 'win32' ? win32.delimiter : posix.delimiter; 1481 | --------------------------------------------------------------------------------