├── icon.png
├── images
├── init.gif
├── fossil.png
├── fossil-auth.png
├── fossil-diff.gif
├── fossil-root.png
├── fossil-uri.png
├── fossil-user.png
├── change-branch.gif
├── fossil-commands.png
└── fossil-ssl-fail.png
├── .github
├── release.yml
├── issue_template.md
├── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ └── bug_report.yml
└── workflows
│ └── fossil.yml
├── .prettierrc
├── .vscode
├── extensions.json
├── tasks.json
├── launch.json
└── settings.json
├── docs
├── dev
│ ├── pikchr.md
│ ├── build.md
│ ├── release.md
│ ├── scenarios.md
│ └── api.md
└── cloning.md
├── .nycrc
├── .vscodeignore
├── pikchr
├── assets
│ └── pikchr.svg
├── language-configuration.json
├── test
│ ├── numproperty.test.pikchr
│ ├── place.test.pikchr
│ ├── textposition.test.pikchr
│ ├── nth.test.pikchr
│ ├── comments.test.pikchr
│ ├── position.test.pikchr
│ ├── homepage.example.test.pikchr
│ ├── boolproperty.test.pikchr
│ ├── attribute.test.pikchr
│ ├── statements.test.pikchr
│ ├── macro.test.pikchr
│ └── advanced.test.pikchr
├── codeblock.pikchr.tmLanguage.json
└── syntax_tree.py
├── resources
└── icons
│ ├── dark
│ ├── status-added.svg
│ ├── status-conflict.svg
│ ├── status-deleted.svg
│ ├── status-ignored.svg
│ ├── status-missing.svg
│ ├── status-modified.svg
│ ├── status-renamed.svg
│ ├── status-untracked.svg
│ ├── open-change.svg
│ └── status-clean.svg
│ └── light
│ ├── status-added.svg
│ ├── status-conflict.svg
│ ├── status-deleted.svg
│ ├── status-ignored.svg
│ ├── status-missing.svg
│ ├── status-modified.svg
│ ├── status-renamed.svg
│ ├── status-untracked.svg
│ ├── open-change.svg
│ └── status-clean.svg
├── media
├── tsconfig.json
├── preview.css
└── preview.ts
├── .gitignore
├── tsconfig.json
├── src
├── typings
│ └── refs.d.ts
├── uri.ts
├── test
│ ├── suite
│ │ ├── index.ts
│ │ ├── extension.test.ts
│ │ ├── Infrastructure.test.ts
│ │ ├── timelineSuite.ts
│ │ ├── revertSuite.ts
│ │ ├── branchSuite.ts
│ │ ├── renameSuite.ts
│ │ └── utilitiesSuite.ts
│ ├── runTest.ts
│ └── summaryAsMarkdown.ts
├── iterators.ts
├── throttlingQueue.ts
├── fossilFinder.ts
├── config.ts
├── main.ts
├── util.ts
├── revert.ts
├── humanise.ts
├── decorators.ts
├── resourceGroups.ts
├── statusBar.ts
├── praise.ts
└── fileSystemProvider.ts
├── esbuild.config.js
├── LICENSE.txt
├── .eslintrc.json
├── package.nls.json
└── README.md
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/icon.png
--------------------------------------------------------------------------------
/images/init.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/init.gif
--------------------------------------------------------------------------------
/images/fossil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil.png
--------------------------------------------------------------------------------
/images/fossil-auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-auth.png
--------------------------------------------------------------------------------
/images/fossil-diff.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-diff.gif
--------------------------------------------------------------------------------
/images/fossil-root.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-root.png
--------------------------------------------------------------------------------
/images/fossil-uri.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-uri.png
--------------------------------------------------------------------------------
/images/fossil-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-user.png
--------------------------------------------------------------------------------
/images/change-branch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/change-branch.gif
--------------------------------------------------------------------------------
/images/fossil-commands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-commands.png
--------------------------------------------------------------------------------
/images/fossil-ssl-fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koog1000/vscode-fossil/HEAD/images/fossil-ssl-fail.png
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | categories:
3 | - title: All Changes
4 | labels:
5 | - "*"
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "arrowParens": "avoid",
5 | "tabWidth": 4
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "RedCMD.tmlanguage-syntax-highlighter",
4 | "pflannery.vscode-versionlens",
5 | ]
6 | }
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | Fossil version: x.x.x
2 |
3 | Operating system: ???
4 |
5 | Vscode/Vscodium version: x.x.x
6 |
7 | I want this to be fixed/implemented (on a scale 1-10): #
8 |
--------------------------------------------------------------------------------
/docs/dev/pikchr.md:
--------------------------------------------------------------------------------
1 | # Debugging TextMate rules
2 |
3 | ## Test rules
4 |
5 | 1) Clone `git@github.com:microsoft/vscode-textmate.git`
6 | 2) Execute `npm run inspect pikchr/pikchr.tmLanguage.json example.pikchr`
7 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "lines": 92,
3 | "branches": 85,
4 | "statements": 92,
5 | "watermarks": {
6 | "lines": [80, 95],
7 | "functions": [80, 95],
8 | "branches": [80, 95],
9 | "statements": [80, 95]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .mocharc.js
3 | esbuild.config.js
4 | .nycrc
5 | .vscode
6 | **/.gitignore
7 | **/*.map
8 | **/*.ts
9 | **/tsconfig.json
10 | coverage/
11 | docs/
12 | images/
13 | node_modules
14 | src/
15 | pikchr/test/*
16 | pikchr/*.py
17 |
--------------------------------------------------------------------------------
/pikchr/assets/pikchr.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-added.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-conflict.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-deleted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-ignored.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-missing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-modified.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-renamed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-untracked.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-added.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-conflict.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-deleted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-ignored.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-missing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-modified.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-renamed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/status-untracked.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "outDir": "./out/media",
5 | "jsx": "react",
6 | "esModuleInterop": true,
7 | "lib": [
8 | "es2018",
9 | "DOM",
10 | "DOM.Iterable"
11 | ],
12 | "composite": true
13 |
14 | },
15 | "include": [
16 | "./*.ts"
17 | ],
18 | "typeAcquisition": {
19 | "include": [
20 | "@types/vscode-webview"
21 | ]
22 | },
23 | }
--------------------------------------------------------------------------------
/resources/icons/dark/open-change.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/open-change.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pikchr/language-configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "comments": {
3 | "lineComment": "//",
4 | "blockComment": [ "/*", "*/" ]
5 | },
6 | "brackets": [
7 | ["{", "}"],
8 | ["[", "]"],
9 | ["(", ")"]
10 | ],
11 | "autoClosingPairs": [
12 | { "open": "{", "close": "}", "notIn": ["string"] },
13 | { "open": "[", "close": "]", "notIn": ["string"] },
14 | { "open": "(", "close": ")", "notIn": ["string"] },
15 | { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
16 | ],
17 | "indentationRules": {
18 | "decreaseIndentPattern": "^\\s",
19 | "increaseIndentPattern": "^\\S.*\\\\$"
20 | }
21 | }
--------------------------------------------------------------------------------
/pikchr/test/numproperty.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "all statements must work with commants"
2 |
3 | box/**/width/**/10px
4 | // <--- storage.type.class.pikchr
5 | // ^^^^ ^^^^ comment.block.pikchr
6 | // ^^^^ support.constant.property-value.pikchr
7 | // ^^^^ constant.numeric.pikchr
8 |
9 | box diameter 1 ht 1 height 1 rad 1 radius 1 thickness 1 width 1 wid 1
10 | // <--- storage.type.class.pikchr
11 | // ^^^^^^^^ ^^ ^^^^^^ ^^^ ^^^^^^ ^^^^^^^^^ ^^^^^ ^^^ support.constant.property-value.pikchr
12 | // ^ ^ ^ ^ ^ ^ ^ ^ constant.numeric.pikchr
13 |
--------------------------------------------------------------------------------
/pikchr/test/place.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "place node"
2 |
3 | arrow from last dot to X0
4 | // ^^^^ keyword.pikchr
5 | // ^^^^ constant.language.pikchr
6 | // ^^^ entity.name.function.pikchr
7 | // ^^ keyword.pikchr
8 | // ^^ variable.language.place.pikchr
9 | circle at end of first arrow
10 | // ^^ keyword.pikchr
11 | // ^^^ support.constant.edge.pikchr
12 | // ^^ keyword.control.of.pikchr
13 | // ^ -keyword.control.of.pikchr
14 | // ^^^^^ constant.language.pikchr
15 | // ^^^^^ entity.name.function.pikchr
16 |
--------------------------------------------------------------------------------
/docs/dev/build.md:
--------------------------------------------------------------------------------
1 | # Building Extension from Source
2 | **Note:** The official way to install vscode-fossil is from within
3 | [VSCode Extensions](https://code.visualstudio.com/docs/editor/extension-gallery#_browse-for-extensions)
4 |
5 |
6 |
7 | ### Dependencies
8 | You will need to install [Node.js](https://nodejs.org/en/download/)
9 | on your computer and add it to your `$PATH`.
10 |
11 | ### Build Steps
12 | 1. `git clone` repository anywhere on your filesystem.
13 | 2. `npm install` from clone directory to install local dependencies.
14 | 3. `npm run compile` to build extension.
15 | 4. press `F5` to run the extension or run the tests.
16 |
17 | See more commands in `package.json`
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # compilation output
3 | out*/**
4 | media/out*/**
5 | # all js files are compiled
6 | *.js
7 | # except this one
8 | !esbuild.config.js
9 | # compiled package, generated by `npm run package`:
10 | *.vsix
11 | # ignore GIMP files:
12 | *.xcf
13 | # cache directory, generated by `npm run test`:
14 | .vscode-test
15 | # yarn stuff for when someone is testing yarn:
16 | yarn.lock
17 | yarn-error.log
18 | # temporary directory for preparing ./images:
19 | images/production
20 | # generated by `npm run coverage`:
21 | /coverage
22 | # graphviz files generated by pikchr/syntax_tree.py:
23 | *.gv
24 | # pikchr/syntax_tree.py in pdf format:
25 | *.pdf
26 | # eslint result:
27 | *.sarif
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "target": "es2020",
5 | "lib": [
6 | "es2020"
7 | ],
8 | "noImplicitOverride": true,
9 | "noImplicitThis": true,
10 | "module": "commonjs",
11 | "outDir": "out",
12 | "strict": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | //"noUnusedParameters": true,
16 | "removeComments": true,
17 | "sourceMap": true,
18 | "strictNullChecks": true,
19 | "experimentalDecorators": true,
20 | },
21 | "include": [
22 | "src/**/*",
23 | ],
24 | "references": [
25 | {"path": "./media"}
26 | ]
27 | }
--------------------------------------------------------------------------------
/pikchr/test/textposition.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "everything related to using text"
2 |
3 | box "" above
4 | // ^^^^^ keyword.pikchr
5 |
6 | box "" aligned
7 | // ^^^^^^^ keyword.pikchr
8 |
9 | box "" below
10 | // ^^^^^ keyword.pikchr
11 |
12 | box "" big
13 | // ^^^ keyword.pikchr
14 |
15 | box "" bold
16 | // ^^^^ keyword.pikchr
17 |
18 | box "" mono
19 | // ^^^^ keyword.pikchr
20 |
21 | box "" monospace
22 | // ^^^^^^^^^ keyword.pikchr
23 |
24 | box "" center
25 | // ^^^^^^ keyword.pikchr
26 |
27 | box "" italic
28 | // ^^^^^^ keyword.pikchr
29 |
30 | box "" ljust
31 | // ^^^^^ keyword.pikchr
32 |
33 | box "" rjust
34 | // ^^^^^ keyword.pikchr
35 |
36 | box "" small
37 | // ^^^^^ keyword.pikchr
38 |
39 | box "" small bold
40 | // ^^^^^ ^^^^ keyword.pikchr
41 |
--------------------------------------------------------------------------------
/src/typings/refs.d.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | ///
8 |
9 | declare namespace TSReset {
10 | type NonFalsy = T extends false | 0 | '' | null | undefined | 0n
11 | ? never
12 | : T;
13 | }
14 |
15 | interface Array {
16 | filter(predicate: BooleanConstructor, thisArg?: any): TSReset.NonFalsy[];
17 | }
18 |
19 | interface ReadonlyArray {
20 | filter(predicate: BooleanConstructor, thisArg?: any): TSReset.NonFalsy[];
21 | }
22 |
--------------------------------------------------------------------------------
/pikchr/test/nth.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "all checks for 'nth' tree"
2 |
3 | <
4 | < all checks are `attribute -> at -> position -> place -> place2 -> nth -> ...`
5 | <
6 |
7 | <
8 | < `nth -> NTH -> classname`
9 | <
10 | box at 1st box
11 | // ^^ keyword.pikchr
12 | // ^^^ constant.language.pikchr
13 | // ^^^ entity.name.function.pikchr
14 |
15 | <
16 | < `nth -> NTH -> last|previous -> classname`
17 | <
18 | box at 2nd previous box
19 | // ^^ keyword.pikchr
20 | // ^^^ constant.language.pikchr
21 | // ^^^^^^^^ constant.language.pikchr
22 | // ^^^ entity.name.function.pikchr
23 |
24 | <
25 | < `nth -> last|previous -> classname
26 | <
27 |
28 | box at last box
29 | // ^^ keyword.pikchr
30 | // ^^^^ constant.language.pikchr
31 | // ^^^ entity.name.function.pikchr
32 | box at previous box
33 | // ^^^^^^^^ constant.language.pikchr
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: ⭐ Feature request
3 | description: Suggest an idea.
4 | labels: [enhancement]
5 | body:
6 | - type: dropdown
7 | id: desire
8 | attributes:
9 | label: Desire
10 | description: How much do you want this feature?
11 | options:
12 | - 1
13 | - 2
14 | - 3
15 | - 4
16 | - 5
17 | - 6
18 | - 7
19 | - 8
20 | - 9
21 | - 10
22 | validations:
23 | required: true
24 | - type: checkboxes
25 | id: help
26 | attributes:
27 | label: Help
28 | description: How much are you willing to help?. Its okay if you choose none.
29 | options:
30 | - label: Discuss
31 | - label: Test
32 | - label: Code
33 | - type: textarea
34 | attributes:
35 | label: Description
36 | validations:
37 | required: true
38 |
--------------------------------------------------------------------------------
/resources/icons/dark/status-clean.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/resources/icons/light/status-clean.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/docs/dev/release.md:
--------------------------------------------------------------------------------
1 | # How to release vscode-fossil extension
2 |
3 | ## Describe the changes
4 | 1. Update version in `package.json`
5 | 1. Update `CHANGELOG.md`
6 |
7 |
8 | ## Ensure everything is working
9 | 1. Run tests: `npm run test`
10 | 1. Remove out directory: `rm -rf out`
11 | 1. Create package (.vsix file): `npm run package`
12 | 1. Ensure all files are there: `unzip -l fossil-#.#.#.vsix`. There should be FOUR .js files.
13 |
14 |
15 | ## Make commits
16 |
17 | 1. Create a brunch `git switch --create $USER-release-#.#.#`
18 | 1. Make a commit 'release: #.#.#'
19 | 1. Make a pull request
20 | 1. "Merge and rebase" on a successful pull request
21 | 1. Switch to `master`
22 | 1. Tag `git tag v#.#.# && git push origin $_`
23 |
24 |
25 | ## Release
26 | 1. Download .vsix file from github "Releases"*
27 | 1. Upload it to https://marketplace.visualstudio.com/manage/publishers/koog1000
28 | 1. Upload it to https://open-vsx.org/extension/koog1000/fossil
29 |
30 | Storing tokens
--------------------------------------------------------------------------------
/pikchr/test/comments.test.pikchr:
--------------------------------------------------------------------------------
1 | !! SYNTAX TEST "source.pikchr" "all statements must work with commants"
2 |
3 | /**/box
4 | !! <---- comment.block.pikchr
5 | !! ^^^ storage.type.class.pikchr
6 |
7 | /*
8 | !! <-- comment.block.pikchr
9 |
10 | test
11 | !! <---- comment.block.pikchr
12 |
13 | */
14 | !! <-- comment.block.pikchr
15 |
16 | box//test
17 | !! <--- storage.type.class.pikchr
18 | !! ^^^^^^ comment.line.pikchr
19 |
20 | #!
21 | !! <-- comment.line.pikchr
22 | # box
23 | !! ^^^^^ comment.line.pikchr
24 | // TEST:
25 | !! ^^^^^^^^ comment.line.pikchr
26 | /* TEST
27 | !! ^^^^^^^ comment.block.pikchr
28 | */
29 | !! ^^ comment.block.pikchr
30 |
31 | [] // test
32 | !! <- punctuation.bracket.square.begin.pikchr
33 | !! <~- punctuation.bracket.square.end.pikchr
34 | !! ^^^^^^^ comment.line.pikchr
35 |
36 | $hello = 1 // world
37 | !! <------ variable.language.pikchr
38 | !! ^ keyword.operator.assignment.pikchr
39 | !! ^ constant.numeric.pikchr
40 | !! ^^^^^^^^ comment.line.pikchr
41 |
--------------------------------------------------------------------------------
/pikchr/codeblock.pikchr.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "comment": "idea from extensions/markdown-basics/syntaxes/markdown.tmLanguage.json and https://github.com/mjbvz/vscode-fenced-code-block-grammar-injection-example",
3 | "fileTypes": [],
4 | "injectionSelector": "L:text.html.markdown",
5 | "patterns": [
6 | {
7 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(pikchr)(\\s+[^`~]*)?$)",
8 | "name": "markup.fenced_code.block.markdown",
9 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$",
10 | "patterns": [
11 | {
12 | "begin": "(^|\\G)(\\s*)(.*)",
13 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)",
14 | "contentName": "meta.embedded.block.pikchr",
15 | "patterns": [{"include": "source.pikchr"}]
16 | }
17 | ],
18 | "beginCaptures": {
19 | "3": {"name": "punctuation.definition.markdown"},
20 | "4": {"name": "fenced_code.block.language.markdown"},
21 | "5": {"name": "fenced_code.block.language.attributes.markdown"}
22 | },
23 | "endCaptures": {"1": {"name": "punctuation.definition.markdown"}}
24 | }
25 | ],
26 | "scopeName": "markdown.pikchr.codeblock"
27 | }
--------------------------------------------------------------------------------
/esbuild.config.js:
--------------------------------------------------------------------------------
1 | const esbuild = require('esbuild');
2 | const minify = process.argv.includes('--minify');
3 | const sourcemap = process.argv.includes('--sourcemap');
4 |
5 | function buildConfig(entryPoint, outfile) {
6 | return {
7 | minify,
8 | entryPoints: [entryPoint],
9 | bundle: true,
10 | platform: 'node',
11 | sourcemap,
12 | target: 'node18',
13 | format: 'cjs',
14 | external: ['vscode', './gitExport', './praise'],
15 | ...(outfile ? {outfile} : {}),
16 | }
17 | }
18 |
19 | async function main() {
20 | const results = await Promise.all([
21 | esbuild.build(
22 | {...buildConfig('./src/main.ts', 'out/main.js')}
23 | ),
24 | esbuild.build(
25 | {...buildConfig('./src/praise.ts', 'out/praise.js'), external: ['vscode']}
26 | ),
27 | esbuild.build(
28 | {...buildConfig('./src/gitExport.ts', 'out/gitExport.js')}
29 | ),
30 | esbuild.build(
31 | {...buildConfig('./media/preview.ts', 'media/preview.js'), platform:'browser', format: 'iife'}
32 | ),
33 | ])
34 | console.log('fossil extension js files are ready')
35 | }
36 |
37 | main()
38 |
--------------------------------------------------------------------------------
/src/uri.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | 'use strict';
8 |
9 | import { Uri } from 'vscode';
10 | import { FossilCheckin } from './openedRepository';
11 |
12 | export interface FossilUriParams {
13 | // full filesystem path
14 | path: string;
15 | checkin: FossilCheckin;
16 | }
17 |
18 | export function fromFossilUri(uri: Uri): FossilUriParams {
19 | return JSON.parse(uri.query);
20 | }
21 |
22 | export function toFossilUri(uri: Uri, checkin: FossilCheckin = 'current'): Uri {
23 | const params: FossilUriParams = {
24 | path: uri.fsPath,
25 | checkin: checkin,
26 | };
27 | return uri.with({
28 | scheme: 'fossil',
29 | path: uri.path,
30 | query: JSON.stringify(params),
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/pikchr/test/position.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "all checks for 'position' tree"
2 |
3 | <
4 | < test for `position -> expr -> ',' -> expr`
5 | <
6 | box at 1 , 1
7 | // ^^ keyword.pikchr
8 | // ^ ^ constant.numeric.pikchr
9 | // ^ punctuation.separator.pikchr
10 |
11 | <
12 | < `position -> place`
13 | <
14 | box at 1st box
15 | // ^^ keyword.pikchr
16 | // ^^^ constant.language.pikchr
17 | // ^^^ entity.name.function.pikchr
18 |
19 | <
20 | < `position -> place -> + -> expr -> ',' -> expr`
21 | <
22 | box at 3rd box + 0.1,9.3
23 | // ^^ keyword.pikchr
24 | // ^^^ constant.language.pikchr
25 | // ^^^ entity.name.function.pikchr
26 | // ^ keyword.operator.arithmetic.pikchr
27 | // ^^^ ^^^ constant.numeric.pikchr
28 | // ^ punctuation.separator.pikchr
29 |
30 | <
31 | < `position -> ( -> position -> )`
32 | <
33 | circle at (-0.055, -0.25)
34 | // ^^ keyword.pikchr
35 | // ^ punctuation.parenthesis.begin.pikchr
36 | // ^ ^ keyword.operator.arithmetic.pikchr
37 | // ^^^^^ ^^^^ constant.numeric.pikchr
38 | // ^ punctuation.parenthesis.end.pikchr
39 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 - present Keenan Kugler
4 | Original Copyright (c) 2017 - present Ben Crowl
5 |
6 | All rights reserved.
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // Available variables which can be used inside of strings.
2 | // ${workspaceRoot}: the root folder of the team
3 | // ${file}: the current opened file
4 | // ${fileBasename}: the current opened file's basename
5 | // ${fileDirname}: the current opened file's dirname
6 | // ${fileExtname}: the current opened file's extension
7 | // ${cwd}: the current working directory of the spawned process
8 |
9 | // A task runner that calls a custom npm script that compiles the extension.
10 | {
11 | "version": "2.0.0",
12 |
13 | // we want to run npm
14 | "command": "npm",
15 |
16 | // the command is a shell script
17 | "type":"shell",
18 |
19 | // show the output window always.
20 | "presentation": {
21 | "echo": true,
22 | "reveal": "always",
23 | "focus": false,
24 | "panel": "shared",
25 | "showReuseMessage": true,
26 | "clear": false
27 | },
28 |
29 | // we run the custom script "compile" as defined in package.json
30 | "args": ["run", "compile", "--loglevel", "info"],
31 |
32 | // The tsc compiler is started in watching mode
33 | "isBackground": true,
34 |
35 | // use the standard tsc in watch mode problem matcher to find compile problems in the output.
36 | "problemMatcher": "$tsc-watch"
37 | }
--------------------------------------------------------------------------------
/src/test/suite/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as Mocha from 'mocha';
3 | import * as fs from 'fs';
4 |
5 | export function run(testsRoot: string): Promise {
6 | // Create the mocha test
7 | const mocha = new Mocha({
8 | ui: 'tdd',
9 | });
10 |
11 | return new Promise((c, e) => {
12 | fs.readdir(testsRoot, { withFileTypes: true }, (err, files) => {
13 | /* c8 ignore next 3 */
14 | if (err) {
15 | return e(err);
16 | }
17 |
18 | // Add files to the test suite
19 | files
20 | .filter(f => f.isFile() && f.name.endsWith('.test.js'))
21 | .forEach(f => mocha.addFile(path.resolve(testsRoot, f.name)));
22 |
23 | try {
24 | // Run the mocha test
25 | mocha.run(failures => {
26 | /* c8 ignore next 2 */
27 | if (failures > 0) {
28 | e(new Error(`${failures} tests failed.`));
29 | } else {
30 | c();
31 | }
32 | });
33 | /* c8 ignore next 3 */
34 | } catch (err) {
35 | e(err);
36 | }
37 | });
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/pikchr/test/homepage.example.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "https://pikchr.org/home/doc/trunk/homepage.md"
2 |
3 | arrow right 200% "Markdown" "Source"
4 | // <----- storage.type.class.pikchr
5 | // ^^^^^ support.constant.edge.pikchr
6 | // ^^^ constant.numeric.pikchr
7 | // ^ keyword.other.unit.percentage.pikchr
8 | // ^^^^^^^^^^ ^^^^^^^^ string.quoted.double.pikchr
9 |
10 | box rad 10px "Markdown" "Formatter" "(markdown.c)" fit
11 | // <--- storage.type.class.pikchr
12 | // ^ -storage.type.class.pikchr
13 | // ^^^ support.constant.property-value.pikchr
14 | // ^^^^ constant.numeric.pikchr
15 | // ^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^ string.quoted.double.pikchr
16 | // ^^^ entity.name.tag.pikchr
17 |
18 | arrow <-> down 70% from last box.s
19 | // <--- storage.type.class.pikchr
20 | // ^^^ entity.name.tag.pikchr
21 | // ^^^^ support.constant.edge.pikchr
22 | // ^^ constant.numeric.pikchr
23 | // ^ keyword.other.unit.percentage.pikchr
24 | // ^^^^ keyword.pikchr
25 | // ^^^^ constant.language.pikchr
26 | // ^^^ entity.name.function.pikchr
27 | // ^ punctuation.separator.period.pikchr
28 | // ^ constant.language.pikchr
29 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "@typescript-eslint",
6 | "prettier"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended" //,
12 | //"prettier/@typescript-eslint",
13 | //"plugin:prettier/recommended"
14 | ],
15 | "rules": {
16 | "prettier/prettier": "error",
17 | "no-irregular-whitespace": [
18 | "error",
19 | {
20 | "skipTemplates": true
21 | }
22 | ],
23 | "@typescript-eslint/no-unused-vars": [
24 | "error",
25 | {
26 | "varsIgnorePattern": "^_|^toString$",
27 | "argsIgnorePattern": "^_"
28 | }
29 | ],
30 | "no-cond-assign": 2,
31 | "no-constant-condition": 0,
32 | "no-inner-declarations": 2,
33 | "no-prototype-builtins": 0,
34 | "@typescript-eslint/no-explicit-any": 0,
35 | "@typescript-eslint/no-non-null-assertion": 0,
36 | "@typescript-eslint/ban-types": 0
37 | },
38 | "ignorePatterns": [
39 | "node_modules/",
40 | "out/",
41 | "coverage/",
42 | "resources/",
43 | ".vscode/"
44 | ]
45 | }
--------------------------------------------------------------------------------
/pikchr/test/boolproperty.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "Properties with no argument"
2 |
3 | arrow cw
4 | // ^^ source.pikchr entity.name.tag.pikchr
5 | box ccw
6 | // ^^^ source.pikchr entity.name.tag.pikchr
7 | box <-
8 | // ^^ source.pikchr entity.name.tag.pikchr
9 | box ->
10 | // ^^ source.pikchr entity.name.tag.pikchr
11 | box <->
12 | // ^^^ source.pikchr entity.name.tag.pikchr
13 | arrow invis
14 | // ^^^^^ source.pikchr entity.name.tag.pikchr
15 | box invisible
16 | // ^^^^^^^^^ source.pikchr entity.name.tag.pikchr
17 | box thick
18 | // ^^^^^ source.pikchr entity.name.tag.pikchr
19 | box thin
20 | // ^^^^ source.pikchr entity.name.tag.pikchr
21 | arrow solid
22 | // ^^^^^ source.pikchr entity.name.tag.pikchr
23 | cylinder same
24 | // <-------- storage.type.class.pikchr
25 | // ^ source.pikchr
26 | // ^^^^ source.pikchr entity.name.tag.pikchr
27 | box same;
28 | // <--- storage.type.class.pikchr
29 | // ^^^^ source.pikchr entity.name.tag.pikchr
30 | arrow →
31 | // ^^^^^^ entity.name.tag.pikchr
32 | arrow →
33 | // ^^^^^^^^^^^^ entity.name.tag.pikchr
34 | arrow ←
35 | // ^^^^^^ entity.name.tag.pikchr
36 | arrow ←
37 | // ^^^^^^^^^^^ entity.name.tag.pikchr
38 | arrow ↔
39 | // ^^^^^^^^^^^^^^^^ entity.name.tag.pikchr
40 | arrow ←
41 | // ^ entity.name.tag.pikchr
42 | arrow →
43 | // ^ entity.name.tag.pikchr
44 | arrow ↔
45 | // ^ entity.name.tag.pikchr
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🕷️ Bug report
2 | description: Report errors or unexpected behavior.
3 | labels: [bug]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for reporting issues of fossil-scm vscode extension!
9 |
10 | To make it easier for us to help you please enter detailed information below.
11 | - type: dropdown
12 | id: desire
13 | attributes:
14 | label: Desire
15 | description: How much do you want this to be fixed?
16 | options:
17 | - 1
18 | - 2
19 | - 3
20 | - 4
21 | - 5
22 | - 6
23 | - 7
24 | - 8
25 | - 9
26 | - 10
27 | validations:
28 | required: true
29 | - type: checkboxes
30 | id: help
31 | attributes:
32 | label: Help
33 | description: How much are you willing to help?. Its okay if you choose none.
34 | options:
35 | - label: Discuss
36 | - label: Test
37 | - label: Code
38 | - type: textarea
39 | attributes:
40 | label: Steps to reproduce
41 | placeholder: |
42 | 1.
43 | 2.
44 | 3.
45 | - type: textarea
46 | attributes:
47 | label: Description
48 | validations:
49 | required: true
50 | - type: input
51 | attributes:
52 | label: OS
53 | - type: input
54 | attributes:
55 | label: Fossil version
56 | - type: input
57 | attributes:
58 | label: Vscode/Vscodium version
59 |
--------------------------------------------------------------------------------
/src/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs/promises';
3 | import * as os from 'os';
4 |
5 | import { runTests } from '@vscode/test-electron';
6 |
7 | async function main() {
8 | try {
9 | // The folder containing the Extension Manifest package.json
10 | // Passed to `--extensionDevelopmentPath`
11 | const extensionDevelopmentPath = path.resolve(__dirname, '../../');
12 |
13 | // The path to the extension test runner script
14 | // Passed to --extensionTestsPath
15 | const extensionTestsPath = path.resolve(__dirname, './suite');
16 |
17 | const testWorkspace = path.resolve(os.tmpdir(), './test_repo');
18 | await fs.mkdir(testWorkspace, { recursive: true });
19 | console.log(`testWorkspace: '${testWorkspace}'`);
20 | console.log(`extensionDevelopmentPath: '${extensionDevelopmentPath}'`);
21 |
22 | // Download VS Code, unzip it and run the integration test
23 | await runTests({
24 | extensionDevelopmentPath,
25 | extensionTestsPath,
26 | launchArgs: [testWorkspace, '--disable-extensions', '--no-sandbox'],
27 | // Fix version to stop tests failing as time goes by. See:
28 | // https://github.com/microsoft/vscode-test/issues/221
29 | version: '1.79.2',
30 | });
31 | /* c8 ignore next 4 */
32 | } catch (err) {
33 | console.error('Failed to run tests', err);
34 | process.exit(1);
35 | }
36 | }
37 |
38 | main();
39 |
--------------------------------------------------------------------------------
/.github/workflows/fossil.yml:
--------------------------------------------------------------------------------
1 | name: Fossil
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | tags:
7 | - v*
8 | pull_request:
9 | branches: [ "master" ]
10 |
11 | jobs:
12 | main:
13 | name: Run Fossil Tests
14 | runs-on: ubuntu-latest
15 | permissions:
16 | contents: write
17 | security-events: write
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | - name: Set up Node
22 | uses: actions/setup-node@v4
23 | with:
24 | cache: 'npm'
25 | node-version: 18
26 | - name: Install dependencies
27 | run: npm ci
28 | - name: Run ESLint
29 | run: npm run lint --
30 | --format @microsoft/eslint-formatter-sarif
31 | --output-file eslint-results.sarif
32 | continue-on-error: true
33 |
34 | - name: Upload analysis results to GitHub
35 | uses: github/codeql-action/upload-sarif@v3
36 | with:
37 | sarif_file: eslint-results.sarif
38 | wait-for-processing: true
39 |
40 | - name: Install fossil
41 | # we need 'xvfb libnss3-dev libgtk-3-dev libasound2'
42 | # when running locally using `gh act` command
43 | run: sudo apt-get install -y fossil
44 |
45 | - name: Run code tests and coverage
46 | run: xvfb-run -a npm run coverage-ci $GITHUB_STEP_SUMMARY
47 |
48 | - name: Run pikchr grammar tests
49 | run: npm run grammar-test
50 |
51 | - name: Package
52 | run: rm -rf out && npm run package
53 |
54 | - name: Release
55 | uses: softprops/action-gh-release@v2
56 | if: startsWith(github.ref, 'refs/tags/v')
57 | with:
58 | files: fossil-*.*.*.vsix
59 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.1.0",
4 | "configurations": [
5 | {
6 | "name": "Fossil Extension (--disable-extensions)",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "runtimeExecutable": "${execPath}",
10 | "args": [
11 | "--extensionDevelopmentPath=${workspaceRoot}",
12 | "--disable-extensions"
13 | ],
14 | "sourceMaps": true,
15 | "outFiles": [
16 | "${workspaceRoot}/out/*.js"
17 | ],
18 | "preLaunchTask": "npm"
19 | },
20 | {
21 | "name": "Fossil Extension",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "runtimeExecutable": "${execPath}",
25 | "args": [
26 | "--extensionDevelopmentPath=${workspaceRoot}",
27 | ],
28 | "sourceMaps": true,
29 | "outFiles": [
30 | "${workspaceRoot}/out/*.js"
31 | ],
32 | "preLaunchTask": "npm"
33 | },
34 | {
35 | "name": "Fossil Tests",
36 | "type": "extensionHost",
37 | "request": "launch",
38 | "runtimeExecutable": "${execPath}",
39 | "args": [
40 | "--extensionDevelopmentPath=${workspaceFolder}",
41 | "--extensionTestsPath=${workspaceFolder}/out/test/suite",
42 | "${workspaceFolder}/out/test/test_repo",
43 | "--disable-extensions"
44 | ],
45 | "outFiles": [
46 | "${workspaceFolder}/out/test/**/*.js"
47 | ],
48 | "preLaunchTask": "npm"
49 | }
50 | ]
51 | }
--------------------------------------------------------------------------------
/media/preview.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: var(--markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif);
3 | font-size: var(--markdown-font-size, 14px);
4 | padding: 0 26px;
5 | line-height: var(--markdown-line-height, 22px);
6 | word-wrap: break-word;
7 | }
8 | .oldFossil .pikchr-svg, .oldFossil svg.pikchr {
9 | background-color: white;
10 | color: #333;
11 | }
12 | .pikchr-src {
13 | display: none;
14 | }
15 |
16 | body {
17 | padding-top: 1em;
18 | margin-bottom: calc(100vh - 22px);
19 | }
20 | h1, h2, h3, h4, h5, h6,
21 | p, ol, ul, pre {
22 | margin-top: 0;
23 | }
24 |
25 | h2, h3, h4, h5, h6 {
26 | font-weight: normal;
27 | margin-bottom: 0.2em;
28 | }
29 | select:focus,
30 | textarea:focus {
31 | outline: 1px solid -webkit-focus-ring-color;
32 | outline-offset: -1px;
33 | }
34 |
35 | p {
36 | margin-bottom: 0.7em;
37 | }
38 |
39 | ul,
40 | ol {
41 | margin-bottom: 0.7em;
42 | }
43 |
44 | hr {
45 | border: 0;
46 | height: 2px;
47 | border-bottom: 2px solid;
48 | }
49 |
50 | h1 {
51 | padding-bottom: 0.3em;
52 | line-height: 1.2;
53 | border-bottom-width: 1px;
54 | border-bottom-style: solid;
55 | font-weight: normal;
56 | }
57 |
58 | table {
59 | border-collapse: collapse;
60 | margin-bottom: 0.7em;
61 | }
62 |
63 | th {
64 | text-align: left;
65 | border-bottom: 1px solid;
66 | }
67 |
68 | th,
69 | td {
70 | padding: 5px 10px;
71 | }
72 |
73 | table > tbody > tr + tr > td {
74 | border-top: 1px solid;
75 | }
76 |
77 | blockquote {
78 | margin: 0 7px 0 5px;
79 | padding: 0 16px 0 10px;
80 | border-left-width: 5px;
81 | border-left-style: solid;
82 | }
83 |
84 | code {
85 | font-family: var(--vscode-editor-font-family, "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace);
86 | font-size: 1em;
87 | line-height: 1.357em;
88 | }
89 |
--------------------------------------------------------------------------------
/docs/cloning.md:
--------------------------------------------------------------------------------
1 | # Cloning with Fossil in VS Code
2 |
3 | 
4 |
5 | Cloning is possible from the fossil extension through the command palette
6 | (Ctrl-Shift-P). Search for `Fossil: Clone`.
7 |
8 | ### Fossil Repository
9 | You'll first be prompted to enter the repository URI. Enter the entire
10 | URI, including the scheme (ex. `http://` , `file://` , `https://` , etc)
11 |
12 | As an Example:
13 | 
14 |
15 | Hitting `Esc` will abort the cloning process
16 |
17 | ### Username
18 | 
19 |
20 | You will be prompted for your repository authentication user name.
21 | If you do not have a repository user name leave this blank.
22 | Because a user name is not required, hitting `Esc` at this step does not
23 | abort the cloning process.
24 |
25 | ### User Authentication
26 | 
27 |
28 | If you entered a username you will be prompted to enter your user
29 | authentication (password). Aborting here (by hitting `Esc`) does not
30 | abort the cloning process but falls back to an anonymous clone (no
31 | usernname and no authentication).
32 |
33 | ### Parent Directory
34 | 
35 |
36 | Enter the root directory for the cloned repo. If VS Code is opened to a
37 | folder the parent root directory will default to the currently opened
38 | folder, otherwise it will be blank. Hitting `Esc` here will abort the
39 | cloning process.
40 |
41 | ### Input Prompts
42 | Various prompts may come up while cloning.
43 | If these prompts are unclear then abort by hitting `Esc` and run your
44 | `fossil clone` command from the built-in terminal (Ctrl+`).
45 |
46 | Most notably this rather ugly prompt about SSL failure
47 | can be read about on the
48 | [Fossil SSL Certificate](https://fossil-scm.org/home/doc/trunk/www/ssl.wiki#certs)
49 | wikipage:
50 | 
51 |
--------------------------------------------------------------------------------
/src/iterators.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | function* filter(
8 | it: IterableIterator,
9 | condition: (t: T, i: number) => boolean
10 | ): IterableIterator {
11 | let i = 0;
12 | for (const t of it) {
13 | if (condition(t, i++)) {
14 | yield t;
15 | }
16 | }
17 | }
18 |
19 | function* map(
20 | it: IterableIterator,
21 | fn: (t: T, i: number) => R
22 | ): IterableIterator {
23 | let i = 0;
24 | for (const t of it) {
25 | yield fn(t, i++);
26 | }
27 | }
28 |
29 | export interface FunctionalIterator extends Iterable {
30 | filter(condition: (t: T, i: number) => boolean): FunctionalIterator;
31 | map(fn: (t: T, i: number) => R): FunctionalIterator;
32 | toArray(): T[];
33 | }
34 |
35 | class FunctionalIteratorImpl implements FunctionalIterator {
36 | constructor(private iterator: IterableIterator) {}
37 |
38 | filter(condition: (t: T, i: number) => boolean): FunctionalIterator {
39 | return new FunctionalIteratorImpl(filter(this.iterator, condition));
40 | }
41 |
42 | map(fn: (t: T, i: number) => R): FunctionalIterator {
43 | return new FunctionalIteratorImpl(map(this.iterator, fn));
44 | }
45 |
46 | toArray(): T[] {
47 | return Array.from(this.iterator);
48 | }
49 |
50 | [Symbol.iterator](): IterableIterator {
51 | return this.iterator;
52 | }
53 | }
54 |
55 | export function iterate(
56 | obj: T[] | IterableIterator
57 | ): FunctionalIterator {
58 | if (Array.isArray(obj)) {
59 | return new FunctionalIteratorImpl(obj[Symbol.iterator]());
60 | }
61 |
62 | return new FunctionalIteratorImpl(obj);
63 | }
64 |
--------------------------------------------------------------------------------
/src/test/suite/extension.test.ts:
--------------------------------------------------------------------------------
1 | import { before, afterEach, Suite } from 'mocha';
2 | import * as sinon from 'sinon';
3 | import {
4 | TagSuite,
5 | StatusSuite,
6 | CleanSuite,
7 | FileSystemSuite,
8 | DiffSuite,
9 | } from './commandSuites';
10 | import { MergeSuite } from './mergeSuite';
11 | import { cleanRoot, fossilInit, fossilOpen } from './common';
12 | import { utilitiesSuite } from './utilitiesSuite';
13 | import { resourceActionsSuite } from './resourceActionsSuite';
14 | import { timelineSuite } from './timelineSuite';
15 | import { CommitSuite } from './commitSuite';
16 | import { QualityOfLifeSuite as QualityOfLifeSuite } from './qualityOfLifeSuite';
17 | import { PatchSuite, StageSuite, StashSuite, UpdateSuite } from './stateSuite';
18 | import { RenameSuite } from './renameSuite';
19 | import { BranchSuite } from './branchSuite';
20 | import { RevertSuite } from './revertSuite';
21 | import { GitExportSuite } from './gitExportSuite';
22 | import { StatusBarSuite } from './statusBarSuite';
23 | import { workspace } from 'vscode';
24 |
25 | suite('Fossil.OpenedRepo', function (this: Suite) {
26 | this.ctx.sandbox = sinon.createSandbox();
27 | this.ctx.workspaceUri = workspace.workspaceFolders![0].uri;
28 |
29 | before(async () => {
30 | this.timeout(5555);
31 | await cleanRoot();
32 | await fossilInit(this.ctx.sandbox);
33 | await fossilOpen(this.ctx.sandbox);
34 | });
35 |
36 | suite('Utilities', utilitiesSuite);
37 | suite('Update', UpdateSuite);
38 | suite('Status Bar', StatusBarSuite);
39 | suite('Resource Actions', resourceActionsSuite);
40 | suite('Timeline', timelineSuite);
41 | suite('Revert', RevertSuite);
42 | suite('Stash', StashSuite);
43 | suite('Branch', BranchSuite);
44 | suite('Commit', CommitSuite);
45 | suite('Patch', PatchSuite);
46 | suite('Merge', MergeSuite);
47 | suite('Tag', TagSuite);
48 | suite('Status', StatusSuite);
49 | suite('Stage', StageSuite);
50 | suite('Rename', RenameSuite);
51 | suite('Clean', CleanSuite);
52 | suite('FileSystem', FileSystemSuite);
53 | suite('Diff', DiffSuite);
54 | suite('Quality of Life', QualityOfLifeSuite);
55 | suite('Git Export', GitExportSuite);
56 |
57 | afterEach(() => {
58 | this.ctx.sandbox.restore();
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/media/preview.ts:
--------------------------------------------------------------------------------
1 | const $ = (s: string) => document.getElementById(s);
2 | const vscode = acquireVsCodeApi<{ uri: string }>();
3 | type Nodes = NodeListOf;
4 |
5 | function htmlToElements(html: string): Nodes {
6 | const template = document.createElement('template');
7 | template.innerHTML = html;
8 | return template.content.childNodes;
9 | }
10 |
11 | function find_diff(olds: Nodes, news: Nodes): ChildNode {
12 | const n = Math.max(olds.length, news.length);
13 | for (let i = 0; i < n; ++i) {
14 | if (olds[i] && news[i]) {
15 | if (
16 | olds[i].isEqualNode(news[i]) ||
17 | news[i].nodeType == Node.COMMENT_NODE
18 | ) {
19 | continue;
20 | } else {
21 | if (!news[i].childNodes.length) {
22 | return news[i];
23 | }
24 | return find_diff(olds[i].childNodes, news[i].childNodes);
25 | }
26 | } else {
27 | return news[i];
28 | }
29 | }
30 | return news[0];
31 | }
32 |
33 | function scrollIntoViewIfNeeded(target: ChildNode): void {
34 | while (target.nodeType != target.ELEMENT_NODE) {
35 | target = target.previousSibling ?? target.parentElement;
36 | }
37 | const rect = (target as Element).getBoundingClientRect();
38 | if (rect.bottom > window.innerHeight || rect.top < 0) {
39 | (target as Element).scrollIntoView({
40 | behavior: 'auto',
41 | block: 'center',
42 | inline: 'center',
43 | });
44 | }
45 | }
46 |
47 | window.addEventListener(
48 | 'message',
49 | (event: MessageEvent<{ html: string; uri: string }>): void => {
50 | const news = htmlToElements(event.data.html);
51 | const content = $('fossil-preview-content');
52 | const diff_el = find_diff(content.childNodes, news);
53 | content.replaceChildren(...news);
54 | vscode.setState({ uri: event.data.uri });
55 | if (diff_el) {
56 | scrollIntoViewIfNeeded(diff_el);
57 | }
58 | }
59 | );
60 |
61 | window.addEventListener('keydown', e => {
62 | if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
63 | vscode.postMessage({ action: 'save' });
64 | }
65 | });
66 |
67 | window.addEventListener('load', () => {
68 | vscode.postMessage({ action: 'update' });
69 | });
70 |
--------------------------------------------------------------------------------
/pikchr/test/attribute.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "checks for 'attribute' tree"
2 |
3 | arrow from C6 to C3P chop
4 | // ^^^^ ^^ keyword.pikchr
5 | // ^^ ^^^ variable.language.place.pikchr
6 | // ^^^^ entity.name.tag.pikchr
7 | arrow right then down
8 | // ^^^^^ ^^^^ support.constant.edge.pikchr
9 | // ^^^^ keyword.pikchr
10 | // ^ -keyword.pikchr
11 | circle at bot of first arrow
12 | // ^^^ support.constant.edge.pikchr
13 | circle at bottom of first arrow
14 | // ^^^^^^ support.constant.edge.pikchr
15 | circle at c of first arrow
16 | // ^ support.constant.edge.pikchr
17 | circle at center of first arrow
18 | // ^^^^^^ support.constant.edge.pikchr
19 | circle at e of first arrow
20 | // ^ support.constant.edge.pikchr
21 | circle at east of first arrow
22 | // ^^^^ support.constant.edge.pikchr
23 | circle at end of first arrow
24 | // ^^^ support.constant.edge.pikchr
25 | circle at left of first arrow
26 | // ^^^^ support.constant.edge.pikchr
27 | circle at n of first arrow
28 | // ^ support.constant.edge.pikchr
29 | circle at ne of first arrow
30 | // ^^ support.constant.edge.pikchr
31 | circle at north of first arrow
32 | // ^^^^^ support.constant.edge.pikchr
33 | circle at nw of first arrow
34 | // ^^ support.constant.edge.pikchr
35 | circle at right of first arrow
36 | // ^^^^^ support.constant.edge.pikchr
37 | circle at s of first arrow
38 | // ^ support.constant.edge.pikchr
39 | circle at se of first arrow
40 | // ^^ support.constant.edge.pikchr
41 | circle at south of first arrow
42 | // ^^^^^ support.constant.edge.pikchr
43 | circle at start of first arrow
44 | // ^^^^^ support.constant.edge.pikchr
45 | circle at sw of first arrow
46 | // ^^ support.constant.edge.pikchr
47 | circle at t of first arrow
48 | // ^ support.constant.edge.pikchr
49 | circle at top of first arrow
50 | // ^^^ support.constant.edge.pikchr
51 | circle at w of first arrow
52 | // ^ support.constant.edge.pikchr
53 | circle at west of first arrow
54 | // ^^^^ support.constant.edge.pikchr
55 | cylinder "B" at 5cm heading 125 from A
56 | // <-------- storage.type.class.pikchr
57 | // ^^^ string.quoted.double.pikchr
58 | // ^^ ^^^^^^^ ^^^^ keyword.pikchr
59 | // ^^^ ^^^ constant.numeric.pikchr
60 | // ^ variable.language.place.pikchr
61 |
--------------------------------------------------------------------------------
/docs/dev/scenarios.md:
--------------------------------------------------------------------------------
1 | # Scenarios for README.md videos and images
2 |
3 |
4 | ## Software
5 |
6 | * Chronicler plugin for VScode is used to capture video
7 |
8 | ```json
9 | "chronicler.recording-defaults": {
10 | "animatedGif": false,
11 | "fps": 5,
12 | "gifScale": 1.0,
13 | "countdown": 1,
14 | "flags": {
15 | "pix_fmt": "rgb24",
16 | "c:v": "png",
17 | "vcodec": "rawvideo",
18 | "vframes": "1"
19 | }
20 | }
21 | ```
22 |
23 | * Extract frames with `ffmpeg -i fossil-1677169739283.mp4 'out%06d.png'`
24 | * Use professional software to generate `gif`
25 |
26 |
27 | ## Images
28 |
29 | ### Setup
30 |
31 | * Color theme: `Dark+`
32 | * Window size: `1168 x 720`
33 | * Set using you OS or window manager api
34 | * Zoom level: `1`
35 | * Set zoom level using `ctrl+,`
36 | * All extensions except *Chronicler* and *Fossil* are disabled
37 | * Use latest Fossil extension
38 |
39 | ### `fossil.png`
40 |
41 | * Opened official fossil repository
42 | * Source control panel is visible
43 | * Unresolved conflicts: some
44 | * Changes:
45 | * Added: some
46 | * Modified: some
47 | * Untracked files:
48 | * Staged files:
49 | ```bash
50 | #!/usr/bin/env bash
51 | # example
52 | fossil clone https://fossil-scm.org/home fossil.fossil
53 | fossil open fossil.fossil
54 | printf 'modification for fossil.png\n' >> www/quotes.wiki
55 | fossil commit -m "commit for conflict"
56 | fossil up forumpost-locking --nosync
57 | touch autoconfig.h config.log a_new_file.md
58 | fossil add a_new_file.md
59 | printf 'conflict for fossil.png\n' >> www/quotes.wiki
60 | printf 'modification for fossil.png\n' >> BUILD.txt
61 | fossil merge trunk
62 | ```
63 | * Status bar shows current branch as `trunk+`
64 | * Editor is split horizontally
65 | * Left:
66 | * `pikchr.md`
67 | * `quotes.wiki`
68 | * `fossil_prompt.wiki`
69 | * Right:
70 | * `pikchr.md` - preview with pikchr diagram visible
71 |
72 | ### `fossil-diff.gif` (View file changes)
73 |
74 | 1. Same setup as `fossil.png`, but all files are closed
75 | 2. Click on a file that shows diff in quotes.wiki
76 | 3. Highlight the diff with funny text:
77 | > You should give a try to this fossil VSCode plugin
78 | 4. End
79 |
80 | ### `init.gif` (Initialize a new repo)
81 |
82 | 1. Status: No repository is opened, Explorer view is selected
83 | 2. Source Control panel is opened
84 | 3. Fossil icon is clicked
85 | 4. Fossil file location is selected
86 | 5. Project name is entered
87 | 6. Project description is entered
88 | 7. "Would you like to open the cloned repository?" yes
89 | 8. End
90 |
91 | ### `change-branch.gif` (Update to a branch/tag)
92 |
93 | 1. Status: Fossil repo is opened
94 | 2. Branch name is status bar is clicked
95 | 3. A branch is selected
96 | 4. End
97 |
--------------------------------------------------------------------------------
/package.nls.json:
--------------------------------------------------------------------------------
1 | {
2 | "command.clone": "Clone Fossil Repository",
3 | "command.init": "Initialize Fossil Repository",
4 | "command.open": "Open Fossil Repository",
5 | "command.close": "Close Repository",
6 | "command.openUI": "Open web UI",
7 | "command.refresh": "Refresh",
8 | "command.openChange": "Open Changes",
9 | "command.openFile": "Open File",
10 | "command.openFiles": "Open Files",
11 | "command.stage": "Stage Changes",
12 | "command.stageAll": "Stage All Changes",
13 | "command.unstage": "Unstage Changes",
14 | "command.unstageAll": "Unstage All Changes",
15 | "command.clean": "Delete Extras",
16 | "command.revert": "Discard Changes",
17 | "command.revertAll": "Discard All Changes",
18 | "command.commit": "Commit",
19 | "command.commitStaged": "Commit Staged",
20 | "command.commitAll": "Commit All",
21 | "command.commitBranch": "Commit Creating New Branch...",
22 | "command.sync": "Sync",
23 | "command.undo": "Undo",
24 | "command.update": "Update",
25 | "command.redo": "Redo",
26 | "command.branchChange": "Change Branch / Update to...",
27 | "command.branch": "Create Branch...",
28 | "command.pull": "Pull",
29 | "command.push": "Push",
30 | "command.pushTo": "Push to...",
31 | "command.add": "Add Files",
32 | "command.addAll": "Add All Untracked Files",
33 | "command.addFilePicker": "Select Files to Add",
34 | "command.ignore": "Add to ignore-glob",
35 | "command.forget": "Forget Files",
36 | "command.relocate": "Select New File Location",
37 | "command.showOutput": "Show fossil output",
38 | "command.fileLog": "Show file history...",
39 | "command.render": "Preview Using Fossil Renderer",
40 | "command.wikiCreate": "Publish as Fossil Wiki or Technote",
41 | "command.log": "Log...",
42 | "command.merge": "Merge into working directory...",
43 | "command.integrate": "Integrate into working directory...",
44 | "command.cherrypick": "Cherry-pick into working directory...",
45 | "config.path": "Path to the `fossil` executable (only required if auto-detection fails)",
46 | "config.username": "The username associated with each commit (`--user-override` argument, only required if different from the user that originally cloned the repo).",
47 | "config.defaultUsername": "Make the default user be USER (`--user` option)",
48 | "config.autoSyncInterval": "The duration, in seconds, between each background `fossil sync` operation. 0 to disable.",
49 | "config.autoRefresh": "Whether auto refreshing is enabled",
50 | "config.enableRenaming": "Show rename request after a file was renamed in UI",
51 | "config.enableLongCommitWarning": "Whether long commit messages should be warned about",
52 | "submenu.commit": "Commit",
53 | "submenu.merge": "Merge",
54 | "submenu.patch": "Patch",
55 | "submenu.stash": "Stash",
56 | "submenu.timeline": "Timeline"
57 | }
--------------------------------------------------------------------------------
/src/throttlingQueue.ts:
--------------------------------------------------------------------------------
1 | type Task = {
2 | action: () => Promise;
3 | key: string;
4 | resolve: (value: any) => void;
5 | reject: (reason?: any) => void;
6 | };
7 |
8 | class TPromise extends Promise {
9 | public newer?: Promise;
10 | }
11 |
12 | /**
13 | * Run tasks sequentially, allows throttling by key
14 | */
15 | export class ThrottlingQueue {
16 | private readonly _items: Task[] = [];
17 | private readonly _keys = new Map>();
18 | private _loopRunning: boolean = false;
19 |
20 | enqueue(action: () => Promise, key: string): Promise {
21 | const existing = this._keys.get(key);
22 | if (existing) {
23 | if (!existing.newer) {
24 | existing.newer = new Promise((resolve, reject) => {
25 | this._items.push({ action, resolve, reject, key });
26 | });
27 | }
28 | return existing.newer;
29 | }
30 | const promise = this._new(action, key);
31 | this._keys.set(key, promise);
32 | return promise;
33 | }
34 |
35 | private _new(action: () => Promise, key: string): Promise {
36 | return new Promise((resolve, reject) => {
37 | this._items.push({ action, resolve, reject, key });
38 | this._task_loop();
39 | });
40 | }
41 |
42 | private _delete(key: string): void {
43 | const promise = this._keys.get(key)!;
44 | this._keys.delete(key);
45 | if (promise.newer) {
46 | this._keys.set(key, promise!.newer);
47 | }
48 | }
49 |
50 | private async _task_loop(): Promise {
51 | if (!this._loopRunning) {
52 | const item = this._items.shift();
53 |
54 | if (item) {
55 | this._loopRunning = true;
56 | try {
57 | const payload = await item.action();
58 | // this._loopRunning = false;
59 | item.resolve(payload);
60 | } catch (reason) {
61 | // this._loopRunning = false;
62 | item.reject(reason);
63 | } finally {
64 | this._delete(item.key);
65 | this._loopRunning = false;
66 | this._task_loop();
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
73 | type ObjectOfType = {
74 | [K in keyof T]: T[K] extends U ? K : never;
75 | };
76 |
77 | type KeysOfType = ObjectOfType[keyof T];
78 |
79 | export function queue>(
80 | queue_name: KeysOfType,
81 | queueKey: string
82 | ) {
83 | return function (
84 | target: This,
85 | key: string,
86 | descriptor: TypedPropertyDescriptor<
87 | (this: This, ...args: any[]) => Promise
88 | >
89 | ) {
90 | const fn = descriptor.value!;
91 |
92 | descriptor.value = function (this: This, ...args: any[]): Promise {
93 | return this[queue_name].enqueue(fn.bind(this, ...args), queueKey);
94 | };
95 | };
96 | }
97 |
--------------------------------------------------------------------------------
/src/fossilFinder.ts:
--------------------------------------------------------------------------------
1 | import * as cp from 'child_process';
2 | import type { Distinct } from './openedRepository';
3 | import type {
4 | FossilExecutablePath,
5 | FossilStdOut,
6 | FossilVersion,
7 | } from './fossilExecutable';
8 | import { localize } from './main';
9 | import { LogOutputChannel } from 'vscode';
10 |
11 | export type UnvalidatedFossilExecutablePath = Distinct<
12 | string,
13 | 'unvalidated fossil executable path' | 'fossil executable path'
14 | >;
15 |
16 | export interface FossilExecutableInfo {
17 | path: FossilExecutablePath;
18 | version: FossilVersion;
19 | }
20 |
21 | function getVersion(
22 | path: UnvalidatedFossilExecutablePath
23 | ): Promise {
24 | return new Promise((c, e) => {
25 | const buffers: Buffer[] = [];
26 | const child = cp.spawn(path, ['version']);
27 | child.stdout.on('data', (b: Buffer) => buffers.push(b));
28 | child.on('error', e);
29 | child.on('close', code => {
30 | if (!code) {
31 | return c(
32 | Buffer.concat(buffers).toString('utf8') as FossilStdOut
33 | );
34 | }
35 | return e(new Error('Not found'));
36 | });
37 | });
38 | }
39 |
40 | export async function findFossil(
41 | hint: UnvalidatedFossilExecutablePath,
42 | outputChannel: LogOutputChannel
43 | ): Promise {
44 | for (const [path, isHint] of [
45 | [hint, 1],
46 | ['fossil' as UnvalidatedFossilExecutablePath, 0],
47 | ] as const) {
48 | if (path) {
49 | let stdout: string;
50 | try {
51 | stdout = await getVersion(path);
52 | } catch (e: unknown) {
53 | if (isHint) {
54 | outputChannel.warn(
55 | `\`fossil.path\` '${path}' is unavailable (${e}). Will try 'fossil' as the path`
56 | );
57 | } else {
58 | outputChannel.error(
59 | `'${path}' is unavailable (${e}). Fossil extension commands will be disabled`
60 | );
61 | }
62 | continue;
63 | }
64 |
65 | const match = stdout.match(/version (.+)\[/);
66 | let version = [0] as FossilVersion;
67 | if (match) {
68 | version = match[1]
69 | .split('.')
70 | .map(s => parseInt(s)) as FossilVersion;
71 | } else {
72 | outputChannel.error(
73 | `Failed to parse fossil version from output: '${stdout}'`
74 | );
75 | }
76 |
77 | outputChannel.info(
78 | localize(
79 | 'using fossil',
80 | 'Using fossil {0} from {1}',
81 | version.join('.'),
82 | path
83 | )
84 | );
85 | return {
86 | path: path as FossilExecutablePath,
87 | version: version,
88 | };
89 | }
90 | }
91 | return undefined;
92 | }
93 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { workspace } from 'vscode';
2 | import type { FossilUsername, Distinct } from './openedRepository';
3 | import type { UnvalidatedFossilExecutablePath } from './fossilFinder';
4 |
5 | export type AutoSyncIntervalMs = Distinct;
6 |
7 | interface ConfigScheme {
8 | ignoreMissingFossilWarning: boolean;
9 | path: UnvalidatedFossilExecutablePath;
10 | autoSyncInterval: number;
11 | username: FossilUsername; // must be ignored when empty
12 | defaultUsername: FossilUsername; // must be ignored when empty
13 | autoRefresh: boolean;
14 | enableRenaming: boolean;
15 | confirmGitExport: 'Automatically' | 'Never' | null;
16 | globalArgs: string[];
17 | commitArgs: string[];
18 | }
19 |
20 | class Config {
21 | private get config() {
22 | return workspace.getConfiguration('fossil');
23 | }
24 |
25 | private get(
26 | name: TName
27 | ): ConfigScheme[TName] {
28 | // for keys existing in packages.json this function
29 | // will not return `undefined`
30 | return this.config.get(name)!;
31 | }
32 |
33 | get path(): UnvalidatedFossilExecutablePath {
34 | return this.get('path').trim() as UnvalidatedFossilExecutablePath;
35 | }
36 |
37 | /**
38 | * Enables automatic refreshing of Source Control tab and badge
39 | * counter when files within the project change.
40 | */
41 | get autoRefresh(): boolean {
42 | return this.get('autoRefresh');
43 | }
44 |
45 | get autoSyncIntervalMs(): AutoSyncIntervalMs {
46 | return (this.get('autoSyncInterval') * 1000) as AutoSyncIntervalMs;
47 | }
48 |
49 | get enableRenaming(): boolean {
50 | return this.get('enableRenaming');
51 | }
52 |
53 | get ignoreMissingFossilWarning(): boolean {
54 | return this.get('ignoreMissingFossilWarning');
55 | }
56 |
57 | disableMissingFossilWarning() {
58 | return this.config.update('ignoreMissingFossilWarning', true, false);
59 | }
60 |
61 | /**
62 | * * Specifies an explicit user to use for fossil commits.
63 | * * This should only be used if the user is different
64 | * than the fossil default user.
65 | */
66 | get username(): FossilUsername {
67 | return this.get('username');
68 | }
69 |
70 | disableRenaming() {
71 | return this.config.update('enableRenaming', false, false);
72 | }
73 |
74 | setGitExport(how: NonNullable) {
75 | return this.config.update('confirmGitExport', how, false);
76 | }
77 |
78 | get gitExport() {
79 | return this.get('confirmGitExport');
80 | }
81 |
82 | get globalArgs() {
83 | const defaultUsername = this.get('defaultUsername');
84 | const globalArgs = this.get('globalArgs');
85 | if (defaultUsername) {
86 | // Return a copy to avoid modifying `globalArgs`
87 | return [...globalArgs, '--user', defaultUsername];
88 | }
89 | return globalArgs;
90 | }
91 | get commitArgs() {
92 | return this.get('commitArgs');
93 | }
94 | }
95 |
96 | const typedConfig = new Config();
97 | export default typedConfig;
98 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | // based on https://github.com/Microsoft/vscode/commit/41f0ff15d7327da30fdae73aa04ca570ce34fa0a
8 |
9 | import { ExtensionContext, window, Disposable, commands, Uri } from 'vscode';
10 | import { Model } from './model';
11 | import { CommandCenter } from './commands';
12 | import { FossilFileSystemProvider } from './fileSystemProvider';
13 | import * as nls from 'vscode-nls';
14 | import typedConfig from './config';
15 | import { findFossil } from './fossilFinder';
16 | import { FossilExecutable } from './fossilExecutable';
17 |
18 | export const localize = nls.loadMessageBundle();
19 |
20 | async function init(context: ExtensionContext): Promise {
21 | const disposables: Disposable[] = [];
22 | context.subscriptions.push(
23 | new Disposable(() => Disposable.from(...disposables).dispose())
24 | );
25 |
26 | const outputChannel = window.createOutputChannel('Fossil', { log: true });
27 | disposables.push(outputChannel);
28 |
29 | const fossilHist = typedConfig.path;
30 | const fossilInfo = await findFossil(fossilHist, outputChannel);
31 | const executable = new FossilExecutable(outputChannel);
32 |
33 | const model = new Model(executable, fossilHist);
34 | disposables.push(model);
35 | model.foundExecutable(fossilInfo);
36 | if (!fossilInfo && !typedConfig.ignoreMissingFossilWarning) {
37 | const download = localize('downloadFossil', 'Download Fossil');
38 | const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
39 | const editPath = localize('editPath', 'Edit "fossil.path"');
40 | const choice = await window.showWarningMessage(
41 | localize(
42 | 'notfound',
43 | "Fossil was not found. Install it or configure it using the 'fossil.path' setting."
44 | ),
45 | download,
46 | editPath,
47 | neverShowAgain
48 | );
49 | if (choice === download) {
50 | commands.executeCommand(
51 | 'vscode.open',
52 | Uri.parse('https://www.fossil-scm.org/')
53 | );
54 | } else if (choice === editPath) {
55 | commands.executeCommand(
56 | 'workbench.action.openSettings',
57 | 'fossil.path'
58 | );
59 | } else if (choice === neverShowAgain) {
60 | await typedConfig.disableMissingFossilWarning();
61 | }
62 | }
63 |
64 | const onRepository = () =>
65 | commands.executeCommand(
66 | 'setContext',
67 | 'fossilOpenRepositoryCount',
68 | model.repositories.length
69 | );
70 | model.onDidOpenRepository(onRepository, null, disposables);
71 | model.onDidCloseRepository(onRepository, null, disposables);
72 | onRepository();
73 |
74 | disposables.push(
75 | new CommandCenter(executable, model, outputChannel, context),
76 | new FossilFileSystemProvider(model)
77 | );
78 | return model;
79 | }
80 |
81 | export async function activate(
82 | context: ExtensionContext
83 | ): Promise {
84 | return init(context).catch(err => console.error(err));
85 | }
86 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | import { Event } from 'vscode';
8 |
9 | export interface IDisposable {
10 | dispose(): void;
11 | }
12 |
13 | export function dispose(
14 | disposables: Set | T[]
15 | ): void {
16 | disposables.forEach(d => d.dispose());
17 | if (disposables instanceof Set) {
18 | disposables.clear();
19 | } else {
20 | disposables.length = 0;
21 | }
22 | }
23 |
24 | export function toDisposable(dispose: () => void): IDisposable {
25 | return { dispose };
26 | }
27 |
28 | function combinedDisposable(disposables: IDisposable[]): IDisposable {
29 | return toDisposable(() => dispose(disposables));
30 | }
31 |
32 | export function filterEvent(
33 | event: Event,
34 | filter: (e: T) => boolean
35 | ): Event {
36 | return (
37 | listener: (e: T) => any,
38 | thisArgs?: any,
39 | disposables?: IDisposable[]
40 | ) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
41 | }
42 |
43 | export function anyEvent(...events: Event[]): Event {
44 | return (
45 | listener: (e: T) => any,
46 | thisArgs?: any,
47 | disposables?: IDisposable[]
48 | ) => {
49 | const result = combinedDisposable(
50 | events.map(event => event(i => listener.call(thisArgs, i)))
51 | );
52 | disposables?.push(result);
53 | return result;
54 | };
55 | }
56 |
57 | export function done(promise: Promise): Promise {
58 | return promise.then(() => undefined);
59 | }
60 |
61 | export function once(event: Event): Event {
62 | return (
63 | listener: (e: T) => any,
64 | thisArgs?: any,
65 | disposables?: IDisposable[]
66 | ) => {
67 | const result = event(
68 | e => {
69 | result.dispose();
70 | return listener.call(thisArgs, e);
71 | },
72 | null,
73 | disposables
74 | );
75 |
76 | return result;
77 | };
78 | }
79 |
80 | export function eventToPromise(event: Event): Promise {
81 | return new Promise(c => once(event)(c));
82 | }
83 |
84 | export function groupBy(
85 | arr: T[],
86 | fn: (el: T) => string
87 | ): { [key: string]: T[] } {
88 | return arr.reduce((result, el) => {
89 | const key = fn(el);
90 | result[key] = [...(result[key] || []), el];
91 | return result;
92 | }, Object.create(null));
93 | }
94 |
95 | export function partition(
96 | array: T[],
97 | fn: (el: T, i: number, ary: T[]) => boolean
98 | ): [T[], T[]] {
99 | return array.reduce(
100 | (result: [T[], T[]], element: T, i: number) => {
101 | if (fn(element, i, array)) {
102 | result[0].push(element);
103 | } else {
104 | result[1].push(element);
105 | }
106 | return result;
107 | },
108 | <[T[], T[]]>[[], []]
109 | );
110 | }
111 |
112 | export const delay = (ms: number): Promise =>
113 | new Promise(c => setTimeout(c, ms));
114 |
115 | // export const isInArray = (
116 | // item: T,
117 | // array: ReadonlyArray
118 | // ): item is A => array.includes(item as A);
119 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": true,
5 | "**/.git": true,
6 | ".fossil-settings/*.no-warn": true,
7 | "**/.DS_Store": true,
8 | "*.vsix": true
9 | },
10 | "cSpell.words": [
11 | "aliceblue",
12 | "antiquewhite",
13 | "anypath",
14 | "atof",
15 | "bgcolor",
16 | "blanchedalmond",
17 | "blueviolet",
18 | "boolproperty",
19 | "branchcolor",
20 | "burlywood",
21 | "cadetblue",
22 | "Checkin",
23 | "cherrypick",
24 | "colorname",
25 | "colorproperty",
26 | "cornflowerblue",
27 | "cornsilk",
28 | "Crowl",
29 | "darkcyan",
30 | "darkgoldenrod",
31 | "darkgray",
32 | "darkgreen",
33 | "darkgrey",
34 | "darkkhaki",
35 | "darkmagenta",
36 | "darkolivegreen",
37 | "darkorange",
38 | "darkorchid",
39 | "darkred",
40 | "darksalmon",
41 | "darkseagreen",
42 | "darkslateblue",
43 | "darkslategray",
44 | "darkslategrey",
45 | "darkturquoise",
46 | "darkviolet",
47 | "dashproperty",
48 | "deeppink",
49 | "deepskyblue",
50 | "dimgray",
51 | "dimgrey",
52 | "dodgerblue",
53 | "esbuild",
54 | "floralwhite",
55 | "forestgreen",
56 | "fossilpatch",
57 | "fslckout",
58 | "gainsboro",
59 | "ghostwhite",
60 | "greenyellow",
61 | "hotpink",
62 | "humanise",
63 | "indianred",
64 | "koog1000",
65 | "larr",
66 | "lavenderblush",
67 | "lawngreen",
68 | "leftarrow",
69 | "leftrightarrow",
70 | "lemonchiffon",
71 | "lightcoral",
72 | "lightcyan",
73 | "lightgoldenrodyellow",
74 | "lightgray",
75 | "lightgreen",
76 | "lightpink",
77 | "lightsalmon",
78 | "lightseagreen",
79 | "lightskyblue",
80 | "lightslategray",
81 | "lightslategrey",
82 | "lightsteelblue",
83 | "lightyellow",
84 | "limegreen",
85 | "ljust",
86 | "lvalue",
87 | "mediumaquamarine",
88 | "mediumblue",
89 | "mediumorchid",
90 | "mediumpurple",
91 | "mediumseagreen",
92 | "mediumslateblue",
93 | "mediumspringgreen",
94 | "mediumturquoise",
95 | "mediumvioletred",
96 | "midnightblue",
97 | "mintcream",
98 | "mistyrose",
99 | "navajowhite",
100 | "numproperty",
101 | "numproperty",
102 | "objectname",
103 | "oldlace",
104 | "olivedrab",
105 | "orangered",
106 | "outfile",
107 | "palegoldenrod",
108 | "palegreen",
109 | "paleturquoise",
110 | "palevioletred",
111 | "papayawhip",
112 | "peachpuff",
113 | "pikchr",
114 | "powderblue",
115 | "prlist",
116 | "rarr",
117 | "rebeccapurple",
118 | "repourl",
119 | "rightarrow",
120 | "rjust",
121 | "rosybrown",
122 | "royalblue",
123 | "rvalue",
124 | "saddlebrown",
125 | "sandybrown",
126 | "seagreen",
127 | "sequentialize",
128 | "skyblue",
129 | "slateblue",
130 | "slategray",
131 | "slategrey",
132 | "springgreen",
133 | "steelblue",
134 | "Technote",
135 | "tmgrammar",
136 | "unstage",
137 | "userid",
138 | "whitesmoke",
139 | "yellowgreen"
140 | ],
141 | "tmlanguage-syntax-highlighter.formattingStyle": "tight"
142 | }
--------------------------------------------------------------------------------
/pikchr/syntax_tree.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import re
3 | from collections import defaultdict
4 | import graphviz
5 | from argparse import ArgumentParser
6 |
7 |
8 | def parse(filename: str):
9 | with open(filename) as f:
10 | code = f.read()
11 | no_paren = re.compile(r"\(.+\)", re.MULTILINE | re.DOTALL)
12 | spaces = re.compile(r"\s+", re.MULTILINE | re.DOTALL)
13 | for left, right in re.findall(
14 | r"^\s*(\S+)\s*::=(.*?)\.", code, re.MULTILINE | re.DOTALL
15 | ):
16 | left = no_paren.sub("", left)
17 | right = [
18 | no_paren.sub("", item) for item in spaces.split(right.strip())
19 | ]
20 | yield left, right
21 |
22 |
23 | subgraphs = (
24 | "basetype",
25 | "between",
26 | "boolproperty",
27 | "colorproperty",
28 | "dashproperty",
29 | "direction",
30 | "edge",
31 | "even",
32 | "lvalue",
33 | "numproperty",
34 | "print",
35 | "pritem",
36 | "prsep",
37 | "rvalue",
38 | "savelist",
39 | # "unnamed_statement",
40 | "withclause",
41 | "optrelexpr",
42 | # "objectname", recursion
43 | )
44 |
45 | dbg_count = 0
46 |
47 |
48 | def render(
49 | dot: graphviz.Digraph,
50 | dot_key: str,
51 | items: list[str],
52 | dd: dict[str, list[list[str]]],
53 | ):
54 | global dbg_count
55 | prev = dot_key
56 | replace = {
57 | "EOL": r"\\n|;",
58 | "": r"\",
59 | "EDGEPT": "bot|c|e|east|n|ne|north|nw|s|se|south|sw|w|west",
60 | "LAST": "last|previous",
61 | "PERCENT": "%",
62 | "LP": "(",
63 | "RP": ")",
64 | "LB": "[",
65 | "RB": "]",
66 | "COMMA": ",",
67 | "COLON": ":",
68 | "GT": ">",
69 | "LT": "<",
70 | "EQ": "=",
71 | "PLUS": "+",
72 | "MINUS": "-",
73 | "LRARROW": "\\<-\\>",
74 | "LARROW": "\\<-",
75 | "RARROW": "-\\>",
76 | "PLACENAME": "PLACENAME ([A-Z]+)",
77 | "CLASSNAME": "arc|arrow|box|circle|cylinder|dot|ellipse|file|line|move|oval|spline|text",
78 | "even": r"UNTIL EVEN WITH|EVEN WITH",
79 | }
80 | for orig_value in items:
81 | dbg_count += 1
82 | dot2_key = f"{orig_value}_{dot_key}_{dbg_count}"
83 | value = replace.get(orig_value, orig_value)
84 | if value in subgraphs:
85 | dot2_key = f"{value}_{dot_key}_{dbg_count}"
86 | with dot.subgraph(
87 | name=dot2_key,
88 | comment=value,
89 | node_attr={"shape": "box", "color": "green", "label": value},
90 | ) as c:
91 | for item in dd[value]:
92 | render(c, dot2_key, item, dd)
93 | else:
94 | if value.isupper() or orig_value in replace:
95 | color = "red"
96 | else:
97 | color = "blue"
98 | dot.node(dot2_key, value, color=color)
99 | dot.edge(prev, dot2_key)
100 | prev = dot2_key
101 |
102 |
103 | def main():
104 | parser = ArgumentParser()
105 | parser.add_argument("pikchr_y_path")
106 | pikchr_y_path = parser.parse_args().pikchr_y_path
107 | dot = graphviz.Digraph("round-table", comment="Pikchr")
108 | dd = defaultdict[str, list[list[str]]](list)
109 | for left, right in parse(pikchr_y_path):
110 | dd[left].append(right)
111 |
112 | for idx, (key, items) in enumerate(dd.items()):
113 | if key in subgraphs:
114 | continue
115 | dot_key = f"{key}_{idx}"
116 | dot.node(dot_key, key)
117 | for item in items:
118 | render(dot, dot_key, item, dd)
119 |
120 | dot.render(directory="doctest-output", view=True)
121 |
122 |
123 | if __name__ == "__main__":
124 | main()
125 |
--------------------------------------------------------------------------------
/src/revert.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TextDocument,
3 | Range,
4 | TextEditor,
5 | WorkspaceEdit,
6 | workspace,
7 | Position,
8 | } from 'vscode';
9 | import { toFossilUri } from './uri';
10 |
11 | export interface LineChange {
12 | readonly originalStartLineNumber: number;
13 | readonly originalEndLineNumber: number;
14 | readonly modifiedStartLineNumber: number;
15 | readonly modifiedEndLineNumber: number;
16 | }
17 |
18 | // copy from vscode/extensions/git/src/staging.ts
19 | function applyLineChanges(
20 | original: TextDocument,
21 | modified: TextDocument,
22 | diffs: LineChange[]
23 | ): string {
24 | const result: string[] = [];
25 | let currentLine = 0;
26 |
27 | for (const diff of diffs) {
28 | const isInsertion = diff.originalEndLineNumber === 0;
29 | const isDeletion = diff.modifiedEndLineNumber === 0;
30 |
31 | let endLine = isInsertion
32 | ? diff.originalStartLineNumber
33 | : diff.originalStartLineNumber - 1;
34 | let endCharacter = 0;
35 |
36 | // if this is a deletion at the very end of the document,then we need to account
37 | // for a newline at the end of the last line which may have been deleted
38 | // https://github.com/microsoft/vscode/issues/59670
39 | if (isDeletion && diff.originalEndLineNumber === original.lineCount) {
40 | endLine -= 1;
41 | endCharacter = original.lineAt(endLine).range.end.character;
42 | }
43 |
44 | result.push(
45 | original.getText(new Range(currentLine, 0, endLine, endCharacter))
46 | );
47 |
48 | if (!isDeletion) {
49 | let fromLine = diff.modifiedStartLineNumber - 1;
50 | let fromCharacter = 0;
51 |
52 | // if this is an insertion at the very end of the document,
53 | // then we must start the next range after the last character of the
54 | // previous line, in order to take the correct eol
55 | if (
56 | isInsertion &&
57 | diff.originalStartLineNumber === original.lineCount
58 | ) {
59 | fromLine -= 1;
60 | fromCharacter = modified.lineAt(fromLine).range.end.character;
61 | }
62 |
63 | result.push(
64 | modified.getText(
65 | new Range(
66 | fromLine,
67 | fromCharacter,
68 | diff.modifiedEndLineNumber,
69 | 0
70 | )
71 | )
72 | );
73 | }
74 |
75 | currentLine = isInsertion
76 | ? diff.originalStartLineNumber
77 | : diff.originalEndLineNumber;
78 | }
79 |
80 | result.push(
81 | original.getText(new Range(currentLine, 0, original.lineCount, 0))
82 | );
83 |
84 | return result.join('');
85 | }
86 |
87 | // copy from vscode/extensions/git/src/commands.ts (modified)
88 | export async function revertChanges(
89 | textEditor: TextEditor,
90 | changes: LineChange[]
91 | ): Promise {
92 | const modifiedDocument = textEditor.document;
93 | const modifiedUri = modifiedDocument.uri;
94 |
95 | if (modifiedUri.scheme !== 'file') {
96 | return;
97 | }
98 |
99 | const originalUri = toFossilUri(modifiedUri);
100 | const originalDocument = await workspace.openTextDocument(originalUri);
101 | const visibleRangesBeforeRevert = textEditor.visibleRanges;
102 | const result = applyLineChanges(
103 | originalDocument,
104 | modifiedDocument,
105 | changes
106 | );
107 |
108 | const edit = new WorkspaceEdit();
109 | edit.replace(
110 | modifiedUri,
111 | new Range(
112 | new Position(0, 0),
113 | modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end
114 | ),
115 | result
116 | );
117 | const success = await workspace.applyEdit(edit);
118 | /* c8 ignore next 3 */
119 | if (!success) {
120 | throw new Error('failed to revert a range');
121 | }
122 |
123 | textEditor.revealRange(visibleRangesBeforeRevert[0]);
124 | }
125 |
--------------------------------------------------------------------------------
/src/test/summaryAsMarkdown.ts:
--------------------------------------------------------------------------------
1 | //import * as data from '../coverage/coverage-summary.json';
2 |
3 | import * as fs from 'fs/promises';
4 |
5 | interface Details {
6 | total: number;
7 | covered: number;
8 | skipped: number;
9 | pct: number;
10 | }
11 |
12 | interface Info {
13 | lines: Details;
14 | statements: Details;
15 | functions: Details;
16 | branches: Details;
17 | branchesTrue?: Details; // total only
18 | }
19 |
20 | interface Summary {
21 | [key: string]: Info;
22 | }
23 |
24 | function addDetails(a: Details, b: Details): Details {
25 | const total = a.total + b.total;
26 | const covered = a.covered + b.covered;
27 |
28 | return {
29 | total,
30 | covered,
31 | skipped: a.skipped + b.skipped,
32 | pct: parseFloat(((covered / total) * 100).toFixed(2)),
33 | };
34 | }
35 |
36 | function filterSummary(summary: Summary, predicate: (key: string) => boolean) {
37 | const entries = Object.entries(summary);
38 | const filtered = entries.filter(
39 | ([key, _info]) => key !== 'total' && predicate(key)
40 | );
41 | return Object.fromEntries(filtered);
42 | }
43 |
44 | function calcTotal(summary: Summary): Info {
45 | let branches: Details = { covered: 0, pct: 0, skipped: 0, total: 0 };
46 | let lines: Details = { covered: 0, pct: 0, skipped: 0, total: 0 };
47 | let statements: Details = { covered: 0, pct: 0, skipped: 0, total: 0 };
48 | let functions: Details = { covered: 0, pct: 0, skipped: 0, total: 0 };
49 | for (const info of Object.values(summary)) {
50 | branches = addDetails(branches, info.branches);
51 | lines = addDetails(lines, info.lines);
52 | statements = addDetails(statements, info.statements);
53 | functions = addDetails(functions, info.functions);
54 | }
55 | return {
56 | branches,
57 | lines,
58 | statements,
59 | functions,
60 | };
61 | }
62 |
63 | function formatDetails(details: Details): string {
64 | if (!details) {
65 | return '?';
66 | }
67 | const mark = (() => {
68 | if (details.pct < 66.0) return '🟥';
69 | else if (details.pct != 100.0) return '🟨';
70 | else return '🟩';
71 | })();
72 | return `${mark} ${details.pct}% (${details.covered}/${details.total})`;
73 | }
74 |
75 | function writeSummary(summary: Summary): string {
76 | const lines: string[] = [];
77 | lines.push('|File|🙈 Lines|🙉 Branches|🙊 Functions|');
78 | lines.push('|-|-|-|-|');
79 | const total = calcTotal(filterSummary(summary, Boolean));
80 | lines.push(
81 | `|total|${formatDetails(total.lines)}|${formatDetails(
82 | total.branches
83 | )}|${formatDetails(total.functions)}|`
84 | );
85 | for (const [key, info] of Object.entries(summary)) {
86 | const split = key.indexOf('/src/');
87 | const normalized = split == -1 ? key : key.slice(split + 1);
88 | lines.push(
89 | `|${normalized}|${formatDetails(info.lines)}|${formatDetails(
90 | info.branches
91 | )}|${formatDetails(info.functions)}|`
92 | );
93 | }
94 | return lines.join('\n');
95 | }
96 |
97 | async function main(
98 | srcPath = 'coverage/coverage-summary.json',
99 | dstPath?: string
100 | ): Promise {
101 | const raw = await fs.readFile(srcPath);
102 | const obj: unknown = JSON.parse(raw.toString('utf-8'));
103 | if (!(obj instanceof Object)) {
104 | console.log('not an object', Object.prototype.toString.call(obj));
105 | return 1;
106 | }
107 | const lines: string[] = [];
108 | lines.push('### Code\n');
109 | lines.push(
110 | writeSummary(
111 | filterSummary(obj as Summary, key => key.indexOf('src/test') == -1)
112 | )
113 | );
114 | lines.push('\n### Test\n');
115 | lines.push(
116 | writeSummary(
117 | filterSummary(obj as Summary, key => key.indexOf('src/test') != -1)
118 | )
119 | );
120 | lines.push('');
121 |
122 | const out = lines.join('\n');
123 | if (dstPath) {
124 | await fs.writeFile(dstPath, out);
125 | } else {
126 | process.stdout.write(out);
127 | }
128 | return 0;
129 | }
130 | main(...process.argv.slice(2))
131 | .then(code => process.exit(code))
132 | .catch(reason => {
133 | console.log(reason);
134 | process.exit(1);
135 | });
136 |
--------------------------------------------------------------------------------
/pikchr/test/statements.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "first test"
2 |
3 | box
4 | // <--- storage.type.class.pikchr
5 | box
6 | // <--- storage.type.class.pikchr
7 | \
8 | // <- punctuation.separator.continuation.line.pikchr
9 | cylinder ;
10 | // <-------- storage.type.class.pikchr
11 | // ^ source.pikchr
12 | cylinder;
13 | // <--- storage.type.class.pikchr
14 | // ^ source.pikchr punctuation.separator.delimiter.end.pikchr
15 | cylinder;box
16 | // <--- storage.type.class.pikchr
17 | // ^ source.pikchr punctuation.separator.delimiter.end.pikchr
18 | // ^ storage.type.class.pikchr
19 |
20 |
21 | cylinder ; box
22 | // <--- storage.type.class.pikchr
23 | // ^ source.pikchr
24 | // ^ source.pikchr punctuation.separator.delimiter.end.pikchr
25 | // ^ source.pikchr
26 | // ^ storage.type.class.pikchr
27 |
28 | arrow right
29 | // <--- storage.type.class.pikchr
30 | // ^ -storage.type.class.pikchr
31 | // ^^^^^ support.constant.edge.pikchr
32 |
33 | diamond "hi"
34 | // <--- storage.type.class.pikchr
35 | // ^^^^ string.quoted.double.pikchr
36 |
37 |
38 | Cat: box "box"
39 | // <--- variable.language.place.pikchr
40 | // ^ punctuation.separator.pikchr
41 | // ^^^ storage.type.class.pikchr
42 | // ^^^^^ string.quoted.double.pikchr
43 | Cat : box "box"
44 | // ^^^ variable.language.place.pikchr
45 | // ^ punctuation.separator.pikchr
46 | // ^^^ storage.type.class.pikchr
47 | // ^^^^^ string.quoted.double.pikchr
48 |
49 | box "hello, \"world\""
50 | // ^^^^^^^^^^^^^^^^^^ string.quoted.double.pikchr
51 | box "hello, \x";;;;
52 | // ^^^^^^^^^^^ source.pikchr string.quoted.double.pikchr
53 | // ^^^^ punctuation.separator.delimiter.end.pikchr
54 | linewid = 0.25
55 | // <--- variable.language.pikchr
56 | // ^ keyword.operator.assignment.pikchr
57 | // ^^^^ constant.numeric.pikchr
58 | $r = 0.2in ; _a=0; @b=-1
59 | // <-- ^^ ^^ variable.language.pikchr
60 | // ^ ^ ^ keyword.operator.assignment.pikchr
61 | // ^^^^^ ^ ^ constant.numeric.pikchr
62 | // ^^ keyword.other.unit.in.pikchr
63 | // ^ ^ punctuation.separator.delimiter.end.pikchr
64 | // ^ keyword.operator.arithmetic.pikchr
65 |
66 | linerad = 0.75*$r
67 | // <------- variable.language.pikchr
68 | // ^ -variable.language.pikchr
69 | // ^ keyword.operator.assignment.pikchr
70 | // ^^^^ constant.numeric.pikchr
71 | // ^ keyword.operator.arithmetic.pikchr
72 | // ^^ variable.language.pikchr
73 |
74 | boxht = .2; boxwid = .3; circlerad = .3; dx = 0.05
75 | // <----- variable.language.pikchr
76 | // ^^^^^^ ^^^^^^^^^ ^^ variable.language.pikchr
77 | // ^ ^ ^ ^ keyword.operator.assignment.pikchr
78 | // ^^ ^^ ^^ ^^^^ constant.numeric.pikchr
79 | // ^ ^ ^ punctuation.separator.delimiter.end.pikchr
80 |
81 | left
82 | // <---- support.constant.direction.pikchr
83 | down with the sickness
84 | // <---- support.constant.direction.pikchr
85 | // ^^^^ ^^^ ^^^^^^^^ invalid.illegal.pikchr
86 |
87 | line dashed
88 | // ^^^^^^ entity.name.tag.pikchr
89 | line dotted
90 | // ^^^^^^ entity.name.tag.pikchr
91 | box :? 3 ``
92 | // ^^ ^^ invalid.illegal.pikchr
93 | print .!
94 | // ^^ invalid.illegal.pikchr
95 | print "t" .!
96 | // ^^^ string.quoted.double.pikchr
97 | // ^^ invalid.illegal.pikchr
98 | d = 43 !^
99 | // ^^ invalid.illegal.pikchr
100 | define par {} !
101 | // ^ invalid.illegal.pikchr
102 | assert(1==1),
103 | // ^ invalid.illegal.pikchr
104 |
105 | "splines"
106 | // <--------- string.quoted.double.pikchr
107 |
108 | box ht $1 wid $1 $2
109 | // ^^ ^^^ support.constant.property-value.pikchr
110 | // ^^ ^^ ^^ variable.language.pikchr
111 |
112 | print "Oval at: ",previous.x, ",", previous.y
113 | // <----- keyword.control.directive.define.pikchr
114 | // ^^^^^^^^^^^ ^^^ string.quoted.double.pikchr
115 | // ^ ^ punctuation.separator.pikchr
116 | // ^^^^^^^^ ^^^^^^^^ constant.language.pikchr
117 | // ^ ^ punctuation.separator.period.pikchr
118 | // ^ ^ constant.language.pikchr
119 |
120 | circlerad = previous.radius
121 | // <--------- variable.language.pikchr
122 | // ^^^^^^^^^^^^^^^^^ meta.assignment.pikchr
123 | // ^ keyword.operator.assignment.pikchr
124 | // ^^^^^^^^ constant.language.pikchr
125 | // ^ punctuation.separator.period.pikchr
126 | // ^^^^^^ support.constant.property-value.pikchr
127 | X0: last.e
128 | //> <-- variable.language.pikchr
129 | //>^ punctuation.separator.pikchr
130 | //> ^^^^ variable.language.pikchr
131 | //> ^ punctuation.separator.period.pikchr
132 | //> ^ entity.name.class.pikchr
133 |
--------------------------------------------------------------------------------
/pikchr/test/macro.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "macro definition and expansion"
2 |
3 |
4 | define par {}
5 | // <------ keyword.control.directive.define.pikchr
6 | // ^^^ variable.language.pikchr
7 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
8 | // ^ punctuation.section.block.end.bracket.curly.pikchr
9 | define mymacro {box $1}
10 | // <------ keyword.control.directive.define.pikchr
11 | // ^^^^^^^ variable.language.pikchr
12 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
13 | // ^^^ storage.type.class.pikchr
14 | // ^^ variable.language.pikchr
15 | // ^ punctuation.section.block.end.bracket.curly.pikchr
16 | define keyword {right}
17 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
18 | // ^^^^^ support.constant.direction.pikchr
19 | // ^ punctuation.section.block.end.bracket.curly.pikchr
20 | par
21 | // <--- entity.name.function.pikchr
22 | par()
23 | // <--- entity.name.function.pikchr
24 | // ^ punctuation.parenthesis.begin.pikchr
25 | // ^ punctuation.parenthesis.end.pikchr
26 | par() par() par par
27 | // <--- entity.name.function.pikchr
28 | // ^^^ entity.name.function.pikchr
29 | // ^^^ ^^^ variable.language.pikchr
30 | mymacro( /**/ "hi" /**/ )
31 | // <------- entity.name.function.pikchr
32 | // ^ punctuation.parenthesis.begin.pikchr
33 | // ^^^^ ^^^^ comment.block.pikchr
34 | // ^^^^ string.quoted.double.pikchr
35 | // ^ punctuation.parenthesis.end.pikchr
36 | define square { box ht $7 wid $8 $9 }
37 | // <------ keyword.control.directive.define.pikchr
38 | // ^^^^^^ variable.language.pikchr
39 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
40 | // ^^^ storage.type.class.pikchr
41 | // ^^ ^^^ support.constant.property-value.pikchr
42 | // ^^ ^^ ^^ variable.language.pikchr
43 | // ^ punctuation.section.block.end.bracket.curly.pikchr
44 | define m01 {m00("m01",$1,$2)}
45 | // <------ keyword.control.directive.define.pikchr
46 | // ^^^ variable.language.pikchr
47 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
48 | // ^^^ entity.name.function.pikchr
49 | // ^ punctuation.parenthesis.begin.pikchr
50 | // ^^^^^ string.quoted.double.pikchr
51 | // ^ ^ punctuation.separator.pikchr
52 | // ^^ ^^ variable.language.pikchr
53 | // ^ punctuation.parenthesis.end.pikchr
54 | // ^ punctuation.section.block.end.bracket.curly.pikchr
55 | define dodef {define $1 {box "Hello"}}
56 | // <------ keyword.control.directive.define.pikchr
57 | // ^^^^^^ keyword.control.directive.define.pikchr
58 | define dodef {mymacro}
59 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
60 | // ^^^^^^^ entity.name.function.pikchr
61 | // ^ punctuation.section.block.end.bracket.curly.pikchr
62 | mymacro("o") mymacro("o")
63 | // <------- entity.name.function.pikchr
64 | // ^^^^^^^ entity.name.function.pikchr
65 | // ^^^ ^^^ string.quoted.double.pikchr
66 | E2: emo at last move.end;
67 | // <-- variable.language.place.pikchr
68 | //^ punctuation.separator.pikchr
69 | // ^^^ variable.language.pikchr
70 | // ^^ keyword.pikchr
71 | // ^^^^ constant.language.pikchr
72 | // ^^^^ entity.name.function.pikchr
73 | // ^ punctuation.separator.period.pikchr
74 | // ^^^ constant.language.pikchr
75 | define place_as_parameter {$1: [C1: circle radius 1;];}
76 | // <------ keyword.control.directive.define.pikchr
77 | // ^^^^^^^^^^^^^^^^^^ variable.language.pikchr
78 | // ^ punctuation.section.block.begin.bracket.curly.pikchr
79 | // ^^ ^^ variable.language.place.pikchr
80 | // ^ punctuation.separator.pikchr
81 | // ^ punctuation.bracket.square.begin.pikchr
82 | // ^ punctuation.separator.pikchr
83 | // ^^^^^^ storage.type.class.pikchr
84 | // ^ ^ punctuation.separator.delimiter.end.pikchr
85 | // ^ punctuation.bracket.square.end.pikchr
86 | // ^ punctuation.section.block.end.bracket.curly.pikchr
87 | define place_as_parameter {$1: [C1: circle radius 1;]}
88 | // ^ punctuation.section.block.end.bracket.curly.pikchr
89 | emo at last move.end;
90 | // <--- entity.name.function.pikchr
91 | // ^^ keyword.pikchr
92 | // ^^^^ constant.language.pikchr
93 | // ^^^^ entity.name.function.pikchr
94 | // ^ punctuation.separator.period.pikchr
95 | // ^^^ constant.language.pikchr
96 | // ^ punctuation.separator.delimiter.end.pikchr
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Integrated Fossil source control for Visual Studio Code
2 |
3 | ### Prerequisites
4 |
5 | > This extension leverages your machine's Fossil installation,
6 | so you need to
7 | [install Fossil](https://www.fossil-scm.org/fossil/doc/trunk/www/quickstart.wiki)
8 | first. Also read the [cloning](/docs/cloning.md) documentation for info
9 | about cloning from the extension.
10 |
11 | 
12 |
13 | # Features
14 |
15 | * Add files and commit from the source control panel
16 | (i.e. where git normally appears).
17 |
18 | * All the basics: commit, add, revert, update, push and pull.
19 |
20 | * See changes inline within text editor.
21 |
22 | * Interactive log for basic file history and diff.
23 |
24 | * Branch, merge, resolve files.
25 |
26 | * Praise
27 |
28 | * Quickly switch branches, push and pull via status bar.
29 |
30 | * Supports named-branches workflows.
31 |
32 | * Automatic incoming/outgoing counters.
33 |
34 | * Undo/Redo.
35 |
36 | * Preview `md`, `wiki` and `pikchr` files
37 |
38 | * Syntax highlighting for `pikchr` language
39 |
40 | * Use command palette `Ctrl-Shift-P` >> `fossil:` to see all commands. (Not everything has a UI control.)
41 |
42 |
43 | ## View file changes
44 | 
45 |
46 | * Click a file see the diff view
47 | * Or open a file by using context menu
48 |
49 | ## Initialize a new repo
50 |
51 | 
52 |
53 | * Just click the Fossil icon from the source control title area
54 | * Follow prompts
55 |
56 | ## Update to a branch/tag
57 |
58 | 
59 |
60 | * The current branch name is shown in the bottom-left corner.
61 | * Click it to see a list of branches and tags that you can update to.
62 |
63 | # How to
64 |
65 | * **Checkout by hash?**
66 |
67 | Use branch menu in the status bar.
68 |
69 | * **Create a new branch?**
70 |
71 | Create a branch with "Commit Creating New Branch..." action in SCM menu or in command palette.
72 |
73 | * **Modify commit message?**
74 |
75 | Use "Fossil log" from command palette and navigate the options till specific checkout.
76 |
77 | * **Get current checkout hash or tags?**
78 |
79 | Hover over current branch name in the status bar
80 |
81 | * **Close/reopen a branch?**
82 |
83 | Use 'Close branch...' and 'Reopen branch...' actions from command palette.
84 |
85 | * **Commit partially**
86 |
87 | 1. Run `Stash Snapshot` command
88 | 2. Manually remove lines that you don't want in the commit
89 | 3. Make a commit
90 | 4. Run `Stash Pop`
91 |
92 | * **Blame**
93 |
94 | Use `Fossil: praise` command from command palette
95 |
96 | # Settings
97 |
98 | `fossil.autoRefresh { boolean }`
99 |
100 | * Enables automatic refreshing of Source Control tab and badge counter
101 | when files within the project change:
102 | `"true"` — enabled
103 | `"false"` — disabled, manual refresh still available.
104 |
105 | `fossil.path { string }`
106 |
107 | * Specifies an explicit `fossil` file path to use.
108 | * This should only be used if `fossil` cannot be found automatically.
109 | * The default behavior is to search for `fossil` on the PATH.
110 | * Takes effect immediately.
111 |
112 | `fossil.username { string }`
113 |
114 | * Specifies an explicit user to use for fossil commits (`--user-override`).
115 | * This should only be used if the user is different than the fossil default user.
116 | * Username could be passed with `fossil.commitArgs` but this is just convenient shortcut.
117 |
118 | `fossil.autoSyncInterval { number }`
119 | * The duration, in seconds, between each background `fossil sync` operation.
120 | * 0 to disable.
121 |
122 | `fossil.globalArgs`
123 | * Extra arguments added to each `fossil` command (see `fossil help -o`)
124 |
125 | `fossil.commitArgs`
126 | * Extra arguments added to `fossil commit` command (see `fossil help commit`)
127 |
128 |
129 | # Troubleshooting
130 |
131 | In general, Fossil designers maintain an abundance of
132 | [documentation](https://fossil-scm.org/home/doc/trunk/www/permutedindex.html).
133 | Reference that documentation as much as possible.
134 |
135 | | Issue | Resolution
136 | --------|----------------------------------------------------------------
137 | | Unknown certificate authority | Read the [Fossil SSL Documentation](https://fossil-scm.org/home/doc/trunk/www/ssl.wiki#certs) to update fossil with the correct CA |
138 | | inputBox prompt difficult to read | Run the same fossil command on the built-in terminal (Ctrl+`). Unfortunately VS Code strips newlines and tabs from inputBox prompts. |
139 |
140 |
141 | # Feedback & Contributing
142 |
143 | * Please report any bugs, suggestions or documentation requests via the
144 | [Github issues](https://github.com/koog1000/vscode-fossil/issues)
145 | (_yes_, I see the irony).
146 | * Feel free to submit
147 | [pull requests](https://github.com/koog1000/vscode-fossil/pulls).
148 |
149 |
150 | ### For developers
151 |
152 | * [Building and debugging](docs/dev/build.md)
153 | * [Api behavior](docs/dev/api.md)
154 | * [Releasing](docs/dev/release.md)
155 |
156 | # Acknowledgements
157 |
158 | [Ben Crowl](https://github.com/mrcrowl),
159 | [koog1000](https://github.com/koog1000),
160 | [senyai](https://github.com/senyai),
161 | [ajansveld](https://github.com/ajansveld), [hoffmael](https://github.com/hoffmael), [nioh-wiki](https://github.com/nioh-wiki), [joaomoreno](https://github.com/joaomoreno), [nsgundy](https://github.com/nsgundy)
162 |
--------------------------------------------------------------------------------
/docs/dev/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | This documentation is intended for fossil extension developers
4 | to understand all the commands and to not forget what all commands should do. This documentation should also help find bugs.
5 |
6 | _Work in progress_.
7 |
8 | | Command | Name | Where | Expected behavior |
9 | | - | - | - | - |
10 | | fossil.add | Add Files | • Untracked files section entries
• Command palette | 1. Execute `fossil add $(files)`
2. Add files into staged area |
11 | | fossil.addAll | Add All Untracked Files | • Untracked files section
• Command palette | Same as `fossil.add` for all files |
12 | | fossil.branch | Create Branch... | • Command palette | 1. Input new branch name
2. Try execute `fossil branch new $(branch-name)`
3. On error reopen or update branch
13 | | fossil.branchChange | _not needed_ | • Branch name is clicked in status bar | 1. Pick branch name
2. Execute `fossil update $(branch-name)` |
14 | | fossil.cherrypick | Cherry-pick into working directory... | • Main SCM menu
• Command palette |
15 | | fossil.clean | Delete Extras |
16 | | fossil.clone | Clone Fossil Repository | Source control header |
17 | | fossil.close | Close Repository | | Execute `fossil close`
18 | | fossil.closeBranch | Close branch... | | 1. Pick a branch
2. Execute `fossil tag add --raw closed $(branch-name)`
19 | | fossil.commit | Commit |
20 | | fossil.commitAll | Commit All | • Main SCM menu
• Command palette |
21 | | fossil.commitBranch | Commit Creating New Branch... | • Main SCM menu
• Command palette |
22 | | fossil.commitStaged | Commit Staged | • Main SCM menu
• Command palette |
23 | | fossil.commitWithInput | _not needed_ | • Standard commit input box |
24 | | fossil.deleteFile | Delete Untracked File |
25 | | fossil.deleteFiles | Delete All Untracked Files |
26 | | fossil.fileLog | Show file history... |
27 | | fossil.forget | Forget Files |
28 | | fossil.ignore | Add to ignore-glob | • Command palette
• Untracked submenu | 1. Modify `/.fossil-settings/ignore-glob`
2. Add `ignore-glob` to the current checkout (not staging)
3. Show ignore-glob file
29 | | fossil.init | Initialize Fossil Repository | • Main SCM menu
• Command palette | 1. Ask `.fossil` path
2. Ask project name
3. Ask project description
4. Run `fossil init`
5. Ask to open repository
30 | | fossil.integrate | Integrate into working directory... |
31 | | fossil.log | Log... |
32 | | fossil.merge | Merge into working directory... |
33 | | fossil.open | Open Fossil Repository |
34 | | fossil.openChange | Open Changes |
35 | | fossil.openChangeFromUri | Open Changes | • Editor bar | Switch editor to diff mode
36 | | fossil.openFile | Open File |
37 | | fossil.openFileFromUri | Open File | • Command palette
• Editor bar | When in diff view, there's a special button to show local file
38 | | fossil.openFiles | Open Files |
39 | | fossil.openResource| _not needed_ | • Source Control panel | Resource in opened in a diff view if possible |
40 | | fossil.openUI | Open web UI | • Command palette | execute `fossil ui` in VSCode terminal |
41 | | fossil.patchApply | Apply Patch | • Main SCM menu
• Command palette | 1. Select path
2. Execute `fossil patch apply $(path)`
42 | | fossil.patchCreate | Create Patch | • Main SCM menu
• Command palette | 1. Select path
2. Execute `fossil patch create $(path)`
43 | | fossil.pull | Pull | • Main SCM menu
• Command palette | 1. Select remote URI if many
2. Execute `fossil pull URI`
44 | | fossil.push | Push | • Main SCM menu
• Command palette | 1. Execute `fossil push`
45 | | fossil.pushTo | Push to... | • Main SCM menu
• Command palette | 1. Pick remote if > 1
2. Execute `fossil push URI`
46 | | fossil.redo | Redo | • Main SCM menu
• Command palette | execute `fossil redo`
47 | | fossil.refresh | Refresh | • Source control header | 1. Execute `fossil status`
2. Update related information
48 | | fossil.render | Preview Using Fossil Renderer | • Main SCM menu for any document
• Navigation bar for `.{md, wiki, pikchr}` files or any document with "pikchr" language or any untitled document in a project with fossil repository opened | 1. Create `webview` panel of `fossil.renderPanel` view type.
2. Execute `fossil pikchr`, `fossil test-wiki-render` or `fossil test-markdown-render`
49 | | fossil.reopenBranch | Reopen branch... | | 1. Pick branch
2. execute `fossil tag cancel --raw closed $(branch-name)`
50 | | fossil.revert | Discard Changes |
51 | | fossil.revertAll | Discard All Changes | • SCM group header
• Command palette | 1. When called from Command palette, revert "conflict" and "changes" groups
2. When called from SCM group header, revert all files in that group.
52 | | fossil.revertChange | Revert Change | • Inline diff window |
53 | | fossil.showOutput | Show fossil output | • Main SCM menu
• Command palette | Reveal `outputChannel` channel in the UI
54 | | fossil.stage | Stage Changes |
55 | | fossil.stageAll | Stage All Changes |
56 | | fossil.stashApply | Stash Apply | • Main SCM menu
• Command palette |
57 | | fossil.stashDrop | Stash Drop | • Main SCM menu
• Command palette |
58 | | fossil.stashPop | Stash Pop | • Main SCM menu
• Command palette |
59 | | fossil.stashSave | Stash Push | • Main SCM menu
• Command palette |
60 | | fossil.stashSnapshot | Stash Snapshot | • Main SCM menu
• Command palette |
61 | | fossil.sync | Sync | • Main SCM menu
• Command palette | execute `fossil sync`
62 | | fossil.undo | Undo | • Main SCM menu
• Command palette | execute `fossil undo`
63 | | fossil.unstage | Unstage Changes |
64 | | fossil.unstageAll | Unstage All Changes |
65 | | fossil.update | Update | • Main SCM menu
• Command palette
• Status bar item | 1. Execute `fossil update`
66 | | fossil.wikiCreate | Publish as Fossil Wiki or Technote |
67 |
--------------------------------------------------------------------------------
/src/humanise.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import {
3 | FossilBranch,
4 | FossilCheckin,
5 | FossilCommitMessage,
6 | } from './openedRepository';
7 |
8 | import { localize } from './main';
9 |
10 | class TimeSpan {
11 | private seconds: number;
12 |
13 | constructor(totalSeconds: number) {
14 | this.seconds = totalSeconds;
15 | }
16 |
17 | public get totalSeconds(): number {
18 | return this.seconds;
19 | }
20 | public get totalMinutes(): number {
21 | return this.seconds / 60;
22 | }
23 | public get totalHours(): number {
24 | return this.seconds / 3600;
25 | }
26 | public get totalDays(): number {
27 | return this.seconds / 86400;
28 | }
29 | // public get totalWeeks(): number {
30 | // return this.seconds / 604800;
31 | // }
32 | }
33 |
34 | const BULLET = '\u2022';
35 | const FILE_LIST_LIMIT = 8;
36 |
37 | export function formatFilesAsBulletedList(filenames: string[]): string {
38 | let extraCount = 0;
39 | if (filenames.length > FILE_LIST_LIMIT + 1) {
40 | extraCount = filenames.length - FILE_LIST_LIMIT;
41 | filenames = filenames.slice(0, FILE_LIST_LIMIT);
42 | }
43 |
44 | const osFilenames = filenames.map(f => f.replace(/[/\\]/g, path.sep));
45 | let formatted = ` ${BULLET} ${osFilenames.join(`\n ${BULLET} `)}`;
46 | if (extraCount > 1) {
47 | const andNOthers = localize(
48 | 'and n others',
49 | 'and {0} others',
50 | extraCount
51 | );
52 | formatted += `\n${andNOthers}`;
53 | }
54 |
55 | return formatted;
56 | }
57 |
58 | export function describeMerge(
59 | localBranchName: FossilBranch,
60 | otherBranchName: FossilCheckin
61 | ): FossilCommitMessage {
62 | return localize(
63 | 'merge into',
64 | 'Merge {0} into {1}',
65 | otherBranchName,
66 | localBranchName
67 | ) as FossilCommitMessage;
68 | }
69 |
70 | export const enum Old {
71 | DATE,
72 | EMPTY_STRING,
73 | }
74 |
75 | export function ageFromNow(date: Date, old: Old = Old.DATE): string {
76 | const elapsedSeconds = timeSince(date) / 1e3;
77 | const elapsed = new TimeSpan(elapsedSeconds);
78 | if (elapsed.totalDays >= 0) {
79 | // past
80 | if (elapsed.totalSeconds < 5) {
81 | return 'now';
82 | }
83 | if (elapsed.totalSeconds < 15) {
84 | return 'a few moments ago';
85 | }
86 | if (elapsed.totalSeconds < 99) {
87 | return `${Math.floor(elapsed.totalSeconds)} seconds ago`;
88 | }
89 | if (elapsed.totalMinutes < 60) {
90 | const minutes: string = pluraliseQuantity(
91 | 'minute',
92 | elapsed.totalMinutes
93 | );
94 | return `${minutes} ago`;
95 | }
96 | if (elapsed.totalHours < 24) {
97 | const now: Date = new Date();
98 | const today: Date = datePart(now);
99 | const startDate: Date = datePart(addSeconds(now, -elapsedSeconds));
100 | const yesterday: Date = addDays(today, -1);
101 |
102 | if (startDate.getTime() == yesterday.getTime()) {
103 | return 'yesterday';
104 | } else {
105 | const hours: string = pluraliseQuantity(
106 | 'hour',
107 | elapsed.totalHours
108 | );
109 | return `${hours} ago`;
110 | }
111 | }
112 | if (elapsed.totalDays < 7) {
113 | const now: Date = new Date();
114 | const today: Date = datePart(now);
115 | const startDate: Date = datePart(addSeconds(now, -elapsedSeconds));
116 | const yesterday: Date = addDays(today, -1);
117 | // const wholeDays: number = Math.round(elapsed.totalDays);
118 |
119 | if (startDate.getTime() == yesterday.getTime()) {
120 | return 'yesterday';
121 | } else {
122 | const todayWeek: number = getWeek(today);
123 | const startWeek: number = getWeek(startDate);
124 | if (todayWeek == startWeek) {
125 | return `${Math.round(elapsed.totalDays)} days ago`;
126 | } else {
127 | return 'last week';
128 | }
129 | }
130 | }
131 | if (old == Old.DATE) {
132 | return date.toLocaleDateString(undefined, {
133 | formatMatcher: 'basic',
134 | });
135 | } else {
136 | return '';
137 | }
138 | } else {
139 | // future
140 | const totalDays: number = Math.floor(-elapsed.totalDays);
141 | const totalHours: number = Math.floor(-elapsed.totalHours);
142 | const totalMinutes: number = Math.floor(-elapsed.totalMinutes);
143 | if (totalMinutes < 60) {
144 | return `future (${pluraliseQuantity('minute', totalMinutes)})`;
145 | }
146 | if (totalHours < 48) {
147 | return `future (${pluraliseQuantity('hour', totalHours)})`;
148 | }
149 | return `future (${totalDays} days)`;
150 | }
151 | }
152 |
153 | function timeSince(date: Date): number {
154 | return Date.now() - date.getTime();
155 | }
156 |
157 | function addSeconds(date: Date, numberOfSeconds: number): Date {
158 | const adjustedDate: Date = new Date(date.getTime());
159 | adjustedDate.setSeconds(adjustedDate.getSeconds() + numberOfSeconds);
160 | return adjustedDate;
161 | }
162 |
163 | function addDays(date: Date, numberOfDays: number): Date {
164 | const adjustedDate: Date = new Date(date.getTime());
165 | adjustedDate.setDate(adjustedDate.getDate() + numberOfDays);
166 | return adjustedDate;
167 | }
168 |
169 | function datePart(date: Date): Date {
170 | return new Date(
171 | date.getFullYear(),
172 | date.getMonth(),
173 | date.getDate(),
174 | 0,
175 | 0,
176 | 0,
177 | 0
178 | );
179 | }
180 |
181 | function getWeek(date: Date): number {
182 | const oneJan = new Date(date.getFullYear(), 0, 1);
183 | return Math.ceil(
184 | ((date.getTime() - oneJan.getTime()) / 86400000 + oneJan.getDay() + 1) /
185 | 7
186 | );
187 | }
188 |
189 | function pluraliseQuantity(word: string, quantity: number) {
190 | quantity = Math.floor(quantity);
191 | return `${quantity} ${word}${quantity == 1 ? '' : 's'}`;
192 | }
193 |
--------------------------------------------------------------------------------
/src/decorators.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | import { done } from './util';
8 |
9 | // export function memoize(
10 | // target: (this: Record) => Return,
11 | // context: ClassGetterDecoratorContext
12 | // ) {
13 | // const memoizeKey = `$memoize$${context.name as string}`;
14 | // return function (this: Record): Return {
15 | // if (!this[memoizeKey]) {
16 | // this[memoizeKey] = target.apply(this);
17 | // }
18 | // return this[memoizeKey];
19 | // };
20 | // }
21 | export function memoize(
22 | target: any,
23 | key: string,
24 | descriptor: TypedPropertyDescriptor
25 | ) {
26 | const memoizeKey = `$memoize$${key}`;
27 | const fn = descriptor.get!;
28 | descriptor.get = function (this: Record): Return {
29 | if (!this[memoizeKey]) {
30 | this[memoizeKey] = fn.apply(this);
31 | }
32 | return this[memoizeKey];
33 | };
34 | }
35 |
36 | /**
37 | * Decorator to not allow multiple async calls
38 | */
39 | // export function throttle(
40 | // target: (
41 | // this: Record | undefined>,
42 | // ...args: Args
43 | // ) => Promise,
44 | // context: ClassMethodDecoratorContext
45 | // ) {
46 | // const currentKey = `$thr$c$${String(context.name)}`; // $throttle$current$
47 | // const nextKey = `$thr$n$${String(context.name)}`; // $throttle$next$
48 |
49 | // const trigger = function (
50 | // this: Record | undefined>,
51 | // ...args: Args
52 | // ): Promise {
53 | // if (this[nextKey]) {
54 | // return this[nextKey]!;
55 | // }
56 |
57 | // if (this[currentKey]) {
58 | // this[nextKey] = done(this[currentKey]!).then(() => {
59 | // this[nextKey] = undefined;
60 | // return trigger.apply(this, args);
61 | // });
62 |
63 | // return this[nextKey]!;
64 | // }
65 |
66 | // this[currentKey] = target.apply(this, args);
67 |
68 | // const clear = () => (this[currentKey] = undefined);
69 | // done(this[currentKey]!).then(clear, clear);
70 |
71 | // return this[currentKey]!;
72 | // };
73 |
74 | // return trigger;
75 | // }
76 | export function throttle(
77 | target: any,
78 | key: string,
79 | descriptor: PropertyDescriptor
80 | ): void {
81 | const currentKey = `$thr$c$${key}`; // $throttle$current$
82 | const nextKey = `$thr$n$${key}`; // $throttle$next$
83 | const fn = descriptor.value!;
84 |
85 | descriptor.value = function (
86 | this: Record | undefined>,
87 | ...args: Args
88 | ): Promise {
89 | if (this[nextKey]) {
90 | return this[nextKey]!;
91 | }
92 |
93 | if (this[currentKey]) {
94 | this[nextKey] = done(this[currentKey]!).then(() => {
95 | this[nextKey] = undefined;
96 | return fn.apply(this, args);
97 | });
98 |
99 | return this[nextKey]!;
100 | }
101 |
102 | this[currentKey] = fn.apply(this, args);
103 |
104 | const clear = () => (this[currentKey] = undefined);
105 | done(this[currentKey]!).then(clear, clear);
106 |
107 | return this[currentKey]!;
108 | };
109 | }
110 |
111 | // Make sure asynchronous functions are called one after another.
112 | type ThisPromise = Record>;
113 |
114 | // export function sequentialize(
115 | // target: (this: ThisPromise, ...args: Args) => Promise,
116 | // context: ClassMethodDecoratorContext
117 | // ) {
118 | // const currentKey = `$s11e$${context.name as string}`; // sequentialize
119 |
120 | // return function (this: ThisPromise, ...args: Args): Promise {
121 | // const currentPromise =
122 | // (this[currentKey] as Promise) || Promise.resolve(null);
123 | // const run = async () => await target.apply(this, args);
124 | // this[currentKey] = currentPromise.then(run, run);
125 | // return this[currentKey];
126 | // };
127 | // }
128 |
129 | export function sequentialize(
130 | target: any,
131 | key: string,
132 | descriptor: TypedPropertyDescriptor<
133 | (this: ThisPromise, ...args: Args) => Promise
134 | >
135 | ) {
136 | const currentKey = `$s11e$${key}`; // sequentialize
137 | const fn = descriptor.value!;
138 |
139 | descriptor.value = function (
140 | this: ThisPromise,
141 | ...args: Args
142 | ): Promise {
143 | const currentPromise =
144 | (this[currentKey] as Promise) || Promise.resolve(null);
145 | const run = async () => await fn.apply(this, args);
146 | this[currentKey] = currentPromise.then(run, run);
147 | return this[currentKey];
148 | };
149 | }
150 |
151 | type ThisTimer = Record>;
152 |
153 | // export function debounce(delay: number) {
154 | // return function (
155 | // target: (this: ThisTimer, ...args: Args) => void,
156 | // context: ClassMemberDecoratorContext
157 | // ) {
158 | // const timerKey = `$d6e$${String(context.name)}`; // debounce
159 |
160 | // return function (this: ThisTimer, ...args: Args): void {
161 | // clearTimeout(this[timerKey]);
162 | // this[timerKey] = setTimeout(() => target.apply(this, args), delay);
163 | // };
164 | // };
165 | // }
166 | export function debounce(delay: number) {
167 | return function (
168 | target: any,
169 | key: string,
170 | descriptor: TypedPropertyDescriptor<
171 | (this: ThisTimer, ...args: []) => void
172 | >
173 | ) {
174 | const timerKey = `$d6e$${key}`; // debounce
175 | const fn = descriptor.value!;
176 |
177 | descriptor.value = function (this: ThisTimer, ...args: []): void {
178 | clearTimeout(this[timerKey]);
179 | this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
180 | };
181 | };
182 | }
183 |
--------------------------------------------------------------------------------
/src/resourceGroups.ts:
--------------------------------------------------------------------------------
1 | import { FossilRoot, FileStatus, ResourceStatus } from './openedRepository';
2 | import {
3 | Uri,
4 | SourceControlResourceGroup,
5 | SourceControl,
6 | Disposable,
7 | } from 'vscode';
8 | import * as path from 'path';
9 | import { FossilResource } from './repository';
10 |
11 | import { localize } from './main';
12 |
13 | interface IGroupStatusesParams {
14 | repositoryRoot: FossilRoot;
15 | statusGroups: IStatusGroups;
16 | fileStatuses: FileStatus[];
17 | }
18 |
19 | export interface IStatusGroups {
20 | conflict: FossilResourceGroup;
21 | staging: FossilResourceGroup;
22 | working: FossilResourceGroup;
23 | untracked: FossilResourceGroup;
24 | }
25 |
26 | export type FossilResourceId = keyof IStatusGroups;
27 |
28 | export function createEmptyStatusGroups(scm: SourceControl): IStatusGroups {
29 | const conflictGroup = new FossilResourceGroup(
30 | scm,
31 | 'conflict',
32 | localize('merge conflicts', 'Unresolved Conflicts')
33 | );
34 | const stagingGroup = new FossilResourceGroup(
35 | scm,
36 | 'staging',
37 | localize('staged changes', 'Staged Changes')
38 | ) as FossilResourceGroup;
39 | const workingGroup = new FossilResourceGroup(
40 | scm,
41 | 'working',
42 | localize('changes', 'Changes')
43 | ) as FossilResourceGroup;
44 | const untrackedGroup = new FossilResourceGroup(
45 | scm,
46 | 'untracked',
47 | localize('untracked files', 'Untracked Files')
48 | ) as FossilResourceGroup;
49 |
50 | return {
51 | conflict: conflictGroup,
52 | staging: stagingGroup,
53 | working: workingGroup,
54 | untracked: untrackedGroup,
55 | };
56 | }
57 |
58 | export interface IFossilResourceGroup extends SourceControlResourceGroup {
59 | resourceStates: FossilResource[];
60 | }
61 |
62 | export class FossilResourceGroup {
63 | private readonly _uriToResource: Map;
64 | private readonly _vscode_group: IFossilResourceGroup;
65 | get disposable(): Disposable {
66 | return this._vscode_group;
67 | }
68 | get resourceStates(): FossilResource[] {
69 | return this._vscode_group.resourceStates;
70 | }
71 | getResource(uri: Uri): FossilResource | undefined {
72 | return this._uriToResource.get(uri.toString());
73 | }
74 | includesUri(uri: Uri): boolean {
75 | return this._uriToResource.has(uri.toString());
76 | }
77 | includesDir(uriStr: string): boolean {
78 | // important: `uriStr` should end with path.sep to work properly
79 | for (const key of this._uriToResource.keys()) {
80 | if (key.startsWith(uriStr)) {
81 | return true;
82 | }
83 | }
84 | return false;
85 | }
86 |
87 | constructor(
88 | sourceControl: SourceControl,
89 | id: FossilResourceId,
90 | readonly label: string // translated string
91 | ) {
92 | this._uriToResource = new Map();
93 | this._vscode_group = sourceControl.createResourceGroup(
94 | id,
95 | label
96 | ) as IFossilResourceGroup;
97 | this._vscode_group.hideWhenEmpty = true;
98 | }
99 |
100 | is(id: FossilResourceId): boolean {
101 | return this._vscode_group.id === id;
102 | }
103 |
104 | updateResources(resources: FossilResource[]): void {
105 | this._vscode_group.resourceStates = resources;
106 | this._uriToResource.clear();
107 | resources.forEach(resource =>
108 | this._uriToResource.set(resource.resourceUri.toString(), resource)
109 | );
110 | }
111 |
112 | intersect(resources: FossilResource[]): void {
113 | // existing resources must be updated
114 | // because resource status might change
115 | for (const res of resources) {
116 | this._uriToResource.delete(res.resourceUri.toString());
117 | res.resourceGroup = this;
118 | }
119 | const intersectionResources: FossilResource[] = [
120 | ...resources,
121 | ...this._uriToResource.values(),
122 | ];
123 | this.updateResources(intersectionResources);
124 | }
125 |
126 | except(resources_to_exclude: FossilResource[]): void {
127 | for (const res of resources_to_exclude) {
128 | this._uriToResource.delete(res.resourceUri.toString());
129 | }
130 | this.updateResources([...this._uriToResource.values()]);
131 | }
132 | }
133 |
134 | export function groupStatuses({
135 | repositoryRoot,
136 | statusGroups: { conflict, staging, working, untracked },
137 | fileStatuses,
138 | }: IGroupStatusesParams): void {
139 | const workingDirectoryResources: FossilResource[] = [];
140 | const stagingResources: FossilResource[] = [];
141 | const conflictResources: FossilResource[] = [];
142 | const untrackedResources: FossilResource[] = [];
143 |
144 | const chooseResourcesAndGroup = (
145 | uriString: Uri,
146 | status: ResourceStatus
147 | ): [FossilResource[], FossilResourceGroup] => {
148 | if (status === ResourceStatus.EXTRA) {
149 | return [untrackedResources, untracked];
150 | }
151 |
152 | if (status === ResourceStatus.CONFLICT) {
153 | return [conflictResources, conflict];
154 | }
155 | return staging.includesUri(uriString)
156 | ? [stagingResources, staging]
157 | : [workingDirectoryResources, working];
158 | };
159 |
160 | const seenUriStrings: Map = new Map();
161 |
162 | for (const raw of fileStatuses) {
163 | const uri = Uri.file(path.join(repositoryRoot, raw.path));
164 | const uriString = uri.toString();
165 | seenUriStrings.set(uriString, true);
166 | const renameUri = raw.rename
167 | ? Uri.file(path.join(repositoryRoot, raw.rename))
168 | : undefined;
169 | const [resources, group] = chooseResourcesAndGroup(uri, raw.status);
170 | resources.push(
171 | new FossilResource(group, uri, raw.status, raw.klass, renameUri)
172 | );
173 | }
174 |
175 | conflict.updateResources(conflictResources);
176 | staging.updateResources(stagingResources);
177 | working.updateResources(workingDirectoryResources);
178 | untracked.updateResources(untrackedResources);
179 | }
180 |
181 | export const isResourceGroup = (
182 | obj: FossilResource | SourceControlResourceGroup
183 | ): obj is SourceControlResourceGroup =>
184 | (obj).resourceStates !== undefined;
185 |
--------------------------------------------------------------------------------
/src/statusBar.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Arseniy Terekhin. All rights reserved.
3 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import type { Command, SourceControl } from 'vscode';
7 | import { Repository } from './repository';
8 | import { ageFromNow, Old } from './humanise';
9 | import { localize } from './main';
10 | import { ExecResult } from './fossilExecutable';
11 |
12 | /**
13 | * A bar with 'sync' icon;
14 | * - should run `fossil up` command for specific repository when clicked [ok]
15 | * - the tooltip should show 'changes' that are shown when running `fossil up --dry-run`
16 | * - should show number of remote changes (if != 0)
17 | * - should be animated when sync/update is running
18 | * - sync should be rescheduled after `sync` or `update` commands
19 | * - handle case when there's no sync URL
20 | */
21 | class SyncBar {
22 | private icon: 'sync' | 'warning' = 'sync'; // 'sync-ignored' is nice, but not intuitive
23 | private text = '';
24 | private syncMessage: `${string}\n` | '' = '';
25 | /**
26 | * match for /changes:\s*(string)/
27 | * like `17 files modified.` or ` None. Already up-to-date`
28 | */
29 | private changes: string = ''; //
30 | private nextSyncTime: Date | undefined; // undefined = no auto syncing
31 |
32 | constructor(private repository: Repository) {}
33 |
34 | public onChangesReady(updateResult: ExecResult) {
35 | if (!updateResult.exitCode) {
36 | const match = updateResult.stdout.match(/^changes:\s*((\d*).*)/m);
37 | this.changes = match?.[1] ?? 'unknown changes';
38 | this.text = match?.[2] ?? ''; // digits of nothing
39 | } else {
40 | this.changes = this.text = '';
41 | }
42 | }
43 |
44 | public onSyncReady(result: ExecResult) {
45 | this.icon = 'sync';
46 | if (!result.exitCode) {
47 | this.syncMessage = '';
48 | } else {
49 | if (/^Usage: /.test(result.stderr)) {
50 | // likely only local repo
51 | this.syncMessage = 'repository with no remote\n';
52 | } else {
53 | this.icon = 'warning';
54 | this.syncMessage = `Sync error: ${result.stderr}\n`;
55 | }
56 | }
57 | }
58 |
59 | public onSyncTimeUpdated(date: Date | undefined) {
60 | this.nextSyncTime = date;
61 | }
62 |
63 | public get command(): Command {
64 | const timeMessage = this.nextSyncTime
65 | ? `Next sync ${this.nextSyncTime.toTimeString().split(' ')[0]}`
66 | : `Auto sync disabled`;
67 | return {
68 | command: 'fossil.update',
69 | title: `$(${this.icon}) ${this.text}`.trim(),
70 | tooltip: `${timeMessage}\n${this.syncMessage}${this.changes}\nUpdate`,
71 | arguments: [this.repository satisfies Repository],
72 | };
73 | }
74 | }
75 |
76 | /**
77 | * Create `vscode.Command` that executes 'fossil.branchChange'
78 | * decorated with icon, branch name, and repository status
79 | * with branch details in the tooltip
80 | */
81 | function branchCommand(repository: Repository): Command {
82 | const { currentBranch, fossilStatus } = repository;
83 | const icon = fossilStatus!.isMerge ? '$(git-merge)' : '$(git-branch)';
84 | const title =
85 | icon +
86 | ' ' +
87 | (currentBranch || 'unknown') +
88 | (repository.conflictGroup.resourceStates.length
89 | ? '!'
90 | : repository.workingGroup.resourceStates.length
91 | ? '+'
92 | : '');
93 | let checkoutAge = '';
94 | const d = new Date(fossilStatus!.checkout.date.replace(' UTC', '.000Z'));
95 | checkoutAge = ageFromNow(d, Old.EMPTY_STRING);
96 |
97 | return {
98 | command: 'fossil.branchChange',
99 | tooltip: localize(
100 | 'branch change {0} {1}{2} {3}',
101 | '{0}\n{1}{2}\nTags:\n • {3}\nChange Branch...',
102 | fossilStatus!.checkout.checkin,
103 | fossilStatus!.checkout.date,
104 | checkoutAge && ` (${checkoutAge})`,
105 | fossilStatus!.tags.join('\n • ')
106 | ),
107 | title,
108 | arguments: [repository satisfies Repository],
109 | };
110 | }
111 |
112 | export class StatusBarCommands {
113 | private readonly syncBar: SyncBar;
114 |
115 | constructor(
116 | private readonly repository: Repository,
117 | private readonly sourceControl: SourceControl
118 | ) {
119 | this.syncBar = new SyncBar(repository);
120 | this.update();
121 | }
122 |
123 | public onChangesReady(updateResult: ExecResult) {
124 | this.syncBar.onChangesReady(updateResult);
125 | this.update();
126 | }
127 |
128 | public onSyncTimeUpdated(date: Date | undefined) {
129 | this.syncBar.onSyncTimeUpdated(date);
130 | this.update();
131 | }
132 |
133 | public onSyncReady(syncResult: ExecResult) {
134 | this.syncBar.onSyncReady(syncResult);
135 | this.update();
136 | }
137 |
138 | /**
139 | * Should be called whenever commands text/actions/tooltips
140 | * are updated
141 | */
142 | public update(): void {
143 | let commands: Command[];
144 | if (this.repository.fossilStatus) {
145 | const update = branchCommand(this.repository);
146 | const sideEffects = this.repository.operations;
147 | const messages = [];
148 | for (const [, se] of sideEffects) {
149 | if (se.syncText) {
150 | messages.push(se.syncText);
151 | }
152 | }
153 | messages.sort();
154 | const sync = messages.length
155 | ? {
156 | title: '$(sync~spin)',
157 | command: '',
158 | tooltip: messages.join('\n'),
159 | }
160 | : this.syncBar.command;
161 |
162 | commands = [update, sync];
163 | } else {
164 | // this class was just initialized, repository status is unknown
165 | commands = [
166 | {
167 | command: '',
168 | tooltip: localize(
169 | 'loading {0}',
170 | 'Loading {0}',
171 | this.repository.root
172 | ),
173 | title: '$(sync~spin)',
174 | },
175 | ];
176 | }
177 | this.sourceControl.statusBarCommands = commands;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/test/suite/Infrastructure.test.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as sinon from 'sinon';
3 | import {
4 | FossilCWD,
5 | FossilExecutablePath,
6 | FossilStdOut,
7 | FossilStdErr,
8 | ExecFailure,
9 | toString as ExecFailureToString,
10 | } from '../../fossilExecutable';
11 | import * as assert from 'assert/strict';
12 | import { getExecutable } from './common';
13 | import { after, afterEach, before } from 'mocha';
14 | import { Old, ageFromNow } from '../../humanise';
15 | import { ThrottlingQueue } from '../../throttlingQueue';
16 |
17 | suite('Infrastructure', () => {
18 | const sandbox = sinon.createSandbox();
19 | afterEach(() => {
20 | sandbox.restore();
21 | });
22 | test('Error is not thrown when executing unknown command', async () => {
23 | const rootUri = vscode.workspace.workspaceFolders![0].uri;
24 | const cwd = rootUri.fsPath as FossilCWD;
25 | const showErrorMessage: sinon.SinonStub = sandbox
26 | .stub(vscode.window, 'showErrorMessage')
27 | .resolves();
28 | const executable = getExecutable();
29 | const result = await executable.exec(cwd, ['fizzbuzz'] as any);
30 | const fossilPath = (executable as any).fossilPath as string;
31 | assert.deepEqual(result, {
32 | message: 'Failed to execute fossil',
33 | stderr: (`${fossilPath}: unknown command: fizzbuzz\n` +
34 | `${fossilPath}: use "help" for more information\n`) as FossilStdErr,
35 | stdout: '' as FossilStdOut,
36 | exitCode: 1,
37 | args: ['fizzbuzz' as any],
38 | fossilErrorCode: 'unknown',
39 | cwd: cwd,
40 | fossilPath: (executable as any).fossilPath,
41 | toString: ExecFailureToString,
42 | });
43 | sinon.assert.calledOnceWithExactly(
44 | showErrorMessage,
45 | `Fossil: ${fossilPath}: unknown command: fizzbuzz`,
46 | 'Open Fossil Log'
47 | );
48 | });
49 | test('Error to string is valid', async () => {
50 | const TestError = {
51 | message: 'my message',
52 | stdout: 'my stdout' as FossilStdOut,
53 | stderr: 'my stderr' as FossilStdErr,
54 | exitCode: 1,
55 | fossilErrorCode: 'unknown',
56 | args: ['cat'],
57 | cwd: 'cwd' as FossilCWD,
58 | fossilPath: '/bin/fossil' as FossilExecutablePath,
59 | toString: ExecFailureToString,
60 | } as ExecFailure;
61 | const referenceString =
62 | 'my message {\n' +
63 | ' "stdout": "my stdout",\n' +
64 | ' "stderr": "my stderr",\n' +
65 | ' "exitCode": 1,\n' +
66 | ' "fossilErrorCode": "unknown",\n' +
67 | ' "args": [\n' +
68 | ' "cat"\n' +
69 | ' ],\n' +
70 | ' "cwd": "cwd",\n' +
71 | ' "fossilPath": "/bin/fossil"\n' +
72 | '}';
73 | assert.equal(TestError.toString(), referenceString);
74 | });
75 |
76 | suite('ageFromNow', function () {
77 | const N = 1686899727000; // 2023-06-16T07:15:27.000Z friday
78 | const minutes = (n: number) => new Date(N + n * 60000);
79 | const days = (n: number) => minutes(n * 24 * 60);
80 | let fakeTimers: sinon.SinonFakeTimers;
81 |
82 | before(() => {
83 | fakeTimers = sinon.useFakeTimers(N);
84 | });
85 | after(() => {
86 | fakeTimers.restore();
87 | });
88 | test('Now', () => {
89 | assert.equal(ageFromNow(new Date()), 'now');
90 | });
91 | test('Now - 12 seconds', () => {
92 | assert.equal(ageFromNow(minutes(-0.2)), 'a few moments ago');
93 | });
94 | test('Now - 30 seconds', () => {
95 | assert.equal(ageFromNow(minutes(-0.5)), '30 seconds ago');
96 | });
97 | test('Now - 1 minute', () => {
98 | assert.equal(ageFromNow(minutes(-1)), '60 seconds ago');
99 | });
100 | test('Now - 2 minutes', () => {
101 | assert.equal(ageFromNow(minutes(-2)), '2 minutes ago');
102 | });
103 | test('Now - 10 minute', () => {
104 | assert.equal(ageFromNow(minutes(-10)), '10 minutes ago');
105 | });
106 | test('Now - 1 hour', () => {
107 | assert.equal(ageFromNow(minutes(-60)), '1 hour ago');
108 | });
109 | test('Now - 23.5 hour', () => {
110 | assert.equal(ageFromNow(minutes(-23.5 * 60)), 'yesterday');
111 | });
112 | test('Now - 1 day', () => {
113 | assert.equal(ageFromNow(days(-1)), 'yesterday');
114 | });
115 | test('Now - 2 days', () => {
116 | assert.equal(ageFromNow(days(-2)), '2 days ago');
117 | });
118 | test('Now - 3 days', () => {
119 | assert.equal(ageFromNow(days(-3)), '3 days ago');
120 | });
121 | test('Now - 6 days', () => {
122 | assert.equal(ageFromNow(days(-6)), 'last week');
123 | });
124 | test('Now - 7 days', () => {
125 | assert.equal(ageFromNow(days(-7)), '6/9/2023');
126 | });
127 | test('Long ago is empty string', () => {
128 | assert.equal(ageFromNow(days(-30), Old.EMPTY_STRING), '');
129 | });
130 | test('Now + 1 minute', () => {
131 | assert.equal(ageFromNow(minutes(1)), 'future (1 minute)');
132 | });
133 | test('Now + 1 day', () => {
134 | assert.equal(ageFromNow(days(1)), 'future (24 hours)');
135 | });
136 | test('Now + 7 days', () => {
137 | assert.equal(ageFromNow(days(7)), 'future (7 days)');
138 | });
139 | test('Now + one year', () => {
140 | assert.equal(ageFromNow(days(366)), 'future (366 days)');
141 | });
142 | });
143 | test('Queue "next" logic', async () => {
144 | const queue = new ThrottlingQueue();
145 | const p1 = queue.enqueue(() => new Promise(c => c(1)), 'p');
146 | const p2 = queue.enqueue(() => new Promise(c => c(2)), 'p');
147 | const p3 = queue.enqueue(() => new Promise(c => c(3)), 'p');
148 | const res = await Promise.allSettled([p1, p2, p3]);
149 | assert.deepEqual(res, [
150 | {
151 | status: 'fulfilled',
152 | value: 1,
153 | },
154 | {
155 | status: 'fulfilled',
156 | value: 2,
157 | },
158 | {
159 | status: 'fulfilled',
160 | value: 2,
161 | },
162 | ]);
163 | });
164 | test('Queue can handle exceptions', async () => {
165 | const error = new Error();
166 | const queue = new ThrottlingQueue();
167 | const p1 = queue.enqueue(() => new Promise((c, e) => e(error)), 'p');
168 | const res = await Promise.allSettled([p1]);
169 | assert.deepEqual(res, [
170 | {
171 | status: 'rejected',
172 | reason: error,
173 | },
174 | ]);
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/src/praise.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CancellationToken,
3 | DecorationRangeBehavior,
4 | Disposable,
5 | Hover,
6 | languages,
7 | MarkdownString,
8 | Position,
9 | Range,
10 | TextDocument,
11 | TextDocumentChangeEvent,
12 | TextEditor,
13 | TextEditorSelectionChangeEvent,
14 | ThemableDecorationAttachmentRenderOptions,
15 | ThemeColor,
16 | window,
17 | workspace,
18 | } from 'vscode';
19 | import { FossilHash, Praise } from './openedRepository';
20 | import { Repository } from './repository';
21 |
22 | const annotationDecoration = window.createTextEditorDecorationType({
23 | rangeBehavior: DecorationRangeBehavior.ClosedOpen,
24 | before: {
25 | borderColor: new ThemeColor('focusBorder'),
26 | height: '100%',
27 | margin: '0 26px -1px 0',
28 | fontWeight: 'normal',
29 | fontStyle: 'normal',
30 | backgroundColor: 'rgba(255, 255, 255, 0.07)',
31 | color: new ThemeColor('editor.foreground'),
32 | },
33 | light: {
34 | before: {
35 | backgroundColor: 'rgba(0, 0, 0, 0.07)',
36 | },
37 | },
38 | });
39 |
40 | const annotationHighlight = window.createTextEditorDecorationType({
41 | isWholeLine: true,
42 | backgroundColor: 'rgba(128, 255, 255, 0.07)',
43 | light: {
44 | backgroundColor: 'rgba(0, 128, 128, 0.07)',
45 | },
46 | });
47 |
48 | export class PraiseAnnotator {
49 | private static editors = new WeakMap();
50 | private readonly disposable: Disposable;
51 | private readonly document: TextDocument;
52 | private readonly hoverProvider: Disposable;
53 | private constructor(
54 | private readonly repository: Repository,
55 | private readonly editor: TextEditor,
56 | private readonly hashes: FossilHash[]
57 | ) {
58 | this.document = editor.document;
59 | this.disposable = Disposable.from(
60 | window.onDidChangeTextEditorSelection(
61 | this.onTextEditorSelectionChanged,
62 | this
63 | ),
64 | workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this),
65 | workspace.onDidChangeTextDocument(this.onTextDocumentChanged, this)
66 | );
67 | this.hoverProvider = languages.registerHoverProvider(
68 | { pattern: this.document.uri.fsPath },
69 | { provideHover: this.onHover.bind(this) }
70 | );
71 | }
72 | static async create(
73 | repository: Repository,
74 | editor: TextEditor,
75 | praises: Praise[]
76 | ): Promise {
77 | await editor.document.save(); // for `fossil diff` to work
78 | let prev_hash: FossilHash | undefined;
79 | const nbsp = ' ';
80 | const decorations = praises.map((praise, lineNo) => {
81 | const range = editor.document.validateRange(
82 | new Range(lineNo, 0, lineNo, 0)
83 | );
84 | let before: ThemableDecorationAttachmentRenderOptions;
85 | const common = {
86 | borderStyle: 'solid',
87 | borderWidth: '0 2px 0 0',
88 | };
89 | if (prev_hash == praise[0]) {
90 | before = {
91 | contentText: nbsp,
92 | textDecoration: 'none;padding: 0 33ch 0 0',
93 | ...common,
94 | };
95 | } else {
96 | prev_hash = praise[0];
97 | // total width: 8(hash) + 1 + 10(date) + 1 + 13 = 33
98 | const checkin = praise[0].slice(0, 8) || nbsp.repeat(8);
99 | const date = praise[1] || nbsp.repeat(10);
100 | const username = praise[2].slice(-13);
101 | const contentText = `${checkin} ${date}${nbsp.repeat(
102 | 14 - username.length
103 | )}${username}`;
104 | before = {
105 | contentText,
106 | textDecoration: 'none;padding: 0 1ch 0 0',
107 | ...common,
108 | };
109 | }
110 | if (!praise[0]) {
111 | before.backgroundColor = 'rgba(53, 255, 28, 0.07)';
112 | }
113 | return { renderOptions: { before }, range };
114 | });
115 | editor.setDecorations(annotationDecoration, decorations);
116 | const hashes = praises.map(praise => praise[0]);
117 | const annotator = new PraiseAnnotator(repository, editor, hashes);
118 | PraiseAnnotator.editors.set(editor, annotator);
119 | return annotator;
120 | }
121 |
122 | static tryDelete(editor: TextEditor): boolean {
123 | const praise = PraiseAnnotator.editors.get(editor);
124 | praise?.dispose();
125 | return praise !== undefined;
126 | }
127 |
128 | private onTextEditorSelectionChanged(
129 | event: TextEditorSelectionChangeEvent
130 | ): void {
131 | if (event.textEditor === this.editor) {
132 | const ranges: Range[] = [];
133 | const line = event.selections[0].active.line;
134 | const curHash = this.hashes[line];
135 | this.hashes.map((hash, lineNo) => {
136 | if (hash === curHash) {
137 | ranges.push(new Range(lineNo, 0, lineNo, 0));
138 | }
139 | });
140 | event.textEditor.setDecorations(annotationHighlight, ranges);
141 | }
142 | }
143 |
144 | private onDidCloseTextDocument(document: TextDocument) {
145 | // this event can be delayed, but I believe
146 | // this is the right place to cleanup
147 | if (document === this.document) {
148 | this.dispose();
149 | }
150 | }
151 |
152 | private onTextDocumentChanged(event: TextDocumentChangeEvent) {
153 | if (event.document === this.document) {
154 | this.dispose();
155 | }
156 | }
157 |
158 | private async onHover(
159 | document: TextDocument,
160 | position: Position,
161 | _token: CancellationToken
162 | ): Promise {
163 | if (document !== this.editor.document) {
164 | return undefined;
165 | }
166 | const checkin = this.hashes[position.line];
167 | if (!checkin) {
168 | return new Hover(new MarkdownString('local change'));
169 | }
170 | const info = await this.repository.info(checkin);
171 | const infoString =
172 | `**${info.comment}**\n\n` +
173 | Object.entries(info)
174 | .map(([key, value]) =>
175 | key == 'comment' ? '' : `* ${key}: **${value}**\n`
176 | )
177 | .join('');
178 | return new Hover(new MarkdownString(infoString, true));
179 | }
180 |
181 | dispose(): void {
182 | PraiseAnnotator.editors.delete(this.editor);
183 | this.editor.setDecorations(annotationDecoration, []);
184 | this.editor.setDecorations(annotationHighlight, []);
185 | this.disposable.dispose();
186 | this.hoverProvider.dispose();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/fileSystemProvider.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Ben Crowl. All rights reserved.
3 | * Original Copyright (c) Microsoft Corporation. All rights reserved.
4 | * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
5 | *--------------------------------------------------------------------------------------------*/
6 |
7 | import {
8 | workspace,
9 | Uri,
10 | Disposable,
11 | Event,
12 | EventEmitter,
13 | window,
14 | FileSystemError,
15 | FileStat,
16 | FileType,
17 | FileChangeEvent,
18 | FileSystemProvider,
19 | FileChangeType,
20 | } from 'vscode';
21 | import { debounce, throttle } from './decorators';
22 | import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
23 | import { eventToPromise, filterEvent, toDisposable } from './util';
24 | import { fromFossilUri, toFossilUri } from './uri';
25 | import { sep } from 'path';
26 | export const EmptyDisposable = toDisposable(() => null);
27 |
28 | function isWindowsPath(path: string): boolean {
29 | return /^[a-zA-Z]:\\/.test(path);
30 | }
31 |
32 | function isDescendant(parent: string, descendant: string): boolean {
33 | if (parent === descendant) {
34 | return true;
35 | }
36 |
37 | if (parent.charAt(parent.length - 1) !== sep) {
38 | parent += sep;
39 | }
40 |
41 | // Windows is case insensitive
42 | if (isWindowsPath(parent)) {
43 | parent = parent.toLowerCase();
44 | descendant = descendant.toLowerCase();
45 | }
46 |
47 | return descendant.startsWith(parent);
48 | }
49 |
50 | function pathEquals(a: string, b: string): boolean {
51 | // Windows is case insensitive
52 | if (isWindowsPath(a)) {
53 | a = a.toLowerCase();
54 | b = b.toLowerCase();
55 | }
56 |
57 | return a === b;
58 | }
59 |
60 | interface CacheRow {
61 | uri: Uri;
62 | timestamp: number;
63 | }
64 |
65 | const THREE_MINUTES = 1000 * 60 * 3;
66 | const FIVE_MINUTES = 1000 * 60 * 5;
67 |
68 | function not_implemented(): never {
69 | throw new Error('Method not implemented.');
70 | }
71 |
72 | export class FossilFileSystemProvider implements FileSystemProvider {
73 | private _onDidChangeFile = new EventEmitter();
74 | readonly onDidChangeFile: Event =
75 | this._onDidChangeFile.event;
76 |
77 | private changedRepositoryRoots = new Set();
78 | private cache = new Map();
79 | private mtime = new Date().getTime();
80 | private disposables: Disposable[] = [];
81 |
82 | constructor(private model: Model) {
83 | this.disposables.push(
84 | model.onDidChangeRepository(this.onDidChangeRepository, this),
85 | model.onDidChangeOriginalResource(
86 | this.onDidChangeOriginalResource,
87 | this
88 | ),
89 | workspace.registerFileSystemProvider('fossil', this, {
90 | isReadonly: true,
91 | isCaseSensitive: true,
92 | })
93 | );
94 |
95 | setInterval(() => this.cleanup(), FIVE_MINUTES);
96 | }
97 |
98 | private onDidChangeRepository({ repository }: ModelChangeEvent): void {
99 | this.changedRepositoryRoots.add(repository.root);
100 | this.eventuallyFireChangeEvents();
101 | }
102 |
103 | private onDidChangeOriginalResource({
104 | uri,
105 | }: OriginalResourceChangeEvent): void {
106 | if (uri.scheme !== 'file') {
107 | return;
108 | }
109 |
110 | const fossilUri = toFossilUri(uri);
111 | this.mtime = new Date().getTime();
112 | this._onDidChangeFile.fire([
113 | { type: FileChangeType.Changed, uri: fossilUri },
114 | ]);
115 | }
116 |
117 | @debounce(1100)
118 | private eventuallyFireChangeEvents(): void {
119 | this.fireChangeEvents();
120 | }
121 |
122 | @throttle
123 | private async fireChangeEvents(): Promise {
124 | if (!window.state.focused) {
125 | const onDidFocusWindow = filterEvent(
126 | window.onDidChangeWindowState,
127 | e => e.focused
128 | );
129 | await eventToPromise(onDidFocusWindow);
130 | }
131 |
132 | const events: FileChangeEvent[] = [];
133 |
134 | for (const { uri } of this.cache.values()) {
135 | const fsPath = uri.fsPath;
136 |
137 | for (const root of this.changedRepositoryRoots) {
138 | if (isDescendant(root, fsPath)) {
139 | events.push({ type: FileChangeType.Changed, uri });
140 | break;
141 | }
142 | }
143 | }
144 |
145 | if (events.length > 0) {
146 | this.mtime = new Date().getTime();
147 | this._onDidChangeFile.fire(events);
148 | }
149 |
150 | this.changedRepositoryRoots.clear();
151 | }
152 |
153 | watch(): Disposable {
154 | return EmptyDisposable;
155 | }
156 |
157 | async stat(uri: Uri): Promise {
158 | await this.model.isInitialized;
159 |
160 | const repository = this.model.getRepository(uri);
161 | if (!repository) {
162 | throw FileSystemError.FileNotFound();
163 | }
164 | return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 };
165 | }
166 |
167 | async readFile(uri: Uri): Promise {
168 | await this.model.isInitialized;
169 |
170 | const repository = this.model.getRepository(uri);
171 |
172 | if (!repository) {
173 | throw FileSystemError.FileNotFound();
174 | }
175 |
176 | const cacheKey = uri.toString();
177 | const timestamp = new Date().getTime();
178 | const cacheValue: CacheRow = { uri, timestamp };
179 |
180 | this.cache.set(cacheKey, cacheValue);
181 |
182 | const content = await repository.cat(fromFossilUri(uri));
183 | if (content !== undefined) {
184 | return content;
185 | }
186 | throw FileSystemError.FileNotFound();
187 | }
188 |
189 | /* c8 ignore start */
190 | readDirectory(): never {
191 | not_implemented();
192 | }
193 |
194 | createDirectory(): never {
195 | not_implemented();
196 | }
197 |
198 | writeFile(): never {
199 | not_implemented();
200 | }
201 |
202 | delete(): never {
203 | not_implemented();
204 | }
205 |
206 | rename(): never {
207 | not_implemented();
208 | }
209 | /* c8 ignore stop */
210 |
211 | private cleanup(): void {
212 | const now = new Date().getTime();
213 | const cache = new Map();
214 |
215 | for (const row of this.cache.values()) {
216 | const { path } = fromFossilUri(row.uri);
217 | const isOpen = workspace.textDocuments
218 | .filter(d => d.uri.scheme === 'file')
219 | .some(d => pathEquals(d.uri.fsPath, path));
220 |
221 | if (isOpen || now - row.timestamp < THREE_MINUTES) {
222 | cache.set(row.uri.toString(), row);
223 | } else {
224 | // TODO: should fire delete events?
225 | }
226 | }
227 |
228 | this.cache = cache;
229 | }
230 |
231 | dispose(): void {
232 | this.disposables.forEach(d => d.dispose());
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/test/suite/timelineSuite.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { window } from 'vscode';
3 | import * as assert from 'assert/strict';
4 | import { FossilUriParams, toFossilUri } from '../../uri';
5 | import type { FossilCWD } from '../../fossilExecutable';
6 | import { add, cleanupFossil, getExecutable, getRepository } from './common';
7 | import { Suite, before } from 'mocha';
8 | import * as sinon from 'sinon';
9 | import type { FossilCheckin } from '../../openedRepository';
10 |
11 | // separate function because hash size is different
12 | const uriMatch = (uri: vscode.Uri, checkin: FossilCheckin) =>
13 | sinon.match((exp: vscode.Uri): boolean => {
14 | const exp_q = JSON.parse(exp.query) as FossilUriParams;
15 | return (
16 | uri.path == exp.path &&
17 | uri.fsPath == exp_q.path &&
18 | exp_q.checkin.slice(0, 40) == checkin
19 | );
20 | });
21 |
22 | export function timelineSuite(this: Suite): void {
23 | let file2uri: vscode.Uri;
24 |
25 | before(async () => {
26 | await cleanupFossil(getRepository());
27 | await add('file1.txt', 'line1\n', 'file1.txt: first');
28 | await add('file1.txt', 'line1\nline2\n', 'file1.txt: second', 'SKIP');
29 | await add(
30 | 'file1.txt',
31 | 'line1\nline2\nline3\n',
32 | 'file1.txt: third',
33 | 'SKIP'
34 | );
35 | await add('file2.txt', 'line1\n', 'file2.txt: first');
36 | await add('file2.txt', 'line1\nline2\n', 'file2.txt: second', 'SKIP');
37 | file2uri = await add(
38 | 'file2.txt',
39 | 'line1\nline2\nline3\n',
40 | 'file2.txt: third',
41 | 'SKIP'
42 | );
43 | });
44 |
45 | test('`fossil.fileLog` undefined', async () => {
46 | await vscode.commands.executeCommand('fossil.fileLog');
47 | });
48 |
49 | test('Show diff from `fossil.fileLog`', async () => {
50 | const repository = getRepository();
51 | const showQuickPick = this.ctx.sandbox.stub(window, 'showQuickPick');
52 | showQuickPick.onFirstCall().callsFake(items => {
53 | assert.ok(items instanceof Array);
54 | assert.equal(items[0].label, '$(tag) Current');
55 | return Promise.resolve(items[0]);
56 | });
57 | showQuickPick.onSecondCall().callsFake(items => {
58 | assert.ok(items instanceof Array);
59 | assert.equal(items[0].label, '$(circle-outline) Parent');
60 | return Promise.resolve(items[0]);
61 | });
62 |
63 | const diffCommand = this.ctx.sandbox
64 | .stub(vscode.commands, 'executeCommand')
65 | .callThrough()
66 | .withArgs('vscode.diff')
67 | .resolves();
68 |
69 | await vscode.commands.executeCommand('fossil.fileLog', file2uri);
70 | sinon.assert.calledTwice(showQuickPick);
71 |
72 | const parentHash = await repository.getInfo('current', 'parent');
73 | sinon.assert.calledOnceWithExactly(
74 | diffCommand,
75 | 'vscode.diff',
76 | toFossilUri(file2uri, 'current'),
77 | toFossilUri(file2uri, parentHash),
78 | `file2.txt (current vs. ${parentHash.slice(0, 12)})`
79 | );
80 | }).timeout(2000);
81 |
82 | const testDiff = async (
83 | callback: (
84 | items: readonly vscode.QuickPickItem[]
85 | ) => Thenable
86 | ) => {
87 | const repository = getRepository();
88 | const showQuickPick = this.ctx.sandbox.stub(window, 'showQuickPick');
89 | showQuickPick.onFirstCall().callsFake(items => {
90 | assert.ok(items instanceof Array);
91 | assert.equal(items[0].label, '$(tag) Current');
92 | return Promise.resolve(items[0]);
93 | });
94 | showQuickPick.onSecondCall().callsFake(items => {
95 | assert.ok(items instanceof Array);
96 | return callback(items);
97 | });
98 |
99 | const diffCommand = this.ctx.sandbox
100 | .stub(vscode.commands, 'executeCommand')
101 | .callThrough()
102 | .withArgs('vscode.diff')
103 | .resolves();
104 |
105 | await vscode.commands.executeCommand('fossil.log');
106 | sinon.assert.calledTwice(showQuickPick);
107 |
108 | const currentHash = await repository.getInfo('current', 'hash');
109 | const parentHash = await repository.getInfo(currentHash, 'parent');
110 |
111 | sinon.assert.calledOnceWithExactly(
112 | diffCommand,
113 | 'vscode.diff',
114 | uriMatch(file2uri, parentHash),
115 | uriMatch(file2uri, currentHash),
116 | `file2.txt (${parentHash.slice(0, 12)} vs. ${currentHash.slice(
117 | 0,
118 | 12
119 | )})`,
120 | { preview: false }
121 | );
122 | };
123 |
124 | test('Show diff from `fossil.Log`', async () => {
125 | await testDiff(items => {
126 | assert.equal(items[4].label, ' M file2.txt');
127 | return Promise.resolve(items[4]);
128 | });
129 | });
130 |
131 | test('Show diff all from `fossil.Log`', async () => {
132 | await testDiff(items => {
133 | assert.equal(
134 | items[2].label,
135 | '$(go-to-file) Open all changed files'
136 | );
137 | return Promise.resolve(items[2]);
138 | });
139 | });
140 |
141 | test('Amend commit message', async () => {
142 | const cwd = this.ctx.workspaceUri.fsPath as FossilCWD;
143 |
144 | await add('amend.txt', '\n', 'message to amend');
145 |
146 | const showQuickPick = this.ctx.sandbox.stub(window, 'showQuickPick');
147 | showQuickPick.onFirstCall().callsFake(items => {
148 | assert.ok(items instanceof Array);
149 | assert.equal(items[0].label, '$(tag) Current');
150 | return Promise.resolve(items[0]);
151 | });
152 | showQuickPick.onSecondCall().callsFake(items => {
153 | assert.ok(items instanceof Array);
154 | const aItem = items.find(
155 | item => item.label === ' A amend.txt'
156 | );
157 | assert.ok(aItem);
158 | assert.equal(aItem.description, '.');
159 | assert.equal(items[1].label, '$(edit) Edit commit message');
160 | return Promise.resolve(items[1]);
161 | });
162 | const messageStub = this.ctx.sandbox
163 | .stub(window, 'showInputBox')
164 | .withArgs(sinon.match({ placeHolder: 'Commit message' }))
165 | .resolves('updated commit message');
166 | const sim: sinon.SinonStub = this.ctx.sandbox
167 | .stub(window, 'showInformationMessage')
168 | .resolves();
169 |
170 | await vscode.commands.executeCommand('fossil.log');
171 | sinon.assert.calledTwice(showQuickPick);
172 | sinon.assert.calledOnceWithExactly(messageStub, {
173 | value: 'message to amend',
174 | placeHolder: 'Commit message',
175 | prompt: 'Please provide a commit message',
176 | ignoreFocusOut: true,
177 | });
178 | sinon.assert.calledOnceWithExactly(sim, 'Commit message was updated.');
179 |
180 | const executable = getExecutable();
181 | const stdout = (await executable.exec(cwd, ['info'])).stdout;
182 | assert.match(stdout, /updated commit message/m);
183 | }).timeout(5000);
184 | }
185 |
--------------------------------------------------------------------------------
/src/test/suite/revertSuite.ts:
--------------------------------------------------------------------------------
1 | import { Uri, window, commands } from 'vscode';
2 | import * as sinon from 'sinon';
3 | import { add, fakeFossilStatus, getExecStub, getRepository } from './common';
4 | import * as assert from 'assert/strict';
5 | import * as fs from 'fs/promises';
6 | import type { Suite } from 'mocha';
7 | import type { FossilResourceGroup } from '../../resourceGroups';
8 | import type { FossilResource } from '../../repository';
9 | import { Reason } from '../../fossilExecutable';
10 | import { RelativePath } from '../../openedRepository';
11 |
12 | export function RevertSuite(this: Suite): void {
13 | test('Single source', async () => {
14 | const url = await add(
15 | 'revert_me.txt',
16 | 'Some original text\n',
17 | 'add revert_me.txt'
18 | );
19 | await fs.writeFile(url.fsPath, 'something new');
20 |
21 | const repository = getRepository();
22 | await repository.updateStatus('Test' as Reason);
23 | const resource = repository.workingGroup.getResource(url);
24 | assert.ok(resource);
25 |
26 | const showWarningMessage: sinon.SinonStub = this.ctx.sandbox.stub(
27 | window,
28 | 'showWarningMessage'
29 | );
30 | showWarningMessage.onFirstCall().resolves('&&Discard Changes');
31 |
32 | await commands.executeCommand('fossil.revert', resource);
33 | const newContext = await fs.readFile(url.fsPath);
34 | assert.equal(newContext.toString('utf-8'), 'Some original text\n');
35 | });
36 |
37 | suite('Dialog has no typos', () => {
38 | let swmResources: FossilResource[];
39 | const prepareResources = async () => {
40 | if (swmResources) {
41 | return swmResources;
42 | }
43 | const repository = getRepository();
44 | const fake_status = [];
45 | const execStub = getExecStub(this.ctx.sandbox);
46 | const fileUris: Uri[] = [];
47 | for (const filename of 'abcdefghijklmn') {
48 | // 14 files
49 | const fileUri = Uri.joinPath(
50 | this.ctx.workspaceUri,
51 | 'added',
52 | filename
53 | );
54 | const action = ['k', 'l', 'm', 'n'].includes(filename)
55 | ? 'ADDED'
56 | : 'EDITED';
57 | fake_status.push(`${action} added/${filename}`);
58 | fileUris.push(fileUri);
59 | }
60 | const statusCall = fakeFossilStatus(
61 | execStub,
62 | fake_status.join('\n')
63 | );
64 | await repository.updateStatus('Test' as Reason);
65 | sinon.assert.calledOnce(statusCall);
66 | const resources = fileUris.map(uri => {
67 | const resource = repository.workingGroup.getResource(uri);
68 | assert.ok(resource);
69 | return resource;
70 | });
71 | swmResources = resources;
72 | return swmResources;
73 | };
74 |
75 | test('10 + 4 files', async () => {
76 | const resources = await prepareResources();
77 | const swm = this.ctx.sandbox.stub(window, 'showWarningMessage');
78 | await commands.executeCommand('fossil.revert', ...resources);
79 | sinon.assert.calledOnceWithMatch(
80 | swm,
81 | 'Are you sure you want to discard changes to 10 files?\n' +
82 | '\n • a\n • b\n • c\n • d\n • e\n • f\n • g\n • h\n' +
83 | 'and 2 others\n\n(and forget 4 other added files)',
84 | { modal: true }
85 | );
86 | });
87 |
88 | test('10 files', async () => {
89 | const resources = await prepareResources();
90 | const swm = this.ctx.sandbox.stub(window, 'showWarningMessage');
91 | await commands.executeCommand(
92 | 'fossil.revert',
93 | ...resources.slice(0, 10)
94 | );
95 | sinon.assert.calledOnceWithMatch(
96 | swm,
97 | 'Are you sure you want to discard changes to 10 files?\n' +
98 | '\n • a\n • b\n • c\n • d\n • e\n • f\n • g\n • h\n' +
99 | 'and 2 others',
100 | { modal: true }
101 | );
102 | });
103 |
104 | test('3 files', async () => {
105 | const resources = await prepareResources();
106 | const swm = this.ctx.sandbox.stub(window, 'showWarningMessage');
107 | await commands.executeCommand(
108 | 'fossil.revert',
109 | ...resources.slice(0, 3)
110 | );
111 | sinon.assert.calledOnceWithMatch(
112 | swm,
113 | 'Are you sure you want to discard changes to 3 files?\n' +
114 | '\n • a\n • b\n • c',
115 | { modal: true }
116 | );
117 | });
118 |
119 | test('2 added file', async () => {
120 | const resources = await prepareResources();
121 | const swm = this.ctx.sandbox.stub(window, 'showWarningMessage');
122 | await commands.executeCommand(
123 | 'fossil.revert',
124 | ...resources.slice(10, 12)
125 | );
126 | sinon.assert.notCalled(swm);
127 | });
128 | });
129 |
130 | test('Revert (Nothing)', async () => {
131 | await commands.executeCommand('fossil.revert');
132 | });
133 |
134 | // for testing `fossil.revertAll` only
135 | async function revertAllTest(
136 | sandbox: sinon.SinonSandbox,
137 | groups: FossilResourceGroup[],
138 | message: string,
139 | files: string[]
140 | ): Promise {
141 | const swm: sinon.SinonStub = sandbox.stub(window, 'showWarningMessage');
142 | swm.onFirstCall().resolves('&&Discard Changes');
143 |
144 | const repository = getRepository();
145 | const execStub = getExecStub(sandbox);
146 | const statusStub = fakeFossilStatus(
147 | execStub,
148 | 'EDITED a.txt\nEDITED b.txt\nCONFLICT c.txt\nCONFLICT d.txt'
149 | );
150 | const revertStub = execStub
151 | .withArgs(sinon.match.array.startsWith(['revert']))
152 | .resolves();
153 | await repository.updateStatus('Test' as Reason);
154 | sinon.assert.calledOnce(statusStub);
155 | await commands.executeCommand('fossil.revertAll', ...groups);
156 | sinon.assert.calledOnceWithExactly(
157 | swm,
158 | message,
159 | { modal: true },
160 | '&&Discard Changes'
161 | );
162 | sinon.assert.calledOnceWithExactly(revertStub, [
163 | 'revert',
164 | '--',
165 | ...(files as RelativePath[]),
166 | ]);
167 | }
168 |
169 | test('Revert all (no groups)', async () => {
170 | await revertAllTest(
171 | this.ctx.sandbox,
172 | [],
173 | 'Are you sure you want to discard changes in ' +
174 | '"Changes" and "Unresolved Conflicts" group?',
175 | ['a.txt', 'b.txt', 'c.txt', 'd.txt']
176 | );
177 | });
178 |
179 | test('Revert all (changes group)', async () => {
180 | const repository = getRepository();
181 | await revertAllTest(
182 | this.ctx.sandbox,
183 | [repository.workingGroup],
184 | 'Are you sure you want to discard changes in "Changes" group?',
185 | ['a.txt', 'b.txt']
186 | );
187 | });
188 |
189 | test('Revert all (conflict group)', async () => {
190 | const repository = getRepository();
191 | await revertAllTest(
192 | this.ctx.sandbox,
193 | [repository.conflictGroup],
194 | 'Are you sure you want to discard changes ' +
195 | 'in "Unresolved Conflicts" group?',
196 | ['c.txt', 'd.txt']
197 | );
198 | });
199 | }
200 |
--------------------------------------------------------------------------------
/pikchr/test/advanced.test.pikchr:
--------------------------------------------------------------------------------
1 | // SYNTAX TEST "source.pikchr" "long statements"
2 |
3 | line right even with $r left of X9 then up until even with VARIABLE.n
4 | // <---- storage.type.class.pikchr
5 | // ^^^^^ ^^^^ ^^ support.constant.edge.pikchr
6 | // ^^^^ ^^^^ ^^^^^ ^^^^ ^^^^ keyword.pikchr
7 | // ^^ variable.language.pikchr
8 | // ^^ keyword.control.of.pikchr
9 | // ^^ ^^^^^^^^ variable.language.place.pikchr
10 | // ^^^^ keyword.pikchr
11 |
12 | circle rad 0.05 at Paper + (-0.055, -0.25)
13 | // <------ storage.type.class.pikchr
14 | // ^^^ support.constant.property-value.pikchr
15 | // ^^^^ constant.numeric.pikchr
16 | // ^^ keyword.pikchr
17 | // ^^^^^ variable.language.place.pikchr
18 | // ^ keyword.operator.arithmetic.pikchr
19 | // ^ punctuation.parenthesis.begin.pikchr
20 | // ^ ^ keyword.operator.arithmetic.pikchr
21 | // ^^^^^ ^^^^ constant.numeric.pikchr
22 | // ^ punctuation.parenthesis.end.pikchr
23 |
24 |
25 | line thick right from 0.3cm below start of previous
26 | // <---- storage.type.class.pikchr
27 | // ^^^^^ entity.name.tag.pikchr
28 | // ^^^^^ support.constant.edge.pikchr
29 | // ^^^^ keyword.pikchr
30 | // ^^^^^ constant.numeric.pikchr
31 | // ^^ keyword.other.unit.cm.pikchr
32 | // ^^^^^ keyword.pikchr
33 | // ^^^^^ support.constant.edge.pikchr
34 | // ^^ keyword.control.of.pikchr
35 | // ^^^^^^^^ constant.language.pikchr
36 |
37 | oval width OVAL1.height height OVAL1.width
38 | // <---- storage.type.class.pikchr
39 | // ^^^^^ ^^^^^^ support.constant.property-value.pikchr
40 | // ^^^^^ ^^^^^ variable.language.place.pikchr
41 | // ^ ^ punctuation.separator.period.pikchr
42 | // ^^^^^^ ^^^^^ support.constant.property-value.pikchr
43 |
44 | oval "" fit at $r*1.2 below 1/2 way between X0 and X9
45 | // <---- storage.type.class.pikchr
46 | // ^^ string.quoted.double.pikchr
47 | // ^^^ entity.name.tag.pikchr
48 | // ^ -entity.name.tag.pikchr
49 | // ^^ keyword.pikchr
50 | // ^^ variable.language.pikchr
51 | // ^ keyword.operator.arithmetic.pikchr
52 | // ^^^ ^ ^ constant.numeric.pikchr
53 | // ^^^^^ keyword.pikchr
54 | // ^ keyword.operator.arithmetic.pikchr
55 | // ^^^ ^^^^^^^ keyword.pikchr
56 | // ^^ ^^ variable.language.place.pikchr
57 | // ^^^ keyword.pikchr
58 |
59 | arrow from TYPENAME.e right even with ATTR
60 | // <----- storage.type.class.pikchr
61 | // ^^^^ keyword.pikchr
62 | // ^^^^^^^^ ^^^^ variable.language.place.pikchr
63 | // ^ punctuation.separator.period.pikchr
64 | // ^ constant.language.pikchr
65 | // ^^^^^ support.constant.edge.pikchr
66 | // ^^^^ ^^^^ keyword.pikchr
67 |
68 | box "expr" fit with .w at 1.75*$h below BTW.w
69 | // <--- storage.type.class.pikchr
70 | // ^^^^^^ string.quoted.double.pikchr
71 | // ^^^ entity.name.tag.pikchr
72 | // ^^^^ keyword.pikchr
73 | // ^ punctuation.separator.period.pikchr
74 | // ^ constant.language.pikchr
75 | // ^^ keyword.pikchr
76 | // ^^^^ constant.numeric.pikchr
77 | // ^ keyword.operator.arithmetic.pikchr
78 | // ^^ variable.language.pikchr
79 | // ^^^^^ keyword.pikchr
80 | // ^^^ variable.language.place.pikchr
81 | // ^ punctuation.separator.period.pikchr
82 | // ^ constant.language.pikchr
83 | box (thickness)
84 | // <--- storage.type.class.pikchr
85 | // ^ punctuation.parenthesis.begin.pikchr
86 | // ^^^^^^^^^ variable.language.pikchr
87 | // ^ source.pikchr punctuation.parenthesis.end.pikchr
88 |
89 | spline thin color gray <-> \
90 | // <------ storage.type.class.pikchr
91 | // ^^^^ entity.name.tag.pikchr
92 | // ^^^^^ support.constant.property-value.pikchr
93 | // ^^^^ support.constant.color.w3c-standard-color-name.pikchr
94 | // ^^^ entity.name.tag.pikchr
95 | // ^ source.pikchr
96 | // ^ punctuation.separator.continuation.line.pikchr
97 | from d1+8mm heading 0 from C2
98 | // <---- keyword.pikchr
99 | // ^^ variable.language.pikchr
100 | // ^ keyword.operator.arithmetic.pikchr
101 | // ^^^ constant.numeric.pikchr
102 | // ^^ keyword.other.unit.mm.pikchr
103 | // ^^^^^^^ keyword.pikchr
104 | // ^ constant.numeric.pikchr
105 | // ^^^^ keyword.pikchr
106 | // ^^ variable.language.place.pikchr
107 |
108 | circle "C3" at dist(C2,C4) heading 30 from C2
109 | // ^^^^ entity.name.function.pikchr
110 | // ^^^^^^^ ^^^^ keyword.pikchr
111 | // ^^ ^^ ^^ variable.language.place.pikchr
112 |
113 | box height C3.y-C2.y \
114 | // ^^^^^ support.constant.property-value.pikchr
115 | width (C5P.e.x-C0.w.x)+linewid \
116 | // ^^^^^ support.constant.property-value.pikchr
117 | // ^ ^ ^ ^ constant.language.pikchr
118 | with .w at 0.5*linewid west of C0.w \
119 | // ^^^^ keyword.pikchr
120 | // ^ ^ constant.language.pikchr
121 | behind C0 \
122 | // ^^^^^^ keyword.pikchr
123 | fill 0xc6e2ff thin color gray
124 | // ^^^^ support.constant.property-value.pikchr
125 |
126 | text "radius" at (6/8,1/2 /*comment*/)
127 | // <---- storage.type.class.pikchr
128 | // ^^^^^^^^ string.quoted.double.pikchr
129 | // ^^ keyword.pikchr
130 | // ^ punctuation.parenthesis.begin.pikchr
131 | // ^ ^ ^ ^ constant.numeric.pikchr
132 | // ^ ^keyword.operator.arithmetic.pikchr
133 | // ^ ^ punctuation.bracket.angle.begin.pikchr
134 | // ^ ^ punctuation.bracket.angle.end.pikchr
135 | // ^ punctuation.parenthesis.end.pikchr
136 | // ^^ ^^ ^^ ^^ variable.language.place.pikchr
137 | // ^ ^ punctuation.separator.period.pikchr
138 | // ^ punctuation.separator.pikchr
139 | // ^^^^^^^^^^^ comment.block.pikchr
140 |
141 | "Pikchr source " rjust "code input " rjust at 2nd vertex /**/ of previous
142 | // <---------------- string.quoted.double.pikchr
143 | // ^^^^^^^^^^^^^ string.quoted.double.pikchr
144 | // ^^^^^ ^^^^^ ^^ ^^^^^^ ^^ keyword.pikchr
145 | // ^^ constant.language.pikchr
146 | // ^^^^ comment.block.pikchr
147 | // ^^^^^^^^ constant.language.pikchr
148 |
149 | diamond;One: [line ->] with .n at 1cm below previous
150 | // <------- storage.type.class.pikchr
151 | // ^^^^ storage.type.class.pikchr
152 | // ^ punctuation.separator.delimiter.end.pikchr
153 | // ^^^ variable.language.place.pikchr
154 | // ^ punctuation.separator.pikchr
155 | // ^ punctuation.bracket.square.begin.pikchr
156 | // ^^ entity.name.tag.pikchr
157 | // ^ punctuation.bracket.square.end.pikchr
158 | // ^^^^ ^^ ^^^^^ keyword.pikchr
159 | // ^ punctuation.separator.period.pikchr
160 | // ^ constant.language.pikchr
161 | // ^^^^^^^^ constant.language.pikchr
162 | LE2: box "expr" fit with .w at (4*arrowht+linerad east of LOP3.e,LIKE)
163 | // ^^^^ support.constant.edge.pikchr
164 | // ^^ keyword.control.of.pikchr
165 |
--------------------------------------------------------------------------------
/src/test/suite/branchSuite.ts:
--------------------------------------------------------------------------------
1 | import { InputBox } from 'vscode';
2 | import { window, commands } from 'vscode';
3 | import * as sinon from 'sinon';
4 | import { fakeExecutionResult, getExecStub } from './common';
5 | import * as assert from 'assert/strict';
6 | import { Suite } from 'mocha';
7 | import { FossilBranch } from '../../openedRepository';
8 |
9 | export function BranchSuite(this: Suite): void {
10 | test('Create public branch', async () => {
11 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
12 | cib.onFirstCall().callsFake(() => {
13 | const inputBox: InputBox = cib.wrappedMethod();
14 | const stub = sinon.stub(inputBox);
15 | stub.show.callsFake(() => {
16 | stub.value = 'hello branch';
17 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
18 | const onDidChangeValue =
19 | stub.onDidChangeValue.getCall(0).args[0];
20 | const onDidTriggerButton =
21 | stub.onDidTriggerButton.getCall(0).args[0];
22 | onDidTriggerButton(stub.buttons[1]); // private on
23 | onDidTriggerButton(stub.buttons[1]); // private off
24 | onDidChangeValue(stub.value);
25 | assert.equal(stub.validationMessage, '');
26 | onDidAccept();
27 | });
28 | return stub;
29 | });
30 |
31 | const creation = getExecStub(this.ctx.sandbox).withArgs([
32 | 'branch',
33 | 'new',
34 | 'hello branch',
35 | 'current',
36 | ]);
37 | await commands.executeCommand('fossil.branch');
38 | sinon.assert.calledOnce(creation);
39 | });
40 |
41 | test('Branch already exists warning is shown', async () => {
42 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
43 | cib.onFirstCall().callsFake(() => {
44 | const inputBox: InputBox = cib.wrappedMethod();
45 | const stub = sinon.stub(inputBox);
46 | stub.show.callsFake(() => {
47 | stub.value = 'hello branch';
48 | stub.onDidAccept.getCall(0).args[0]();
49 | });
50 | return stub;
51 | });
52 |
53 | const swm: sinon.SinonStub = this.ctx.sandbox
54 | .stub(window, 'showWarningMessage')
55 | .resolves();
56 |
57 | const creation = getExecStub(this.ctx.sandbox).withArgs([
58 | 'branch',
59 | 'new',
60 | 'hello branch',
61 | 'current',
62 | ]);
63 | await commands.executeCommand('fossil.branch');
64 | sinon.assert.calledOnce(creation);
65 | sinon.assert.calledOnceWithExactly(
66 | swm,
67 | "Branch 'hello branch' already exists. Update or Re-open?",
68 | {
69 | modal: true,
70 | },
71 | '&&Update',
72 | '&&Re-open'
73 | );
74 | }).timeout(14500);
75 |
76 | test('Create private branch', async () => {
77 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
78 | cib.onFirstCall().callsFake(() => {
79 | const inputBox: InputBox = cib.wrappedMethod();
80 | const stub = sinon.stub(inputBox);
81 | stub.show.callsFake(() => {
82 | stub.value = 'hello branch';
83 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
84 | const onDidTriggerButton =
85 | stub.onDidTriggerButton.getCall(0).args[0];
86 | onDidTriggerButton(stub.buttons[1]); // private on
87 | onDidAccept();
88 | });
89 | return stub;
90 | });
91 |
92 | const execStub = getExecStub(this.ctx.sandbox);
93 | const creation = execStub
94 | .withArgs(['branch', 'new', 'hello branch', 'current', '--private'])
95 | .resolves(fakeExecutionResult());
96 | await commands.executeCommand('fossil.branch');
97 | sinon.assert.calledOnce(creation);
98 | });
99 |
100 | test('Create branch with color', async () => {
101 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
102 | cib.onFirstCall().callsFake(() => {
103 | const inputBox: InputBox = cib.wrappedMethod();
104 | const stub = sinon.stub(inputBox);
105 | stub.show.callsFake(() => {
106 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
107 | const onDidTriggerButton =
108 | stub.onDidTriggerButton.getCall(0).args[0];
109 | onDidTriggerButton(stub.buttons[0]);
110 | stub.value = '#aabbcc';
111 | onDidAccept();
112 | stub.value = 'color branch';
113 | onDidAccept();
114 | });
115 | return stub;
116 | });
117 | const execStub = getExecStub(this.ctx.sandbox);
118 | const creation = execStub
119 | .withArgs([
120 | 'branch',
121 | 'new',
122 | 'color branch',
123 | 'current',
124 | '--bgcolor',
125 | '#aabbcc',
126 | ])
127 | .resolves(fakeExecutionResult());
128 | await commands.executeCommand('fossil.branch');
129 | sinon.assert.calledOnce(creation);
130 | });
131 |
132 | test('Create branch canceled', async () => {
133 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
134 | cib.onFirstCall().callsFake(() => {
135 | const inputBox: InputBox = cib.wrappedMethod();
136 | const stub = sinon.stub(inputBox);
137 | stub.show.callsFake(() => {
138 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
139 | stub.value = '';
140 | onDidAccept();
141 | });
142 | return stub;
143 | });
144 | await commands.executeCommand('fossil.branch');
145 | sinon.assert.calledOnce(cib);
146 | });
147 |
148 | test('Branch already exists - reopen branch', async () => {
149 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
150 | cib.onFirstCall().callsFake(() => {
151 | const inputBox: InputBox = cib.wrappedMethod();
152 | const stub = sinon.stub(inputBox);
153 | stub.show.callsFake(() => {
154 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
155 | stub.value = 'trunk';
156 | onDidAccept();
157 | });
158 | return stub;
159 | });
160 | const swm: sinon.SinonStub = this.ctx.sandbox
161 | .stub(window, 'showWarningMessage')
162 | .resolves('&&Re-open' as any);
163 |
164 | const execStub = getExecStub(this.ctx.sandbox);
165 | const newBranchStub = execStub
166 | .withArgs(['branch', 'new', 'trunk', 'current'])
167 | .onSecondCall()
168 | .resolves(fakeExecutionResult());
169 |
170 | await commands.executeCommand('fossil.branch');
171 | sinon.assert.calledOnce(cib);
172 | sinon.assert.calledOnce(swm);
173 | sinon.assert.calledTwice(newBranchStub);
174 | });
175 |
176 | test('Branch already exists - update to', async () => {
177 | const cib = this.ctx.sandbox.stub(window, 'createInputBox');
178 | cib.onFirstCall().callsFake(() => {
179 | const inputBox: InputBox = cib.wrappedMethod();
180 | const stub = sinon.stub(inputBox);
181 | stub.show.callsFake(() => {
182 | const onDidAccept = stub.onDidAccept.getCall(0).args[0];
183 | stub.value = 'trunk';
184 | onDidAccept();
185 | });
186 | return stub;
187 | });
188 | const swm: sinon.SinonStub = this.ctx.sandbox
189 | .stub(window, 'showWarningMessage')
190 | .resolves('&&Update' as any);
191 |
192 | const execStub = getExecStub(this.ctx.sandbox);
193 | const newBranchStub = execStub.withArgs(
194 | sinon.match.array.startsWith(['branch', 'new', 'trunk', 'current'])
195 | );
196 | const updateStub = execStub
197 | .withArgs(sinon.match.array.startsWith(['update', 'trunk']))
198 | .resolves(fakeExecutionResult());
199 |
200 | await commands.executeCommand('fossil.branch');
201 | sinon.assert.calledOnce(cib);
202 | sinon.assert.calledOnce(swm);
203 | sinon.assert.calledOnce(newBranchStub);
204 | sinon.assert.calledOnceWithExactly(
205 | updateStub,
206 | ['update', 'trunk' as FossilBranch],
207 | undefined,
208 | { logErrors: true }
209 | );
210 | });
211 | }
212 |
--------------------------------------------------------------------------------
/src/test/suite/renameSuite.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Uri, window, workspace, commands } from 'vscode';
3 | import * as sinon from 'sinon';
4 | import {
5 | add,
6 | assertGroups,
7 | cleanupFossil,
8 | fakeFossilStatus,
9 | getExecStub,
10 | getRepository,
11 | } from './common';
12 | import * as assert from 'assert/strict';
13 | import * as fs from 'fs/promises';
14 | import {
15 | FossilCommitMessage,
16 | OpenedRepository,
17 | RelativePath,
18 | ResourceStatus,
19 | } from '../../openedRepository';
20 | import { delay, eventToPromise } from '../../util';
21 | import { Suite, before } from 'mocha';
22 | import { Reason } from '../../fossilExecutable';
23 |
24 | export function RenameSuite(this: Suite): void {
25 | const rootUri = this.ctx.workspaceUri;
26 | const config = () => workspace.getConfiguration('fossil');
27 |
28 | before(async () => {
29 | await config().update('enableRenaming', true);
30 | await cleanupFossil(getRepository());
31 | });
32 |
33 | test('Rename a file', async () => {
34 | const oldFilename = 'not_renamed.txt';
35 | const newFilename = 'renamed.txt';
36 | await add(oldFilename, 'foo content\n', `add: ${oldFilename}`, 'ADDED');
37 |
38 | const sim: sinon.SinonStub = this.ctx.sandbox.stub(
39 | window,
40 | 'showInformationMessage'
41 | );
42 | const answeredYes = sim.onFirstCall().resolves('Yes');
43 |
44 | const edit = new vscode.WorkspaceEdit();
45 | const newFilePath = Uri.joinPath(rootUri, newFilename);
46 | edit.renameFile(Uri.joinPath(rootUri, oldFilename), newFilePath);
47 |
48 | const success = await workspace.applyEdit(edit);
49 | assert.ok(success);
50 |
51 | const repository = getRepository();
52 | await answeredYes;
53 | await eventToPromise(repository.onDidRunOperation);
54 | await repository.updateStatus('Test' as Reason);
55 |
56 | assertGroups(repository, {
57 | working: [[newFilePath.fsPath, ResourceStatus.RENAMED]],
58 | });
59 | await cleanupFossil(repository);
60 | }).timeout(6000);
61 |
62 | test("Don't show again", async () => {
63 | const repository = getRepository();
64 | assertGroups(repository, {}, "Previous test didn't cleanup or failed");
65 |
66 | assert.equal(config().get('enableRenaming'), true, 'contract');
67 | const execStub = getExecStub(this.ctx.sandbox);
68 | const oldFilename = 'do_not_show.txt';
69 | const oldUri = Uri.joinPath(rootUri, oldFilename);
70 | await fs.writeFile(oldUri.fsPath, '123');
71 | const newFilename = 'test_failed.txt';
72 |
73 | const edit = new vscode.WorkspaceEdit();
74 | const newFilePath = Uri.joinPath(rootUri, newFilename);
75 | edit.renameFile(oldUri, newFilePath);
76 |
77 | const sim = (
78 | this.ctx.sandbox.stub(
79 | window,
80 | 'showInformationMessage'
81 | ) as sinon.SinonStub
82 | ).resolves("Don't show again");
83 |
84 | const status = fakeFossilStatus(execStub, `EDITED ${oldFilename}\n`);
85 | const success = await workspace.applyEdit(edit);
86 | assert.ok(success);
87 | sinon.assert.calledOnceWithExactly(
88 | status,
89 | ['status', '--differ', '--merge'],
90 | 'file rename event' as Reason
91 | );
92 |
93 | // renaming triggers an async event, that is not awaited. await it.
94 | for (let i = 0; i < 200; ++i) {
95 | if (sim.callCount != 0) {
96 | break;
97 | }
98 | /* c8 ignore next 2 */
99 | await delay(5);
100 | }
101 | sinon.assert.calledOnceWithExactly(
102 | sim,
103 | '"do_not_show.txt" was renamed to "test_failed.txt" on ' +
104 | 'filesystem. Rename in fossil repository too?',
105 | {
106 | modal: false,
107 | },
108 | 'Yes',
109 | 'Cancel',
110 | "Don't show again"
111 | );
112 |
113 | for (let i = 1; i < 200; ++i) {
114 | if (config().get('enableRenaming') === false) {
115 | break;
116 | }
117 | await delay(5);
118 | }
119 | assert.equal(config().get('enableRenaming'), false, 'no update');
120 | await config().update('enableRenaming', true);
121 | assertGroups(repository, {
122 | working: [[oldUri.fsPath, ResourceStatus.MODIFIED]],
123 | });
124 | execStub.restore();
125 | await cleanupFossil(repository);
126 | }).timeout(3000);
127 |
128 | test('Rename directory', async () => {
129 | const repository = getRepository();
130 | assertGroups(repository, {}, "Previous test didn't cleanup or failed");
131 |
132 | const oldDirname = 'not_renamed';
133 | const newDirname = 'renamed';
134 | const oldDirUrl = Uri.joinPath(rootUri, oldDirname);
135 | const newDirUrl = Uri.joinPath(rootUri, newDirname);
136 | await fs.mkdir(oldDirUrl.fsPath);
137 | const filenames = ['mud', 'cabbage', 'brick'];
138 | const oldUris = filenames.map(filename =>
139 | Uri.joinPath(oldDirUrl, filename)
140 | );
141 | const newUris = filenames.map(filename =>
142 | Uri.joinPath(newDirUrl, filename)
143 | );
144 |
145 | await Promise.all(
146 | oldUris.map(uri => fs.writeFile(uri.fsPath, `foo ${uri}\n`))
147 | );
148 | const openedRepository: OpenedRepository = (repository as any)
149 | .repository;
150 | await openedRepository.exec(['add', '--', oldDirname as RelativePath]);
151 | await openedRepository.exec([
152 | 'commit',
153 | '-m',
154 | `add directory: ${oldDirname}` as FossilCommitMessage,
155 | '--no-warnings',
156 | ]);
157 |
158 | const sim: sinon.SinonStub = this.ctx.sandbox.stub(
159 | window,
160 | 'showInformationMessage'
161 | );
162 |
163 | const answeredYes = sim.onFirstCall().resolves('Yes');
164 |
165 | const edit = new vscode.WorkspaceEdit();
166 | edit.renameFile(oldDirUrl, newDirUrl);
167 |
168 | const success = await workspace.applyEdit(edit);
169 | assert.ok(success);
170 |
171 | await answeredYes;
172 | await eventToPromise(repository.onDidRunOperation);
173 | await repository.updateStatus('Test' as Reason);
174 |
175 | assertGroups(repository, {
176 | working: newUris.map((url: Uri) => [
177 | url.fsPath,
178 | ResourceStatus.RENAMED,
179 | ]),
180 | });
181 | await cleanupFossil(repository);
182 | }).timeout(10000);
183 |
184 | test('Relocate', async () => {
185 | const repository = getRepository();
186 | assertGroups(repository, {}, "Previous test didn't cleanup or failed");
187 | const oldFilename = 'not_relocated.txt';
188 | const newFilename = 'relocated.txt';
189 | const newUri = Uri.joinPath(rootUri, newFilename);
190 | const oldUri = await add(
191 | oldFilename,
192 | 'foo content\n',
193 | `add: ${oldFilename}`,
194 | 'ADDED'
195 | );
196 | await fs.rename(oldUri.fsPath, newUri.fsPath);
197 | await repository.updateStatus('Test' as Reason);
198 | assertGroups(repository, {
199 | working: [[oldUri.fsPath, ResourceStatus.MISSING]],
200 | untracked: [[newUri.fsPath, ResourceStatus.EXTRA]],
201 | });
202 | this.ctx.sandbox
203 | .stub(window, 'showQuickPick')
204 | .onFirstCall()
205 | .callsFake(items => {
206 | assert.ok(items instanceof Array);
207 | assert.equal(items.length, 3);
208 | assert.equal(items[0].label, '$(folder-opened) Open Dialog');
209 | assert.equal(items[1].label, 'Extras');
210 | assert.equal(items[2].label, '$(symbol-file) relocated.txt');
211 | return Promise.resolve(items[0]);
212 | });
213 |
214 | const sod = this.ctx.sandbox
215 | .stub(window, 'showOpenDialog')
216 | .resolves([newUri]);
217 | await commands.executeCommand(
218 | 'fossil.relocate',
219 | repository.workingGroup.resourceStates[0]
220 | );
221 | sinon.assert.calledOnce(sod);
222 | assertGroups(repository, {
223 | working: [[newUri.fsPath, ResourceStatus.RENAMED]],
224 | });
225 | }).timeout(10000);
226 |
227 | test('Relocate nothing', async () => {
228 | await commands.executeCommand('fossil.relocate');
229 | });
230 | }
231 |
--------------------------------------------------------------------------------
/src/test/suite/utilitiesSuite.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Uri } from 'vscode';
3 | import * as sinon from 'sinon';
4 | import * as assert from 'assert/strict';
5 | import * as fs from 'fs';
6 | import {
7 | assertGroups,
8 | getExecStub,
9 | getExecutable,
10 | getRepository,
11 | } from './common';
12 | import { Suite, afterEach, beforeEach } from 'mocha';
13 | import { debounce, memoize, sequentialize, throttle } from '../../decorators';
14 | import { delay } from '../../util';
15 | import { DocumentFsPath, Reason } from '../../fossilExecutable';
16 | import { ResourceStatus } from '../../openedRepository';
17 |
18 | function undoSuite(this: Suite) {
19 | test('Undo and redo warning', async () => {
20 | const showWarningMessage: sinon.SinonStub = this.ctx.sandbox.stub(
21 | vscode.window,
22 | 'showWarningMessage'
23 | );
24 | await vscode.commands.executeCommand('fossil.undo');
25 | sinon.assert.calledOnce(showWarningMessage);
26 | sinon.assert.calledWithExactly(
27 | showWarningMessage.firstCall,
28 | 'Nothing to undo.'
29 | );
30 | await vscode.commands.executeCommand('fossil.redo');
31 | sinon.assert.calledTwice(showWarningMessage);
32 | sinon.assert.calledWithExactly(
33 | showWarningMessage.secondCall,
34 | 'Nothing to redo.'
35 | );
36 | }).timeout(2000);
37 | test('Undo and redo working', async () => {
38 | const showWarningMessage: sinon.SinonStub = this.ctx.sandbox.stub(
39 | vscode.window,
40 | 'showWarningMessage'
41 | );
42 |
43 | const undoTxtPath = Uri.joinPath(
44 | this.ctx.workspaceUri,
45 | 'undo-fuarw.txt'
46 | ).fsPath as DocumentFsPath;
47 | await fs.promises.writeFile(undoTxtPath, 'line\n');
48 |
49 | const repository = getRepository();
50 | const execStub = getExecStub(this.ctx.sandbox);
51 |
52 | await repository.updateStatus('Test' as Reason);
53 | assertGroups(repository, {
54 | untracked: [[undoTxtPath, ResourceStatus.EXTRA]],
55 | });
56 |
57 | showWarningMessage.onFirstCall().resolves('&&Delete file');
58 |
59 | assert.ok(fs.existsSync(undoTxtPath));
60 | await vscode.commands.executeCommand(
61 | 'fossil.deleteFiles',
62 | repository.untrackedGroup
63 | );
64 | sinon.assert.calledOnceWithExactly(
65 | showWarningMessage,
66 | 'Are you sure you want to DELETE undo-fuarw.txt?\n' +
67 | 'This is IRREVERSIBLE!\n' +
68 | 'This file will be FOREVER LOST if you proceed.',
69 | { modal: true },
70 | '&&Delete file'
71 | );
72 | sinon.assert.calledWithExactly(execStub, ['clean', undoTxtPath]);
73 | assert.ok(!fs.existsSync(undoTxtPath));
74 |
75 | const showInformationMessage: sinon.SinonStub = this.ctx.sandbox.stub(
76 | vscode.window,
77 | 'showInformationMessage'
78 | );
79 |
80 | showInformationMessage.onFirstCall().resolves('Undo');
81 | await vscode.commands.executeCommand('fossil.undo');
82 | assert.ok(fs.existsSync(undoTxtPath));
83 | const executable = getExecutable();
84 | const fossilPath = (executable as any).fossilPath as string;
85 | assert.equal(
86 | showInformationMessage.firstCall.args[0],
87 | `Undo '${fossilPath} clean ${undoTxtPath}'?`
88 | );
89 |
90 | showInformationMessage.onSecondCall().resolves('Redo');
91 | await vscode.commands.executeCommand('fossil.redo');
92 | assert.ok(!fs.existsSync(undoTxtPath));
93 | assert.equal(
94 | showInformationMessage.secondCall.args[0],
95 | `Redo '${fossilPath} clean ${undoTxtPath}'?`
96 | );
97 | }).timeout(7000);
98 | }
99 |
100 | function decoratorsSuite(this: Suite) {
101 | let fakeTimers: sinon.SinonFakeTimers;
102 | const startTimeStamp = new Date('2024-03-13T00:00:00Z').getTime();
103 |
104 | beforeEach(() => {
105 | fakeTimers = sinon.useFakeTimers(startTimeStamp);
106 | });
107 | afterEach(() => {
108 | // assert.equal(fakeTimers.countTimers(), 0, 'All timers must run');
109 | fakeTimers.restore();
110 | });
111 |
112 | test('Memoize', async () => {
113 | class MemoizeTest {
114 | constructor(
115 | public memoize_count_a = 0,
116 | public memoize_count_b = 0
117 | ) {}
118 | @memoize
119 | public get memoized_property_a(): Uri {
120 | ++this.memoize_count_a;
121 | return Uri.file('memoize_a.txt');
122 | }
123 | @memoize
124 | public get memoized_property_b(): Uri {
125 | ++this.memoize_count_b;
126 | return Uri.file('memoize_b.txt');
127 | }
128 | public get counts(): [number, number] {
129 | return [this.memoize_count_a, this.memoize_count_b];
130 | }
131 | }
132 | const dt = new MemoizeTest();
133 | assert.equal(dt.memoized_property_a.fsPath, '/memoize_a.txt');
134 | assert.deepStrictEqual(dt.counts, [1, 0]);
135 | assert.equal(dt.memoized_property_a.fsPath, '/memoize_a.txt');
136 | assert.deepStrictEqual(dt.counts, [1, 0]);
137 | assert.equal(dt.memoized_property_b.fsPath, '/memoize_b.txt');
138 | assert.deepStrictEqual(dt.counts, [1, 1]);
139 | assert.equal(dt.memoized_property_b.fsPath, '/memoize_b.txt');
140 | assert.deepStrictEqual(dt.counts, [1, 1]);
141 | assert.equal(dt.memoized_property_a.fsPath, '/memoize_a.txt');
142 | assert.deepStrictEqual(dt.counts, [1, 1]);
143 | });
144 | test('Throttle', async () => {
145 | class ThrottledTest {
146 | constructor(public throttle_count = 0) {}
147 | @throttle
148 | async throttled_method(key: string): Promise {
149 | await delay(25);
150 | return `${key}-${this.throttle_count++}`;
151 | }
152 | }
153 | const dt = new ThrottledTest();
154 | assert.equal(fakeTimers.countTimers(), 0);
155 | const p0 = dt.throttled_method('a');
156 | const p1 = dt.throttled_method('b');
157 | const p2 = dt.throttled_method('c');
158 | assert.equal(fakeTimers.countTimers(), 1);
159 | const resPromise = Promise.all([p0, p1, p2]);
160 | await fakeTimers.runAllAsync();
161 | assert.equal(fakeTimers.countTimers(), 0);
162 | assert.deepStrictEqual(await resPromise, ['a-0', 'b-1', 'b-1']);
163 | assert.equal(fakeTimers.countTimers(), 0);
164 | assert.equal(dt.throttle_count, 2);
165 | });
166 | test('Sequentialize', async () => {
167 | class SequentializeTest {
168 | constructor(public sequentialize_count = 0) {}
169 | @sequentialize
170 | async sequentialized_method(
171 | ms: number,
172 | key: string
173 | ): Promise {
174 | await delay(ms);
175 | return `${key}-${this.sequentialize_count++}`;
176 | }
177 | }
178 |
179 | const dt = new SequentializeTest();
180 | const p0 = dt.sequentialized_method(50, 'a');
181 | const p1 = dt.sequentialized_method(20, 'b');
182 | const p2 = dt.sequentialized_method(10, 'c');
183 | const resPromise = Promise.race([p0, p1, p2]);
184 | assert.equal(fakeTimers.countTimers(), 0);
185 | await fakeTimers.runAllAsync();
186 | assert.deepStrictEqual(await resPromise, 'a-0');
187 | assert.equal(fakeTimers.countTimers(), 0);
188 | assert.equal(dt.sequentialize_count, 3);
189 | assert.deepStrictEqual(await p1, 'b-1');
190 | assert.deepStrictEqual(await p2, 'c-2');
191 | });
192 | test('Debounce', async () => {
193 | class DebounceTest {
194 | constructor(public debounce_count = 0) {}
195 | @debounce(50)
196 | debounced_method(): void {
197 | this.debounce_count++;
198 | }
199 | }
200 |
201 | const dt = new DebounceTest();
202 | assert.equal(fakeTimers.countTimers(), 0);
203 | const p0 = dt.debounced_method();
204 | assert.equal(fakeTimers.countTimers(), 1);
205 | const p1 = dt.debounced_method();
206 | const p2 = dt.debounced_method();
207 | const resPromise = Promise.all([p0, p1, p2]);
208 | assert.equal(fakeTimers.countTimers(), 1);
209 | const debounceTimer = fakeTimers.next();
210 | assert.deepEqual(await resPromise, [undefined, undefined, undefined]);
211 | assert.equal(fakeTimers.countTimers(), 0);
212 | assert.equal(debounceTimer, startTimeStamp + 50, 'main timer worked');
213 | assert.equal(dt.debounce_count, 1, 'reached main timer only');
214 | });
215 | }
216 |
217 | export function utilitiesSuite(this: Suite): void {
218 | suite('Undo', undoSuite.bind(this));
219 | suite('Decorators', decoratorsSuite.bind(this));
220 | test('Show output', async () => {
221 | await vscode.commands.executeCommand('fossil.showOutput');
222 | // currently there is no way to validate fossil.showOutput
223 | });
224 | test('Open UI', async () => {
225 | const sendText = sinon.stub();
226 | const terminal = {
227 | sendText: sendText as unknown,
228 | } as vscode.Terminal;
229 | const createTerminalStub = this.ctx.sandbox
230 | .stub(vscode.window, 'createTerminal')
231 | .returns(terminal);
232 | await vscode.commands.executeCommand('fossil.openUI');
233 | sinon.assert.calledOnce(createTerminalStub);
234 | sinon.assert.calledOnceWithExactly(sendText, 'fossil ui');
235 | });
236 | test('Commit input box knows which repository to use', () => {
237 | const repository = getRepository();
238 | assert.deepStrictEqual(repository.sourceControl.acceptInputCommand, {
239 | command: 'fossil.commitWithInput',
240 | title: 'Commit',
241 | arguments: [repository],
242 | });
243 | });
244 | }
245 |
--------------------------------------------------------------------------------