├── .eslintignore
├── .eslintrc.json
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── README.md
├── demo-media
├── formattedPasteImage.gif
├── icon-wirefrane-minimal.svg
├── icon.svg
├── liveSnippets.gif
├── preview-card.png
├── preview-card.svg
├── tikz-preview.gif
└── zotero-integration.gif
├── icon.png
├── package.json
├── resources
├── liveSnippetSchema.json
└── liveSnippets.json
├── scripts
├── countword-linux.sh
├── countword-win.bat
├── saveclipimg-linux.sh
├── saveclipimg-mac.applescript
└── saveclipimg-pc.ps1
├── src
├── components
│ ├── completionWatcher.ts
│ ├── logger.ts
│ ├── paster.ts
│ ├── typeFinder.ts
│ ├── wordCounter.ts
│ └── zotero.ts
├── main.ts
├── providers
│ └── macroDefinitions.ts
├── utils.ts
└── workshop
│ ├── LICENSE.txt
│ ├── finderutils.ts
│ ├── manager.ts
│ ├── pathutils.ts
│ └── utils.ts
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/lib/**/*.ts
2 | resources/**/*.js
3 | out
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | // "plugin:@typescript-eslint/recommended-requiring-type-checking"
10 | ],
11 | "globals": {
12 | "Atomics": "readonly",
13 | "SharedArrayBuffer": "readonly"
14 | },
15 | "parser": "@typescript-eslint/parser",
16 | "parserOptions": {
17 | "ecmaVersion": 2018,
18 | "project": "./tsconfig.json"
19 | },
20 | "rules": {
21 | "no-undef": "off",
22 | "no-unused-vars": "off",
23 | "no-constant-condition": "off",
24 | "@typescript-eslint/ban-ts-comment": "warn",
25 | "@typescript-eslint/prefer-interface": "off",
26 | "@typescript-eslint/explicit-function-return-type": "off",
27 | "@typescript-eslint/explicit-member-accessibility": "off",
28 | "@typescript-eslint/indent": "off",
29 | "@typescript-eslint/interface-name-prefix": "off",
30 | "@typescript-eslint/naming-convention": [
31 | "error",
32 | {
33 | "format": [
34 | "camelCase",
35 | "UPPER_CASE",
36 | "PascalCase"
37 | ],
38 | "leadingUnderscore": "allow",
39 | "selector": "default"
40 | }
41 | ],
42 | "@typescript-eslint/no-use-before-define": "off",
43 | "@typescript-eslint/type-annotation-spacing": [
44 | "error",
45 | {
46 | "before": false,
47 | "after": true,
48 | "overrides": {
49 | "arrow": {
50 | "before": true,
51 | "after": true
52 | }
53 | }
54 | }
55 | ],
56 | "@typescript-eslint/no-unused-vars": [
57 | "error",
58 | {
59 | "args": "none"
60 | }
61 | ],
62 | "prefer-arrow-callback": [
63 | "error",
64 | {
65 | "allowUnboundThis": false
66 | }
67 | ],
68 | "@typescript-eslint/no-parameter-properties": "off",
69 | "@typescript-eslint/no-explicit-any": "off",
70 | "@typescript-eslint/no-inferrable-types": "off",
71 | "@typescript-eslint/prefer-regexp-exec": "off",
72 | "curly": "error",
73 | "eol-last": "error",
74 | "no-caller": "error",
75 | "no-multiple-empty-lines": "error",
76 | "no-new-wrappers": "error",
77 | "no-eval": "error",
78 | "no-invalid-this": "error",
79 | "no-console": "off",
80 | "@typescript-eslint/no-require-imports": "error",
81 | "no-shadow": "off",
82 | "@typescript-eslint/no-shadow": [
83 | "error"
84 | ],
85 | "no-trailing-spaces": "error",
86 | "no-empty": [
87 | "error",
88 | {
89 | "allowEmptyCatch": true
90 | }
91 | ],
92 | "no-unused-expressions": "error",
93 | "no-var": "error",
94 | "object-shorthand": "error",
95 | "one-var": [
96 | "error",
97 | {
98 | "initialized": "never",
99 | "uninitialized": "never"
100 | }
101 | ],
102 | "prefer-const": "error",
103 | "quotes": [
104 | "error",
105 | "single",
106 | {
107 | "avoidEscape": true
108 | }
109 | ],
110 | "@typescript-eslint/member-delimiter-style": [
111 | "error",
112 | {
113 | "multiline": {
114 | "delimiter": "none",
115 | "requireLast": false
116 | },
117 | "singleline": {
118 | "delimiter": "comma",
119 | "requireLast": false
120 | }
121 | }
122 | ],
123 | "default-case": "error",
124 | "eqeqeq": [
125 | "error",
126 | "always"
127 | ],
128 | "space-before-function-paren": [
129 | "error",
130 | {
131 | "anonymous": "always",
132 | "named": "never",
133 | "asyncArrow": "always"
134 | }
135 | ],
136 | "func-call-spacing": [
137 | "error",
138 | "never"
139 | ],
140 | "no-multi-spaces": [
141 | "error",
142 | {
143 | "ignoreEOLComments": true
144 | }
145 | ],
146 | "@typescript-eslint/no-empty-function": "off"
147 | /*
148 | "align": [true, "parameters", "statements"],
149 | "no-angle-bracket-type-assertion": true,
150 | "no-default-export": true,
151 | "one-line": [
152 | true,
153 | "check-catch",
154 | "check-else",
155 | "check-finally",
156 | "check-open-brace",
157 | "check-whitespace"
158 | ],
159 | "typedef-whitespace": [
160 | true,
161 | {
162 | "call-signature": "onespace",
163 | "index-signature": "nospace",
164 | "parameter": "nospace",
165 | "property-declaration": "nospace",
166 | "variable-declaration": "nospace"
167 | },
168 | {
169 | "call-signature": "onespace",
170 | "index-signature": "onespace",
171 | "parameter": "onespace",
172 | "property-declaration": "onespace",
173 | "variable-declaration": "onespace"
174 | }
175 | ],
176 | "whitespace": [
177 | true,
178 | "check-branch",
179 | "check-decl",
180 | "check-operator",
181 | "check-separator",
182 | "check-type",
183 | "check-typecast"
184 | ] */
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
13 |
14 | ## Bug Report
15 |
16 | #### Disable all the other extensions except for LaTeX Workshop and LaTeX Utilities, and check that you still see this issue.
17 |
18 | You still see this issue?: **Yes/No**
19 |
20 | ### Describe the bug
21 |
22 | A clear and concise description of what the bug is.
23 |
24 | ### To Reproduce
25 |
26 | Steps to reproduce the behaviour:
27 |
28 | 1. Go to '...'
29 | 2. Click on '....'
30 | 3. See error
31 |
32 | ### Expected behaviour
33 |
34 | A clear and concise description of what you expected to happen.
35 |
36 | ### Logs
37 |
38 | Please paste the whole log messages here, not parts of ones. It is very important to identify problems. If you think the logs are unrelated, please say so.
39 |
40 |
41 | LaTeX Workshop Output
42 |
43 |
44 | ```
45 | logs here
46 | ```
47 |
48 |
49 |
50 |
51 | LaTeX Utilities Output
52 |
53 |
54 | ```
55 | logs here
56 | ```
57 |
58 |
59 |
60 |
61 | Developer Tools Console
62 |
63 |
64 | ```
65 | logs here
66 | ```
67 |
68 |
69 |
70 | ### Screenshots
71 |
72 | If applicable, add screenshots to help explain your problem.
73 |
74 | ### Desktop
75 |
76 | - OS: Windows 10 / MacOS / Linux
77 | - VS Code version: X.Y.Z
78 | - Extension version: X.Y.Z
79 |
80 | ### Additional context
81 |
82 | Add any other context about the problem here.
83 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Feature Request
11 |
12 | ### Is your feature request related to a problem? Please describe.
13 |
14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when...
15 |
16 | ### Describe the solution you'd like
17 |
18 | A clear and concise description of what you want to happen.
19 |
20 | ### Describe alternatives you've considered
21 |
22 | A clear and concise description of any alternative solutions or features you've considered.
23 |
24 | ### Additional context
25 |
26 | Add any other context or screenshots about the feature request here.
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | node_modules
3 | .vscode-test/
4 | *.vsix
5 | VERSION
6 | .vscode/*
7 | !.vscode/settings.json
8 | !.vscode/tasks.json
9 | !.vscode/launch.json
10 | !.vscode/extensions.json
11 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v3.2.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: end-of-file-fixer
9 | - id: check-yaml
10 | - id: check-added-large-files
11 | - repo: https://github.com/eslint/eslint
12 | rev: "v8.48.0" # Use the sha / tag you want to point at
13 | hooks:
14 | - id: eslint
15 | files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
16 | types: [file]
17 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "James-Yu.latex-workshop"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "runtimeExecutable": "${execPath}",
13 | "args": [
14 | "--extensionDevelopmentPath=${workspaceFolder}"
15 | ],
16 | "outFiles": [
17 | "${workspaceFolder}/out/**/*.js"
18 | ],
19 | "preLaunchTask": "npm: esbuild-watch"
20 | },
21 | {
22 | "name": "Extension Tests",
23 | "type": "extensionHost",
24 | "request": "launch",
25 | "runtimeExecutable": "${execPath}",
26 | "args": [
27 | "--extensionDevelopmentPath=${workspaceFolder}",
28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
29 | ],
30 | "outFiles": [
31 | "${workspaceFolder}/out/test/**/*.js"
32 | ],
33 | "preLaunchTask": "npm: esbuild-watch"
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "out": true,
4 | "node_modules": true
5 | },
6 | "search.exclude": {
7 | "out": true
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off",
11 | // "eslint.workingDirectories": [],
12 | "eslint.provideLintTask": true,
13 | "eslint.validate": ["typescript"]
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "esbuild-watch",
9 | "problemMatcher": "$esbuild-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode-test/**
2 | out/test/**
3 | test/**
4 | **/*.map
5 | .gitignore
6 | tsconfig.json
7 | tsconfig.eslint.json
8 | demo_media/**
9 | .eslintcache
10 | .eslintignore
11 | *.vsix
12 | .vscode
13 | node_modules
14 | src/
15 | tsconfig.json
16 | webpack.config.js
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [0.4.14] - 2024-03-25
4 | ### Improved
5 | - [#356](https://github.com/tecosaur/LaTeX-Utilities/issues/356) Allow setting more language for live reformat
6 |
7 | ## [0.4.13] - 2024-02-13
8 | ### Fixed
9 | - [#397](https://github.com/tecosaur/LaTeX-Utilities/issues/397) Fix configuration name
10 |
11 |
12 | ## [0.4.12] - 2024-02-11
13 | ### Fixed
14 | - [#387](https://github.com/tecosaur/LaTeX-Utilities/issues/387) Add support for R Sweave and Julia Sweave Dcouemnts
15 | - [#389](https://github.com/tecosaur/LaTeX-Utilities/issues/389) Fixes typo
16 | - [#391](https://github.com/tecosaur/LaTeX-Utilities/issues/391) Updated readme to specify how to install texcount
17 | - [#394](https://github.com/tecosaur/LaTeX-Utilities/issues/394) migrate live snippets template to VSCode's internal config
18 | - [#396](https://github.com/tecosaur/LaTeX-Utilities/issues/396) support for searching authors when using vscode as zotero search method
19 |
20 | ## [0.4.11] - 2023-4-3
21 | ### Fixed
22 | - [#383](https://github.com/tecosaur/LaTeX-Utilities/issues/383) Fixes an issue that caused formatted pasting can't paste image on linux.
23 |
24 | ## [0.4.10] - 2023-2-22
25 | ### Added
26 | - Add an option `imagePathOverride` to override the image path in formatted pasting.
27 |
28 | ## [0.4.9] - 2022-12-16
29 | ### Added
30 | - Config to use docker to run texcount
31 |
32 | ## [0.4.8] - 2022-10-16
33 | ### Added
34 | - Support character count
35 |
36 | ## [0.4.7] - 2022-09-25
37 | ### Added
38 | - Support pasting image file in formatted paste
39 |
40 | ## [0.4.6] - 2022-08-06
41 | ### Fixed
42 | - Warn users about JSON errors in snippet file.
43 |
44 | ## [0.4.5] - 2022-07-31
45 | ### Fixed
46 | - Fix bugs introduced in 0.4.3
47 |
48 | ## [0.4.4] - 2022-07-31
49 | ### Improved
50 | - Prompt user when TexDef is not installed
51 |
52 | ## [0.4.3] - 2022-07-30
53 | ### Improved
54 | - Add error telemetry
55 |
56 | ## [0.4.2] - 2022-07-29
57 |
58 | ### Fixed
59 | - Wrong path introduced by esbuild.
60 |
61 | ## [0.4.1] - 2022-07-17
62 |
63 | ### Improved
64 | - Use esbuild to bundle the package to improve performance.
65 |
66 | ## [0.4.0] - 2022-07-06
67 | New maintainer!
68 | - Removed dependency on LaTeX-Workshop.
69 | - Removed support for TiKZ live preview temporarily.
70 | - Fixed pasting image inside WSL.
71 | - Move from NPM to yarn.
72 |
73 | ## [0.3.7] — 2020-05-22
74 |
75 | Announce that extension is no longer maintained.
76 |
77 | ## [0.3.6] — 2020-02-01
78 |
79 | ### Fixed
80 |
81 | - API changed in `got` breaking Zotero citation search.
82 |
83 | ## [0.3.5] — 2020-02-01
84 |
85 | ### Added
86 |
87 | - Command to reset user live snippets file
88 | - Command to compare user live snippets file to default
89 | - Basic support for bulleted lists in formatted paste
90 | - Notification on extension update
91 | - Notify users when save/close a user live snippets file same as the extension default
92 |
93 | ### Improved
94 |
95 | - Tweak default live snippets (yet again, again)
96 | - Account for indent when formatted-pasting text
97 | - Try to avoid plaintext 🡒 LaTeX formatting already LaTeX formatted pastes
98 | - Determination of maths/text type at cursor gets it wrong a bit less
99 |
100 | ### Fixed
101 |
102 | - Formatted pasting a single line of text with cursor at non-zero column resulted in text being cut out
103 | - Account for inconsistency in `texcount` output
104 | - Add `\pgfplotsset{table/search path=...` to TikZ Preview to (hopefully) fix local file references
105 |
106 | ## [0.3.4] — 2019-11-02
107 |
108 | ### Added
109 |
110 | - TikZ Preview `timeout` setting, to ignore the first change made after a certain period
111 | - JSON validation schema for live snippets file
112 |
113 | ### Improved
114 |
115 | - TikZ Preview now uses relevant lines prior to the `tikzpicture`
116 | - Make TikZ Preview work with any environment which matches `\w*tikz\w*`
117 | - Live snippets now treats comments inside a math environment as "text"
118 | - Lots of excess logging with live snippets removed
119 | - More tweaks to live snippets
120 | - Make formatted paste line shaping account for current column
121 |
122 | ### Fixed
123 |
124 | - TikZ Preview delay was dodgy
125 | - TeX Count 'results' line was incorrectly isolated
126 |
127 | ## [0.3.3] — 2019-10-19
128 |
129 | ### Added
130 |
131 | - Setting to make formatted paste the default (`ctrl`+`v`) paste
132 | - New setting for custom delimiter for formatted paste to try with tables
133 | - Telemetry to try to help direct development effort
134 |
135 | ### Improved
136 |
137 | - More tweaks to live snippets (`sr`, `cb` and superscripts)
138 | - Formatted paste of tables now 'just works' with anything which is tab, comma, or `|` delimited,
139 | i.e. spreadsheets, csv, markdown
140 | - Formatted paste of text now joins hyphenated words
141 |
142 | ## [0.3.2] — 2019-10-16
143 |
144 | ### Added
145 |
146 | - Customisable wordcount status
147 | - Formatted paste now shapes text to (configurable) line length
148 |
149 | ### Improved
150 |
151 | - Live snippets now do so more accents, and spaces
152 |
153 | ### Fixed
154 |
155 | - Live snippets can no longer do dodgy stuff when the replacement is the same length as the original
156 |
157 | ## [0.3.1] — 2019-09-27
158 |
159 | ### Improved
160 |
161 | - Live snippets are now a bit better again (see #42)
162 |
163 | ### Fixed
164 |
165 | - Some formatted-paste text replacements
166 | - LiveSnippets now recognise placeholder tabstops
167 |
168 | ## [0.3.0] — 2019-09-06
169 |
170 | ### Added
171 |
172 | - Add Zotero integration with BBT (Better BibTeX)
173 |
174 | ### Improved
175 |
176 | - Change placeholder style in snippets from `$.` to `$$`, because it seems cleaner.
177 |
178 | ### Fixed
179 |
180 | - Some of the default live snippets were a bit dodgy
181 | - Fixed #17 (cursor moving backwards too far with some live snippets)
182 | - TikZ Preview no longer grabs lines after `\begin{document}`
183 | - Fix up some of the text replacements done by formatted paste
184 |
185 | ## [0.2.2] — 2019-08-23
186 |
187 | ### Added
188 |
189 | - Toggle for the define command with `texdef` feature
190 |
191 | ## [0.2.1] — 2019-08-19
192 |
193 | ### Fixed
194 |
195 | - Demo images on marketplace page
196 |
197 | ## [0.2.0] — 2019-08-19
198 |
199 | ### Added
200 |
201 | - Word Count
202 | - TikZ Preview
203 | - Adds a code lense above `\begin{tikzpicture}` that allows for live previewing
204 | - Command Definitions
205 |
206 | ### Improved
207 |
208 | - Full formatted paste features, `ctrl`+`shift`+`v`
209 |
210 | - Reformats some Unicode text for LaTeX
211 | - Table cells are turned into a `tabular`
212 | - Pasting the location of a `.csv` file pastes a table with the contents
213 | - Paste an image from the clipboard (as in 0.1.0)
214 | - Formatted paste the path to a csv (adds tabular) or image file (links to file)
215 |
216 | - Live Snippets: added more mathematics environments to environment (text/maths) detection code
217 | - Live Snippets: may now be _marginally_ faster due to some behind-the-scenes reworking
218 |
219 | ### Fixed
220 |
221 | - Live Snippets: Big! bug with environment (text/maths) detection code
222 |
223 | ## [0.1.0] — 2019-07-31
224 |
225 | ### Added
226 |
227 | - Image Pasting (via `ctrl`+`shift`+`v` and "Paste an Image File")
228 | - Live Snippets (auto-activating, with regex)
229 |
230 | [unreleased]: https://github.com/tecosaur/latex-utilities/compare/v0.4.6...HEAD
231 | [0.4.6]: https://github.com/tecosaur/latex-utilities/compare/v0.4.5...v0.4.6
232 | [0.4.5]: https://github.com/tecosaur/latex-utilities/compare/v0.4.4...v0.4.5
233 | [0.4.4]: https://github.com/tecosaur/latex-utilities/compare/v0.4.3...v0.4.4
234 | [0.4.3]: https://github.com/tecosaur/latex-utilities/compare/v0.4.2...v0.4.3
235 | [0.4.2]: https://github.com/tecosaur/latex-utilities/compare/v0.4.1...v0.4.2
236 | [0.4.1]: https://github.com/tecosaur/latex-utilities/compare/v0.4.0...v0.4.1
237 | [0.4.0]: https://github.com/tecosaur/latex-utilities/compare/v0.3.7...v0.4.0
238 | [0.3.7]: https://github.com/tecosaur/latex-utilities/compare/v0.3.5...v0.3.7
239 | [0.3.6]: https://github.com/tecosaur/latex-utilities/compare/v0.3.5...v0.3.6
240 | [0.3.5]: https://github.com/tecosaur/latex-utilities/compare/v0.3.4...v0.3.5
241 | [0.3.4]: https://github.com/tecosaur/latex-utilities/compare/v0.3.3...v0.3.4
242 | [0.3.3]: https://github.com/tecosaur/latex-utilities/compare/v0.3.2...v0.3.3
243 | [0.3.2]: https://github.com/tecosaur/latex-utilities/compare/v0.3.1...v0.3.2
244 | [0.3.1]: https://github.com/tecosaur/latex-utilities/compare/v0.3.0...v0.3.1
245 | [0.3.0]: https://github.com/tecosaur/latex-utilities/compare/v0.2.2...v0.3.0
246 | [0.2.2]: https://github.com/tecosaur/latex-utilities/compare/v0.2.1...v0.2.2
247 | [0.2.1]: https://github.com/tecosaur/latex-utilities/compare/v0.2.0...v0.2.1
248 | [0.2.0]: https://github.com/tecosaur/latex-utilities/compare/v0.1.0...v0.2.0
249 | [0.1.0]: https://github.com/tecosaur/latex-utilities/compare/bc5bf4f...v0.1.0
250 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | ## 1. Install
4 |
5 | `git clone https://github.com/tecosaur/LaTeX-Utilities.git`,
6 |
7 | ## 2. Initialise
8 |
9 | Open the `LaTeX-Utilities` directory in vscode. Open up the command line and run `yarn`
10 |
11 | ## 3. Make a new branch and add commits
12 |
13 | If you can make a PR, that's fantastic. You just have to be ok with your code going under MIT.
14 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 tecosaur
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | LaTeX Utilities
29 |
30 | An add-on to the vscode extension [LaTeX Workshop](https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop) that provides some fancy features that are less vital to the basic experience editing a LaTeX document, but can be rather nice to have.
31 | The feature should continue to expand at a gradually decreasing rate.
32 |
33 | Got an idea? Make a PR!
34 |
35 |
36 |
37 | ## Features
38 |
39 | - Formatted Pastes
40 | - Unicode characters 🡒 LaTeX characters (e.g. `“is this… a test”` 🡒 ` ``is this\ldots a test'' `)
41 | - Paste table cells (from spreadsheet programs or similar) 🡒 tabular
42 | - Paste images, customisable template
43 | - Paste location of CSVs/images to have them included
44 | - Live Snippets (auto-activating, with regex) [see here](https://github.com/tecosaur/LaTeX-Utilities/wiki/Live-Snippets) for documentation
45 | - Word count in status bar
46 | - Zotero citation management
47 |
48 | ## Documentation
49 |
50 | - See the [wiki](https://github.com/tecosaur/LaTeX-Utilities/wiki)
51 |
52 | ## Requirements
53 |
54 | - A LaTeX installation in your path
55 | - The [`texcount`](https://app.uio.no/ifi/texcount/) script (only necessary for the word-count function). Configure using the `latex-utilities.countWord.path` and `latex-utilities.countWord.args` settings.
56 | - Alternatively, install the `texcount` package from your TeX package manager (e.g., `tlmgr`) if it doesn't come with your TeX distribution.
57 | - Zotero with the [Better BibTeX extension](https://retorque.re/zotero-better-bibtex/) (only necessary for Zotero
58 | functions).
59 |
60 | ## Demos
61 |
62 | ### Formatted Paste (image)
63 |
64 |
65 |
66 | ### Live Snippets
67 |
68 |
69 |
70 | ### Zotero Integration
71 |
72 |
73 |
74 |
75 |
76 |
77 | ---
78 |
79 | ## Telemetry
80 |
81 | ### Why
82 |
83 | As a bunch of fancy, but non-essential features, it can be hard to know what features users actually derive value from.
84 | In adding telemetry to this extension I hope to get an idea of this, and inform future development efforts.
85 | It should also be possible to report errors in the background, and so I also hope this extension will be more stable as a result.
86 |
87 | At the moment I'm just logging when one of the main features is used.
88 |
89 | **TLDR; I want to get around the 1% rule**
90 |
91 | ### I hate telemetry, go away!
92 |
93 | You probably have disabled vscode's `telemetry.enableTelemetry` then, in which case no telemetry is done.
94 |
--------------------------------------------------------------------------------
/demo-media/formattedPasteImage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/demo-media/formattedPasteImage.gif
--------------------------------------------------------------------------------
/demo-media/icon-wirefrane-minimal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
83 |
--------------------------------------------------------------------------------
/demo-media/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
102 |
--------------------------------------------------------------------------------
/demo-media/liveSnippets.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/demo-media/liveSnippets.gif
--------------------------------------------------------------------------------
/demo-media/preview-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/demo-media/preview-card.png
--------------------------------------------------------------------------------
/demo-media/tikz-preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/demo-media/tikz-preview.gif
--------------------------------------------------------------------------------
/demo-media/zotero-integration.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/demo-media/zotero-integration.gif
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tecosaur/LaTeX-Utilities/a3265cc6827bd383eac4945ea8b914c0b4896fa0/icon.png
--------------------------------------------------------------------------------
/resources/liveSnippetSchema.json:
--------------------------------------------------------------------------------
1 | {
2 | "definitions": {},
3 | "$schema": "http://json-schema.org/draft-07/schema#",
4 | "type": "array",
5 | "title": "Set of test strings and replacements",
6 | "items": {
7 | "$id": "#/items",
8 | "type": "object",
9 | "title": "Completion Item",
10 | "required": ["prefix", "body"],
11 | "properties": {
12 | "prefix": {
13 | "$id": "#/items/properties/prefix",
14 | "type": "string",
15 | "title": "Escaped RegEx test string",
16 | "default": "$",
17 | "examples": ["([A-Za-z}\\)\\]])(\\d)$"],
18 | "pattern": "\\$$"
19 | },
20 | "body": {
21 | "$id": "#/items/properties/body",
22 | "type": "string",
23 | "title": "Replacement text",
24 | "description": "Use $1 for regex groups, and $$1 for tabstops",
25 | "default": "",
26 | "examples": ["$1_$2"]
27 | },
28 | "mode": {
29 | "$id": "#/items/properties/mode",
30 | "type": "string",
31 | "title": "Specific LaTeX mode. 'any' by default",
32 | "enum": ["maths", "text", "any"],
33 | "default": "any",
34 | "examples": ["maths"]
35 | },
36 | "triggerWhenComplete": {
37 | "$id": "#/items/properties/triggerWhenComplete",
38 | "type": "boolean",
39 | "title": "Insta-complete. Off by default",
40 | "default": false,
41 | "examples": [true]
42 | },
43 | "description": {
44 | "$id": "#/items/properties/description",
45 | "type": "string",
46 | "title": "Just a nice description to remember this by",
47 | "default": "",
48 | "examples": ["auto subscript"]
49 | },
50 | "priority": {
51 | "$id": "#/items/properties/priority",
52 | "type": "number",
53 | "title": "Higher priority snippets get considered first",
54 | "default": 0,
55 | "examples": [1]
56 | },
57 | "noPlaceholders": {
58 | "$id": "#/items/properties/noPlaceholders",
59 | "type": "boolean",
60 | "title": "For rare cases explicitly specify whether the item has placeholders.",
61 | "description": "This is intelligently worked out by default"
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/resources/liveSnippets.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "prefix": "([A-Za-z}\\)\\]])(\\d)$",
4 | "body": "$1_$2",
5 | "mode": "maths",
6 | "triggerWhenComplete": true,
7 | "description": "auto subscript"
8 | },
9 | {
10 | "prefix": "([A-Za-z}\\)\\]]) ?_(\\d\\d)$",
11 | "body": "$1_{$2}",
12 | "mode": "maths",
13 | "triggerWhenComplete": true,
14 | "description": "auto escape subscript"
15 | },
16 | {
17 | "prefix": "(\\S) ([\\^_])$",
18 | "body": "$1$2",
19 | "mode": "maths",
20 | "triggerWhenComplete": true,
21 | "description": "remove extraneous sub/superscript space",
22 | "priority": 2
23 | },
24 | {
25 | "prefix": "([A-Za-z}\\)\\]]) ?\\^ ?(\\d\\d|[\\+\\-] ?(?:\\d|[A-Za-z]|\\\\\\w+))$",
26 | "body": "$1^{$2}",
27 | "mode": "maths",
28 | "triggerWhenComplete": true,
29 | "description": "auto escape superscript",
30 | "priority": 2
31 | },
32 | {
33 | "prefix": "([^ &\\\\\\+\\-=<>\\|!~@])([\\+\\-=<>])$",
34 | "body": "$1 $2",
35 | "mode": "maths",
36 | "priority": -1,
37 | "description": "whitespace before operators",
38 | "triggerWhenComplete": true
39 | },
40 | {
41 | "prefix": "([\\+\\-=<>])([^ &\\\\\\+\\-=<>\\|!~])$",
42 | "body": "$1 $2",
43 | "mode": "maths",
44 | "priority": -1,
45 | "description": "whitespace after operators",
46 | "triggerWhenComplete": true
47 | },
48 | {
49 | "prefix": "\\.\\.\\.$",
50 | "body": "\\dots ",
51 | "mode": "maths",
52 | "description": "⋯",
53 | "triggerWhenComplete": true
54 | },
55 | {
56 | "prefix": "=>$",
57 | "body": "\\implies ",
58 | "mode": "maths",
59 | "description": "⇒",
60 | "triggerWhenComplete": true
61 | },
62 | {
63 | "prefix": "=<$",
64 | "body": "\\impliedby ",
65 | "mode": "maths",
66 | "description": "implied by",
67 | "triggerWhenComplete": true
68 | },
69 | {
70 | "prefix": "//$",
71 | "body": "\\frac{$$1}{$$2} ",
72 | "mode": "maths",
73 | "description": "fraction (empty)",
74 | "triggerWhenComplete": true
75 | },
76 | {
77 | "prefix": "(([\\d\\.]+)|([\\d\\.]*)(\\\\)?([A-Za-z]+)((\\^|_)(\\{\\d+\\}|\\d|[A-Za-z]|\\\\\\w+))*!?)\\/$",
78 | "body": "\\frac{$1}{$$1}$$0",
79 | "mode": "maths",
80 | "description": "fraction (from regex)",
81 | "triggerWhenComplete": true
82 | },
83 | {
84 | "prefix": "([\\)\\]}]) ?/$",
85 | "body": "SPECIAL_ACTION_FRACTION",
86 | "mode": "maths",
87 | "description": "fraction (parsed)",
88 | "triggerWhenComplete": true,
89 | "noPlaceholders": false
90 | },
91 | {
92 | "prefix": "sympy$",
93 | "body": "sympy $$1 sympy",
94 | "mode": "maths",
95 | "description": "sympy block",
96 | "triggerWhenComplete": false
97 | },
98 | {
99 | "prefix": "sympy.+$",
100 | "body": "SPECIAL_ACTION_BREAK",
101 | "mode": "maths",
102 | "triggerWhenComplete": true,
103 | "priority": 2
104 | },
105 | {
106 | "prefix": "sympy ?(.+?) ?sympy ?$",
107 | "body": "SPECIAL_ACTION_SYMPY",
108 | "mode": "maths",
109 | "priority": 3,
110 | "description": "sympy",
111 | "triggerWhenComplete": true
112 | },
113 | {
114 | "prefix": "(^|[^\\\\])\\biff$",
115 | "body": "$1\\iff ",
116 | "mode": "maths",
117 | "description": "⇔",
118 | "triggerWhenComplete": true
119 | },
120 | {
121 | "prefix": "(^|[^\\\\])\\binn$",
122 | "body": "$1\\in ",
123 | "mode": "maths",
124 | "description": "in",
125 | "triggerWhenComplete": true
126 | },
127 | {
128 | "prefix": "(^|[^\\\\])\\bnotin$",
129 | "body": "$1\\not\\in ",
130 | "mode": "maths",
131 | "description": "∈",
132 | "triggerWhenComplete": true
133 | },
134 | {
135 | "prefix": " ?!=$",
136 | "body": " \\neq ",
137 | "mode": "maths",
138 | "description": "neq",
139 | "triggerWhenComplete": true
140 | },
141 | {
142 | "prefix": "==$",
143 | "body": "&= ",
144 | "mode": "maths",
145 | "description": "aligned equal",
146 | "priority": 1,
147 | "triggerWhenComplete": true
148 | },
149 | {
150 | "prefix": " ?~=$",
151 | "body": " \\approx ",
152 | "mode": "maths",
153 | "description": "≈",
154 | "triggerWhenComplete": true
155 | },
156 | {
157 | "prefix": " ?~~$",
158 | "body": " \\sim ",
159 | "mode": "maths",
160 | "description": "∼",
161 | "triggerWhenComplete": true
162 | },
163 | {
164 | "prefix": " ?>=$",
165 | "body": " \\geq ",
166 | "mode": "maths",
167 | "description": "≥",
168 | "triggerWhenComplete": true
169 | },
170 | {
171 | "prefix": " ?<=$",
172 | "body": " \\leq ",
173 | "mode": "maths",
174 | "description": "≤",
175 | "triggerWhenComplete": true
176 | },
177 | {
178 | "prefix": " ?>>$",
179 | "body": " \\gg ",
180 | "mode": "maths",
181 | "description": "≫",
182 | "triggerWhenComplete": true
183 | },
184 | {
185 | "prefix": " ?<<$",
186 | "body": " \\ll ",
187 | "mode": "maths",
188 | "description": "≪",
189 | "triggerWhenComplete": true
190 | },
191 | {
192 | "prefix": " ?xx$",
193 | "body": " \\times ",
194 | "mode": "maths",
195 | "description": "×",
196 | "triggerWhenComplete": true
197 | },
198 | {
199 | "prefix": " ?\\*\\*$",
200 | "body": " \\cdot ",
201 | "mode": "maths",
202 | "description": "⋅",
203 | "triggerWhenComplete": true
204 | },
205 | {
206 | "prefix": "(^|[^\\\\]\\b|[ ,\\)\\]\\}]\\w*)(to|->)$",
207 | "body": "$1\\to ",
208 | "mode": "maths",
209 | "description": "→",
210 | "triggerWhenComplete": true
211 | },
212 | {
213 | "prefix": " ?(?:\\|->|!>)$",
214 | "body": " \\mapsto ",
215 | "mode": "maths",
216 | "description": "↦",
217 | "priority": 1.1,
218 | "triggerWhenComplete": true
219 | },
220 | {
221 | "prefix": "(^|[^\\\\])a(?:rc)?(sin|cos|tan|cot|csc|sec)$",
222 | "body": "$1\\arc$2 ",
223 | "mode": "maths",
224 | "description": "arc(trig)",
225 | "triggerWhenComplete": true
226 | },
227 | {
228 | "prefix": "(^|[^\\\\])(sin|cos|tan|cot|csc|sec|min|max|log|exp)$",
229 | "body": "$1\\$2 ",
230 | "mode": "maths",
231 | "description": "un-backslashed operator",
232 | "triggerWhenComplete": true
233 | },
234 | {
235 | "prefix": "(^|[^\\\\])(pi)$",
236 | "body": "$1\\$2",
237 | "mode": "maths",
238 | "description": "pi",
239 | "triggerWhenComplete": true
240 | },
241 | {
242 | "prefix": "((?:\\b|\\\\)\\w{1,7})(,\\.|\\.,)$",
243 | "body": "\\vec{$1}",
244 | "mode": "maths",
245 | "description": "vector",
246 | "triggerWhenComplete": true
247 | },
248 | {
249 | "prefix": "(\\\\?[\\w\\^]{1,7})~ $",
250 | "body": "\\tilde{$1}",
251 | "mode": "maths",
252 | "description": "tilde",
253 | "triggerWhenComplete": true
254 | },
255 | {
256 | "prefix": "(\\\\?[\\w\\^]{1,7})\\. $",
257 | "body": "\\dot{$1}",
258 | "mode": "maths",
259 | "description": "dot",
260 | "triggerWhenComplete": true
261 | },
262 | {
263 | "prefix": "(\\\\?[\\w\\^]{1,7})\\.\\. $",
264 | "body": "\\ddot{$1}",
265 | "mode": "maths",
266 | "description": "ddot",
267 | "triggerWhenComplete": true
268 | },
269 | {
270 | "prefix": "\\bbar$",
271 | "body": "\\overline{$$1}",
272 | "mode": "maths",
273 | "description": "overline",
274 | "triggerWhenComplete": true
275 | },
276 | {
277 | "prefix": "\\b(\\\\?[\\w\\^{}]{1,3})bar$",
278 | "body": "\\overline{$1}",
279 | "mode": "maths",
280 | "description": "overline",
281 | "triggerWhenComplete": true
282 | },
283 | {
284 | "prefix": "(^|[^\\\\])\\bhat$",
285 | "body": "$1\\hat{$$1}",
286 | "mode": "maths",
287 | "description": "hat",
288 | "triggerWhenComplete": true
289 | },
290 | {
291 | "prefix": "\\b([\\w\\^{}])hat$",
292 | "body": "\\hat{$1}",
293 | "mode": "maths",
294 | "description": "hat",
295 | "triggerWhenComplete": true
296 | },
297 | {
298 | "prefix": "\\\\\\)(\\w)$",
299 | "body": "\\) $1",
300 | "mode": "any",
301 | "description": "space after inline maths",
302 | "triggerWhenComplete": true
303 | },
304 | {
305 | "prefix": "\\\\\\\\\\\\$",
306 | "body": "\\setminus ",
307 | "mode": "maths",
308 | "description": "∖ (setminus)",
309 | "triggerWhenComplete": true
310 | },
311 | {
312 | "prefix": "\\bpmat$",
313 | "body": "\\begin{pmatrix} $$1 \\end{pmatrix} ",
314 | "mode": "maths",
315 | "description": "pmatrix",
316 | "triggerWhenComplete": true
317 | },
318 | {
319 | "prefix": "\\bbmat$",
320 | "body": "\\begin{bmatrix} $$1 \\end{bmatrix} ",
321 | "mode": "maths",
322 | "description": "bmatrix",
323 | "triggerWhenComplete": true
324 | },
325 | {
326 | "prefix": "\\bpart$",
327 | "body": "\\frac{\\partial $${1:V}}{\\partial $${2:x}} ",
328 | "mode": "maths",
329 | "description": "partial derivative",
330 | "triggerWhenComplete": true
331 | },
332 | {
333 | "prefix": "\\bsq$",
334 | "body": "\\sqrt{$$1}",
335 | "mode": "maths",
336 | "description": "√",
337 | "triggerWhenComplete": true
338 | },
339 | {
340 | "prefix": " ?sr$",
341 | "body": "^2",
342 | "mode": "maths",
343 | "description": "²",
344 | "triggerWhenComplete": true
345 | },
346 | {
347 | "prefix": " ?cb$",
348 | "body": "^3",
349 | "mode": "maths",
350 | "description": "³",
351 | "triggerWhenComplete": true
352 | },
353 | {
354 | "prefix": "\\bEE$",
355 | "body": "\\exists ",
356 | "mode": "maths",
357 | "description": "∃",
358 | "triggerWhenComplete": true
359 | },
360 | {
361 | "prefix": "\\bAA$",
362 | "body": "\\forall ",
363 | "mode": "maths",
364 | "description": "∀",
365 | "triggerWhenComplete": true
366 | },
367 | {
368 | "prefix": "\\b([A-Za-z])([A-Za-z])\\2$",
369 | "body": "$1_$2",
370 | "mode": "maths",
371 | "description": "subscript letter",
372 | "triggerWhenComplete": true
373 | },
374 | {
375 | "prefix": "\\b([A-Za-z])([A-Za-z])\\2?p1$",
376 | "body": "$1_{$2+1}",
377 | "mode": "maths",
378 | "description": "subscript letter + 1",
379 | "priority": 2,
380 | "triggerWhenComplete": true
381 | },
382 | {
383 | "prefix": "\\bdint$",
384 | "body": "\\int_{$${1:-\\infty}}^{$${2:\\infty}} ",
385 | "mode": "maths",
386 | "description": "∫ₐᵇ",
387 | "triggerWhenComplete": true
388 | },
389 | {
390 | "prefix": "([^ \\\\]) $",
391 | "body": "$1\\, ",
392 | "mode": "maths",
393 | "description": "add maths whitespace \\,",
394 | "priority": -1,
395 | "triggerWhenComplete": true
396 | },
397 | {
398 | "prefix": "([^ \\\\])\\\\, {2,4}$",
399 | "body": "$1\\: ",
400 | "mode": "maths",
401 | "description": "add maths whitespace \\:",
402 | "priority": 0.1,
403 | "triggerWhenComplete": true
404 | },
405 | {
406 | "prefix": "([^ \\\\])\\\\: {2,4}$",
407 | "body": "$1\\; ",
408 | "mode": "maths",
409 | "description": "add maths whitespace \\;",
410 | "priority": 0.2,
411 | "triggerWhenComplete": true
412 | },
413 | {
414 | "prefix": "([^ \\\\])\\\\; {2,4}$",
415 | "body": "$1\\ ",
416 | "mode": "maths",
417 | "description": "add maths whitespace \\ ",
418 | "priority": 0.3,
419 | "triggerWhenComplete": true
420 | },
421 | {
422 | "prefix": "([^ \\\\])\\\\ {2,4}$",
423 | "body": "$1\\quad ",
424 | "mode": "maths",
425 | "description": "add maths whitespace quad",
426 | "priority": 0.4,
427 | "triggerWhenComplete": true
428 | },
429 | {
430 | "prefix": "([^ \\\\])\\\\quad {2,4}$",
431 | "body": "$1\\qquad ",
432 | "mode": "maths",
433 | "description": "add maths whitespace qquad",
434 | "priority": 0.5,
435 | "triggerWhenComplete": true
436 | },
437 | {
438 | "prefix": "\\bset$",
439 | "body": "\\\\{$$1\\\\} ",
440 | "mode": "maths",
441 | "description": "set {}",
442 | "triggerWhenComplete": true
443 | },
444 | {
445 | "prefix": " ?\\|\\|$",
446 | "body": " \\mid ",
447 | "mode": "maths",
448 | "description": "∣",
449 | "triggerWhenComplete": true
450 | },
451 | {
452 | "prefix": "< ?>$",
453 | "body": "\\diamond ",
454 | "mode": "maths",
455 | "description": "⋄",
456 | "triggerWhenComplete": true
457 | },
458 | {
459 | "prefix": "\\bcase$",
460 | "body": "\\begin{cases} $$1 \\end{cases} ",
461 | "mode": "maths",
462 | "description": "cases",
463 | "triggerWhenComplete": true
464 | },
465 | {
466 | "prefix": "(^|[^\\\\])\\bst$",
467 | "body": "$1\\text{s.t.} ",
468 | "mode": "maths",
469 | "description": "such that",
470 | "triggerWhenComplete": true
471 | },
472 | {
473 | "prefix": "\\+ ?-$",
474 | "body": "\\pm ",
475 | "mode": "maths",
476 | "description": "±",
477 | "priority": 1,
478 | "triggerWhenComplete": true
479 | },
480 | {
481 | "prefix": "- ?\\+$",
482 | "body": "\\mp ",
483 | "mode": "maths",
484 | "description": "∓",
485 | "priority": 1,
486 | "triggerWhenComplete": true
487 | },
488 | {
489 | "prefix": "(?:([A-Za-z0-9]|\\\\\\w{,7})|\\(([^\\)]+)\\))C(?:([A-Za-z0-9]|\\\\\\w{,7})|\\(([^\\)]+)\\))$",
490 | "body": "\\binom{$1$2}{$3$4}",
491 | "mode": "maths",
492 | "priority": 2,
493 | "description": "binomial",
494 | "triggerWhenComplete": true
495 | }
496 | ]
497 |
--------------------------------------------------------------------------------
/scripts/countword-linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | docker run -i --rm -w "$(pwd)" -v "$(pwd):$(pwd)" $LATEXWORKSHOP_DOCKER_LATEX texcount "$@"
3 |
--------------------------------------------------------------------------------
/scripts/countword-win.bat:
--------------------------------------------------------------------------------
1 | docker run -i --rm -w /data -v "%cd%:/data" %LATEXWORKSHOP_DOCKER_LATEX% texcount %*
2 |
--------------------------------------------------------------------------------
/scripts/saveclipimg-linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SOURCED FROM https://github.com/mushanshitiancai/vscode-paste-image/
3 |
4 | # require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212)
5 | command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; }
6 |
7 | # write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file)
8 | if
9 | xclip -selection clipboard -target image/png -o >/dev/null 2>&1
10 | then
11 | xclip -selection clipboard -target image/png -o >$1 2>/dev/null
12 | echo $1
13 | else
14 | echo "no image"
15 | fi
16 |
--------------------------------------------------------------------------------
/scripts/saveclipimg-mac.applescript:
--------------------------------------------------------------------------------
1 | -- SOURCED FROM https://github.com/mushanshitiancai/vscode-paste-image/
2 |
3 | property fileTypes : {{«class PNGf», ".png"}}
4 |
5 | on run argv
6 | if argv is {} then
7 | return ""
8 | end if
9 |
10 | set imagePath to (item 1 of argv)
11 | set theType to getType()
12 |
13 | if theType is not missing value then
14 | try
15 | set myFile to (open for access imagePath with write permission)
16 | set eof myFile to 0
17 | write (the clipboard as (first item of theType)) to myFile
18 | close access myFile
19 | return (POSIX path of imagePath)
20 | on error
21 | try
22 | close access myFile
23 | end try
24 | return ""
25 | end try
26 | else
27 | return "no image"
28 | end if
29 | end run
30 |
31 | on getType()
32 | repeat with aType in fileTypes
33 | repeat with theInfo in (clipboard info)
34 | if (first item of theInfo) is equal to (first item of aType) then return aType
35 | end repeat
36 | end repeat
37 | return missing value
38 | end getType
39 |
--------------------------------------------------------------------------------
/scripts/saveclipimg-pc.ps1:
--------------------------------------------------------------------------------
1 | # SOURCED FROM https://github.com/mushanshitiancai/vscode-paste-image/
2 |
3 | param($imagePath)
4 |
5 | # Adapted from https://github.com/octan3/img-clipboard-dump/blob/master/dump-clipboard-png.ps1
6 |
7 | Add-Type -Assembly PresentationCore
8 |
9 | if ($PSVersionTable.PSVersion.Major -ge 5 -and $PSVersionTable.PSVersion.Major -ge 1)
10 | {
11 | $file = Get-Clipboard -Format FileDropList
12 | if ($file -ne $null) {
13 | $img = new-object System.Drawing.Bitmap($file[0].Fullname)
14 | } else {
15 | $img = Get-Clipboard -Format Image
16 | }
17 |
18 | if ($img -eq $null) {
19 | "no image"
20 | Exit 1
21 | }
22 |
23 | if (-not $imagePath) {
24 | "no image"
25 | Exit 1
26 | }
27 |
28 | $img.save($imagePath)
29 | }
30 | else
31 | {
32 | $img = [Windows.Clipboard]::GetImage()
33 | if ($img -eq $null) {
34 | "no image"
35 | Exit 1
36 | }
37 |
38 | if (-not $imagePath) {
39 | "no image"
40 | Exit 1
41 | }
42 |
43 | $fcb = new-object Windows.Media.Imaging.FormatConvertedBitmap($img, [Windows.Media.PixelFormats]::Rgb24, $null, 0)
44 | $stream = [IO.File]::Open($imagePath, "OpenOrCreate")
45 | $encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder
46 | $encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($fcb)) | out-null
47 | $encoder.Save($stream) | out-null
48 | $stream.Dispose() | out-null
49 | }
50 |
51 | $imagePath
52 |
--------------------------------------------------------------------------------
/src/components/completionWatcher.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import { Extension } from '../main'
3 | import { TypeFinder } from './typeFinder'
4 | import { exec } from 'child_process'
5 | import * as path from 'path'
6 | import { existsSync, readFileSync } from 'fs'
7 | import { removeSync } from 'fs-extra'
8 |
9 | interface ISnippet {
10 | prefix: RegExp
11 | body: string
12 | description?: string
13 | priority?: number
14 | triggerWhenComplete?: boolean
15 | mode?: 'maths' | 'text' | 'any'
16 | noPlaceholders?: boolean
17 | }
18 |
19 | interface ISnippetConfig {
20 | prefix: string
21 | body: string
22 | description?: string
23 | priority?: number
24 | triggerWhenComplete?: boolean
25 | mode?: 'maths' | 'text' | 'any'
26 | noPlaceholders?: boolean
27 | }
28 |
29 | const DEBUG_CONSOLE_LOG = false
30 |
31 | /* eslint-disable */
32 | let debuglog: (icon: string, start: number, action: string) => void;
33 | if (DEBUG_CONSOLE_LOG) {
34 | debuglog = function (icon, start, action) {
35 | console.log(`${icon} Watcher took ${+new Date() - start}ms ${action}`);
36 | };
37 | } else {
38 | debuglog = (_i, _s, _a) => {};
39 | }
40 | /* eslint-enable */
41 | export class CompletionWatcher {
42 | extension: Extension
43 | typeFinder: TypeFinder
44 | private lastChanges: vscode.TextDocumentChangeEvent | undefined
45 | private lastKnownType:
46 | | {
47 | position: vscode.Position
48 | mode: 'maths' | 'text'
49 | }
50 | | undefined
51 | currentlyExecutingChange = false
52 | private enabled: boolean
53 | private configAge: number
54 | private MAX_CONFIG_AGE = 5000
55 | snippets: ISnippet[] = []
56 | snippetsConfig: ISnippetConfig[] = []
57 | activeSnippets: vscode.CompletionItem[] = []
58 |
59 | constructor(extension: Extension) {
60 | this.extension = extension
61 | this.typeFinder = new TypeFinder()
62 | this.enabled = vscode.workspace.getConfiguration('latex-utilities').get('liveReformat.enabled') as boolean
63 | this.configAge = +new Date()
64 | vscode.workspace.onDidChangeTextDocument(this.watcher, this)
65 |
66 | if (existsSync(this.getUserSnippetsFile())) {
67 | this.extension.logger.addLogMessage('User Snippets File Found, migrating to new config')
68 | const snippets = JSON.parse(readFileSync(this.getUserSnippetsFile(), { encoding: 'utf8' }))
69 | this.extension.logger.addLogMessage('User Snippets File Read')
70 | this.extension.logger.addLogMessage(JSON.stringify(snippets))
71 | // update config
72 | vscode.workspace.getConfiguration('latex-utilities').update('liveReformat.snippets', snippets, true)
73 | // remove user snippets file
74 | removeSync(this.getUserSnippetsFile())
75 | this.extension.logger.addLogMessage('User Snippets File Migrated')
76 | }
77 | this.loadSnippets()
78 | extension.logger.addLogMessage('Completion Watcher Initialised')
79 | }
80 |
81 | private processSnippets() {
82 | for (let i = 0; i < this.snippets.length; i++) {
83 | const snippet = this.snippets[i]
84 | if (!/\$\$(?:\d|{\d)/.test(snippet.body) && snippet.noPlaceholders === undefined) {
85 | snippet.noPlaceholders = true
86 | if (snippet.priority === undefined) {
87 | snippet.priority = -0.1
88 | }
89 | }
90 | if (snippet.priority === undefined) {
91 | snippet.priority = 0
92 | }
93 | if (snippet.triggerWhenComplete === undefined) {
94 | snippet.triggerWhenComplete = false
95 | }
96 | if (snippet.mode === undefined) {
97 | snippet.mode = 'any'
98 | } else if (!/^maths|text|any$/.test(snippet.mode)) {
99 | this.extension.logger.addLogMessage(
100 | `Invalid mode (${snippet.mode}) for live snippet "${snippet.description}"`
101 | )
102 | }
103 | }
104 | this.snippets.sort((a, b) => {
105 | return (b.priority === undefined ? 0 : b.priority) - (a.priority === undefined ? 0 : a.priority)
106 | })
107 | }
108 |
109 | public async watcher(e: vscode.TextDocumentChangeEvent) {
110 | if (+new Date() - this.configAge > this.MAX_CONFIG_AGE) {
111 | this.enabled = vscode.workspace.getConfiguration('latex-utilities').get('liveReformat.enabled') as boolean
112 | this.loadSnippets()
113 | this.configAge = +new Date()
114 | }
115 | const languages = vscode.workspace.getConfiguration('latex-utilities').get('liveReformat.languages') as string[]
116 | if (
117 | !languages.includes(e.document.languageId) ||
118 | e.contentChanges.length === 0 ||
119 | this.currentlyExecutingChange ||
120 | this.sameChanges(e) ||
121 | !this.enabled ||
122 | !vscode.window.activeTextEditor
123 | ) {
124 | return
125 | }
126 |
127 | this.lastChanges = e
128 | this.activeSnippets = []
129 |
130 | const start = +new Date()
131 | let columnOffset = 0
132 | for (const change of e.contentChanges) {
133 | const type = this.typeFinder.getTypeAtPosition(e.document, change.range.start, this.lastKnownType)
134 | this.lastKnownType = { position: change.range.start, mode: type }
135 | if (change.range.isSingleLine) {
136 | let line = e.document.lineAt(change.range.start.line)
137 | for (let i = 0; i < this.snippets.length; i++) {
138 | if (this.snippets[i].mode === 'any' || this.snippets[i].mode === type) {
139 | const newColumnOffset = await this.execSnippet(this.snippets[i], line, change, columnOffset)
140 | if (newColumnOffset === 'break') {
141 | break
142 | } else if (newColumnOffset !== undefined) {
143 | columnOffset += newColumnOffset
144 | line = e.document.lineAt(change.range.start.line)
145 | }
146 | }
147 | }
148 | }
149 | }
150 | this.extension.telemetryReporter.sendTelemetryEvent('liveSnippetTimings', {
151 | timeToCheck: (start - +new Date()).toString()
152 | })
153 | debuglog('🔵', start, 'to check for snippets')
154 | }
155 |
156 | private sameChanges(changes: vscode.TextDocumentChangeEvent) {
157 | if (!this.lastChanges) {
158 | return false
159 | } else if (this.lastChanges.contentChanges.length !== changes.contentChanges.length) {
160 | return false
161 | } else {
162 | const changeSame = this.lastChanges.contentChanges.every((value, index) => {
163 | const newChange = changes.contentChanges[index]
164 | if (value.text !== newChange.text || !value.range.isEqual(newChange.range)) {
165 | return false
166 | }
167 |
168 | return true
169 | })
170 | if (!changeSame) {
171 | return false
172 | }
173 | }
174 |
175 | return true
176 | }
177 |
178 | private async execSnippet(
179 | snippet: ISnippet,
180 | line: vscode.TextLine,
181 | change: vscode.TextDocumentContentChangeEvent,
182 | columnOffset: number
183 | ): Promise {
184 | return new Promise((resolve, reject) => {
185 | const match = snippet.prefix.exec(
186 | line.text.substr(0, change.range.start.character + change.text.length + columnOffset)
187 | )
188 | if (match && vscode.window.activeTextEditor) {
189 | let matchRange: vscode.Range
190 | let replacement: string
191 | if (snippet.body === 'SPECIAL_ACTION_BREAK') {
192 | resolve('break')
193 | return
194 | } else if (snippet.body === 'SPECIAL_ACTION_FRACTION') {
195 | [matchRange, replacement] = this.getFraction(match, line)
196 | } else {
197 | matchRange = new vscode.Range(
198 | new vscode.Position(line.lineNumber, match.index),
199 | new vscode.Position(line.lineNumber, match.index + match[0].length)
200 | )
201 | if (snippet.body === 'SPECIAL_ACTION_SYMPY') {
202 | replacement = this.execSympy(match, line)
203 | } else {
204 | replacement = match[0].replace(snippet.prefix, snippet.body).replace(/\$\$/g, '$')
205 | }
206 | }
207 | if (snippet.triggerWhenComplete) {
208 | this.currentlyExecutingChange = true
209 | const changeStart = +new Date()
210 | if (snippet.noPlaceholders) {
211 | vscode.window.activeTextEditor
212 | .edit(
213 | editBuilder => {
214 | editBuilder.replace(matchRange, replacement)
215 | },
216 | { undoStopBefore: true, undoStopAfter: true }
217 | )
218 | .then(() => {
219 | const offset = replacement.length - match[0].length
220 | if (vscode.window.activeTextEditor && offset > 0) {
221 | vscode.window.activeTextEditor.selection = new vscode.Selection(
222 | vscode.window.activeTextEditor.selection.anchor.translate(0, offset),
223 | vscode.window.activeTextEditor.selection.anchor.translate(0, offset)
224 | )
225 | }
226 | this.currentlyExecutingChange = false
227 | debuglog(' ▹', changeStart, 'to perform text replacement')
228 | resolve(offset)
229 | })
230 | } else {
231 | vscode.window.activeTextEditor
232 | .edit(
233 | editBuilder => {
234 | editBuilder.delete(matchRange)
235 | },
236 | { undoStopBefore: true, undoStopAfter: false }
237 | )
238 | .then(
239 | () => {
240 | if (!vscode.window.activeTextEditor) {
241 | return
242 | }
243 | vscode.window.activeTextEditor
244 | .insertSnippet(new vscode.SnippetString(replacement), undefined, {
245 | undoStopBefore: true,
246 | undoStopAfter: true
247 | })
248 | .then(
249 | () => {
250 | this.currentlyExecutingChange = false
251 | debuglog(' ▹', changeStart, 'to insert snippet')
252 | resolve(replacement.length - match[0].length)
253 | },
254 | (reason: unknown) => {
255 | this.currentlyExecutingChange = false
256 | reject(reason)
257 | }
258 | )
259 | },
260 | (reason: unknown) => {
261 | this.currentlyExecutingChange = false
262 | reject(reason)
263 | }
264 | )
265 | }
266 | } else {
267 | this.activeSnippets.push({
268 | label: replacement,
269 | filterText: match[0],
270 | sortText: match[0],
271 | range: matchRange,
272 | detail: 'live snippet',
273 | kind: vscode.CompletionItemKind.Reference
274 | })
275 | }
276 | } else {
277 | resolve(undefined)
278 | }
279 | })
280 | }
281 |
282 | public provide(): vscode.CompletionItem[] {
283 | return this.activeSnippets
284 | }
285 |
286 | public editSnippetsFile() {
287 | vscode.commands.executeCommand('workbench.action.openSettingsJson', {
288 | revealSetting: {
289 | key: 'latex-utilities.liveReformat.snippets',
290 | edit: true,
291 | }
292 | })
293 | }
294 |
295 | public loadSnippets() {
296 | // if this.snippetsConfig is same with getConfiguration, skip
297 | if (JSON.stringify(this.snippetsConfig) === JSON.stringify(vscode.workspace.getConfiguration('latex-utilities').get('liveReformat.snippets'))) {
298 | return
299 | }
300 | this.snippetsConfig = vscode.workspace.getConfiguration('latex-utilities').get('liveReformat.snippets') as ISnippetConfig[]
301 | try {
302 | this.snippets = []
303 | for (let i = 0; i < this.snippetsConfig.length; i++) {
304 | // snippets[i].prefix = new RegExp(snippets[i].prefix);
305 | this.snippets.push({
306 | ...this.snippetsConfig[i],
307 | prefix: new RegExp(this.snippetsConfig[i].prefix)
308 | })
309 | }
310 | this.processSnippets()
311 | } catch (error) {
312 | this.extension.logger.logError(error)
313 | this.extension.logger.showErrorMessage('Couldn\'t load snippets file. Is it a valid JSON?')
314 | }
315 | }
316 |
317 | private getUserSnippetsFile() {
318 | const codeFolder = vscode.version.indexOf('insider') >= 0 ? 'Code - Insiders' : 'Code'
319 | const templateName = 'latexUtilsLiveSnippets.json'
320 |
321 | if (process.platform === 'win32' && process.env.APPDATA) {
322 | return path.join(process.env.APPDATA, codeFolder, 'User', templateName)
323 | } else if (process.platform === 'darwin' && process.env.HOME) {
324 | return path.join(process.env.HOME, 'Library', 'Application Support', codeFolder, 'User', templateName)
325 | } else if (process.platform === 'linux' && process.env.HOME) {
326 | let conf = path.join(process.env.HOME, '.config', codeFolder, 'User', templateName)
327 | if (existsSync(conf)) {
328 | return conf
329 | } else {
330 | conf = path.join(process.env.HOME, '.config', 'Code - OSS', 'User', templateName)
331 | return conf
332 | }
333 | } else {
334 | return ''
335 | }
336 | }
337 |
338 | private getFraction(match: RegExpExecArray, line: vscode.TextLine): [vscode.Range, string] {
339 | type TclosingBracket = ')' | ']' | '}'
340 | type TopeningBracket = '(' | '[' | '{'
341 | const closingBracket = match[1] as TclosingBracket
342 | // eslint-disable-next-line @typescript-eslint/naming-convention
343 | const openingBracket = { ')': '(', ']': '[', '}': '{' }[closingBracket] as TopeningBracket
344 | let depth = 0
345 | for (let i = match.index; i >= 0; i--) {
346 | if (line.text[i] === closingBracket) {
347 | depth--
348 | } else if (line.text[i] === openingBracket) {
349 | depth++
350 | }
351 | if (depth === 0) {
352 | // if command keep going till the \
353 | const commandMatch = /.*(\\\w+)$/.exec(line.text.substr(0, i))
354 | if (closingBracket === '}') {
355 | if (commandMatch !== null) {
356 | i -= commandMatch[1].length
357 | }
358 | }
359 | const matchRange = new vscode.Range(
360 | new vscode.Position(line.lineNumber, i),
361 | new vscode.Position(line.lineNumber, match.index + match[0].length)
362 | )
363 | const replacement = `\\frac{${commandMatch ? '\\' : ''}${line.text.substring(i + 1, match.index)}}{$1} `
364 | return [matchRange, replacement]
365 | }
366 | }
367 | return [
368 | new vscode.Range(
369 | new vscode.Position(line.lineNumber, match.index + match[0].length),
370 | new vscode.Position(line.lineNumber, match.index + match[0].length)
371 | ),
372 | ''
373 | ]
374 | }
375 |
376 | private execSympy(match: RegExpExecArray, line: vscode.TextLine) {
377 | const replacement = 'SYMPY_CALCULATING'
378 | const command = match[1]
379 | .replace(/\\(\w+) ?/g, '$1')
380 | .replace(/\^/, '**')
381 | .replace('{', '(')
382 | .replace('}', ')')
383 | exec(
384 | `python3 -c "from sympy import *
385 | import re
386 | a, b, c, x, y, z, t = symbols('a b c x y z t')
387 | k, m, n = symbols('k m n', integer=True)
388 | f, g, h = symbols('f g h', cls=Function)
389 | init_printing()
390 | print(eval('''latex(${command})'''), end='')"`,
391 | { encoding: 'utf8' },
392 | (_error, stdout, stderr) => {
393 | if (!vscode.window.activeTextEditor) {
394 | return
395 | } else if (stderr) {
396 | stdout = 'SYMPY_ERROR'
397 | setTimeout(() => {
398 | this.extension.logger.addLogMessage(`error executing sympy command: ${command}`)
399 | if (!vscode.window.activeTextEditor) {
400 | return
401 | }
402 |
403 | vscode.window.activeTextEditor.edit(editBuilder => {
404 | editBuilder.delete(
405 | new vscode.Range(
406 | new vscode.Position(line.lineNumber, match.index),
407 | new vscode.Position(line.lineNumber, match.index + stdout.length)
408 | )
409 | )
410 | })
411 | }, 400)
412 | }
413 | vscode.window.activeTextEditor.edit(editBuilder => {
414 | editBuilder.replace(
415 | new vscode.Range(
416 | new vscode.Position(line.lineNumber, match.index),
417 | new vscode.Position(line.lineNumber, match.index + replacement.length)
418 | ),
419 | stdout
420 | )
421 | })
422 | }
423 | )
424 | return replacement
425 | }
426 | }
427 |
428 | export class Completer implements vscode.CompletionItemProvider {
429 | extension: Extension
430 |
431 | constructor(extension: Extension) {
432 | this.extension = extension
433 | }
434 |
435 | provideCompletionItems(
436 | /* eslint-disable @typescript-eslint/no-unused-vars */
437 | _document: vscode.TextDocument,
438 | _position: vscode.Position,
439 | _token: vscode.CancellationToken,
440 | _context: vscode.CompletionContext
441 | /* eslint-enable @typescript-eslint/no-unused-vars */
442 | ): vscode.ProviderResult {
443 | return this.extension.completionWatcher.activeSnippets
444 | }
445 | }
446 |
--------------------------------------------------------------------------------
/src/components/logger.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 |
3 | import { Extension } from '../main'
4 |
5 | export class Logger {
6 | extension: Extension
7 | logPanel: vscode.OutputChannel
8 |
9 | constructor(extension: Extension) {
10 | this.extension = extension
11 | this.logPanel = vscode.window.createOutputChannel('LaTeX Utilities')
12 | this.addLogMessage('Initializing LaTeX Utilities.')
13 | }
14 |
15 | addLogMessage(message: string) {
16 | this.logPanel.append(`[${new Date().toLocaleTimeString('en-US', { hour12: false })}] ${message}\n`)
17 | }
18 |
19 | showErrorMessage(message: string, ...args: any): Thenable | undefined {
20 | const configuration = vscode.workspace.getConfiguration('latex-utilities')
21 | if (configuration.get('message.error.show')) {
22 | return vscode.window.showErrorMessage(message, ...args)
23 | } else {
24 | return undefined
25 | }
26 | }
27 |
28 | showLog() {
29 | this.logPanel.show()
30 | }
31 |
32 | logError(e: Error) {
33 | this.addLogMessage(e.message)
34 | if (e.stack) {
35 | this.addLogMessage(e.stack)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/paster.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as path from 'path'
3 | import * as fs from 'fs/promises'
4 | import { constants as fsconstants } from 'fs'
5 | import * as fse from 'fs-extra'
6 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
7 | import csv from 'csv-parser'
8 | import { Readable } from 'stream'
9 |
10 | import { Extension } from '../main'
11 |
12 | export class Paster {
13 | extension: Extension
14 |
15 | constructor(extension: Extension) {
16 | this.extension = extension
17 | }
18 |
19 | public async paste() {
20 | this.extension.logger.addLogMessage('Performing formatted paste')
21 |
22 | // get current edit file path
23 | const editor = vscode.window.activeTextEditor
24 | if (!editor) {
25 | return
26 | }
27 |
28 | const fileUri = editor.document.uri
29 | if (!fileUri) {
30 | return
31 | }
32 |
33 | const clipboardContents = await vscode.env.clipboard.readText()
34 |
35 | // if empty try pasting an image from clipboard
36 | // also try pasting image first on linux
37 | if (clipboardContents === '' || process.platform === 'linux') {
38 | if (fileUri.scheme === 'untitled') {
39 | vscode.window.showInformationMessage('You need to the save the current editor before pasting an image')
40 |
41 | return
42 | }
43 | if (await this.pasteImage(editor, fileUri.fsPath)){
44 | this.extension.logger.addLogMessage('paste image success. returning')
45 | return
46 | }
47 | this.extension.logger.addLogMessage('paste image finished and failed.')
48 | }
49 |
50 | if (clipboardContents.split('\n').length === 1) {
51 | let filePath: string
52 | let basePath: string
53 | if (fileUri.scheme === 'untitled') {
54 | filePath = clipboardContents
55 | basePath = ''
56 | } else {
57 | filePath = path.resolve(fileUri.fsPath, clipboardContents)
58 | basePath = fileUri.fsPath
59 | }
60 | try {
61 | await fs.access(filePath, fsconstants.R_OK)
62 | if (await this.pasteFile(editor, basePath, clipboardContents)) {
63 | this.extension.logger.addLogMessage('paste file success. returning')
64 | return
65 | }
66 | } catch (error) {
67 | // pass
68 | }
69 | }
70 | // if not pasting file
71 | try {
72 | await this.pasteTable(editor, clipboardContents)
73 | } catch (error) {
74 | this.pasteNormal(
75 | editor,
76 | this.reformatText(
77 | clipboardContents,
78 | true,
79 | vscode.workspace.getConfiguration('latex-utilities.formattedPaste').get('maxLineLength') as number,
80 | editor
81 | )
82 | )
83 | this.extension.telemetryReporter.sendTelemetryEvent('formattedPaste', { type: 'text' })
84 | }
85 | }
86 |
87 | public pasteNormal(editor: vscode.TextEditor, content: string) {
88 | editor.edit(edit => {
89 | const current = editor.selection
90 |
91 | if (current.isEmpty) {
92 | edit.insert(current.start, content)
93 | } else {
94 | edit.replace(current, content)
95 | }
96 | })
97 | }
98 |
99 | public async pasteFile(editor: vscode.TextEditor, baseFile: string, file: string): Promise {
100 | this.extension.logger.addLogMessage('Pasting: file')
101 | const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.eps', '.pdf']
102 | const TABLE_FORMATS = ['.csv']
103 | const extension = path.extname(file)
104 |
105 | if (IMAGE_EXTENSIONS.indexOf(extension) !== -1) {
106 | this.extension.logger.addLogMessage('File is an image.')
107 | return this.pasteImage(editor, baseFile, file)
108 | } else if (TABLE_FORMATS.indexOf(extension) !== -1) {
109 | if (extension === '.csv') {
110 | const fileContent = await fs.readFile(path.resolve(baseFile, file))
111 | await this.pasteTable(editor, fileContent.toString())
112 | return true
113 | }
114 | }
115 | return false
116 | }
117 |
118 | public async pasteTable(editor: vscode.TextEditor, content: string, delimiter?: string) {
119 | this.extension.logger.addLogMessage('Pasting: Table')
120 | const configuration = vscode.workspace.getConfiguration('latex-utilities.formattedPaste')
121 |
122 | const columnDelimiter: string = delimiter || configuration.customTableDelimiter
123 | const columnType: string = configuration.tableColumnType
124 | const booktabs: boolean = configuration.tableBooktabsStyle
125 | const headerRows: number = configuration.tableHeaderRows
126 |
127 | const trimUnwantedWhitespace = (s: string) =>
128 | s
129 | .replace(/\r\n/g, '\n')
130 | .replace(/^[^\S\t]+|[^\S\t]+$/gm, '')
131 | .replace(/^[\uFEFF\xA0]+|[\uFEFF\xA0]+$/gm, '')
132 | content = trimUnwantedWhitespace(content)
133 |
134 | const TEST_DELIMITERS = new Set([columnDelimiter, '\t', ',', '|'])
135 | const tables: string[][][] = []
136 |
137 | for (const testDelimiter of TEST_DELIMITERS) {
138 | try {
139 | const table = await this.processTable(content, testDelimiter)
140 | tables.push(table)
141 | this.extension.logger.addLogMessage(`Successfully found ${testDelimiter} delimited table`)
142 | } catch (e) {
143 | this.extension.logger.addLogMessage(`Failed to find ${testDelimiter} delimited table`)
144 | this.extension.logger.addLogMessage(e)
145 | }
146 | }
147 |
148 | if (tables.length === 0) {
149 | this.extension.logger.addLogMessage('No table found')
150 | if (configuration.tableDelimiterPrompt) {
151 | const columnDelimiterNew = await vscode.window.showInputBox({
152 | prompt: 'Please specify the table cell delimiter',
153 | value: columnDelimiter,
154 | placeHolder: columnDelimiter,
155 | validateInput: (text: string) => {
156 | return text === '' ? 'No delimiter specified!' : null
157 | }
158 | })
159 | if (columnDelimiterNew === undefined) {
160 | throw new Error('no table cell delimiter set')
161 | }
162 |
163 | try {
164 | const table = await this.processTable(content, columnDelimiterNew)
165 | tables.push(table)
166 | this.extension.logger.addLogMessage(`Successfully found ${columnDelimiterNew} delimited table`)
167 | } catch (e) {
168 | vscode.window.showWarningMessage(e)
169 | throw Error('Unable to identify table')
170 | }
171 | } else {
172 | throw Error('Unable to identify table')
173 | }
174 | }
175 |
176 | // put the 'biggest' table first
177 | tables.sort((a, b) => a.length * a[0].length - b.length * b[0].length)
178 | const table = tables[0].map(row => row.map(cell => this.reformatText(cell.replace(/^\s+|\s+$/gm, ''), false)))
179 |
180 | const tabularRows = table.map(row => '\t' + row.join(' & '))
181 |
182 | if (headerRows && tabularRows.length > headerRows) {
183 | const eol = editor.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n'
184 | const headSep = '\t' + (booktabs ? '\\midrule' : '\\hline') + eol
185 | tabularRows[headerRows] = headSep + tabularRows[headerRows]
186 | }
187 | let tabularContents = tabularRows.join(' \\\\\n')
188 | if (booktabs) {
189 | tabularContents = '\t\\toprule\n' + tabularContents + ' \\\\\n\t\\bottomrule'
190 | }
191 | const tabular = `\\begin{tabular}{${columnType.repeat(table[0].length)}}\n${tabularContents}\n\\end{tabular}`
192 |
193 | editor.edit(edit => {
194 | const current = editor.selection
195 |
196 | if (current.isEmpty) {
197 | edit.insert(current.start, tabular)
198 | } else {
199 | edit.replace(current, tabular)
200 | }
201 | })
202 |
203 | this.extension.telemetryReporter.sendTelemetryEvent('formattedPaste', { type: 'table' })
204 | }
205 |
206 | private processTable(content: string, delimiter = ','): Promise {
207 | const isConsistent = (rows: string[][]) => {
208 | return rows.reduce((accumulator, current, _index, array) => {
209 | if (current.length === array[0].length) {
210 | return accumulator
211 | } else {
212 | return false
213 | }
214 | }, true)
215 | }
216 | // if table is flanked by empty rows/columns, remove them
217 | const trimSides = (rows: string[][]): string[][] => {
218 | const emptyTop = rows[0].reduce((a, c) => c + a, '') === ''
219 | const emptyBottom = rows[rows.length - 1].reduce((a, c) => c + a, '') === ''
220 | const emptyLeft = rows.reduce((a, c) => a + c[0], '') === ''
221 | const emptyRight = rows.reduce((a, c) => a + c[c.length - 1], '') === ''
222 | if (!(emptyTop || emptyBottom || emptyLeft || emptyRight)) {
223 | return rows
224 | } else {
225 | if (emptyTop) {
226 | rows.shift()
227 | }
228 | if (emptyBottom) {
229 | rows.pop()
230 | }
231 | if (emptyLeft) {
232 | rows.forEach(row => row.shift())
233 | }
234 | if (emptyRight) {
235 | rows.forEach(row => row.pop())
236 | }
237 | return trimSides(rows)
238 | }
239 | }
240 | return new Promise((resolve, reject) => {
241 | let rows: string[][] = []
242 | const contentStream = new Readable()
243 | // if markdown / org mode / ascii table we want to strip some rows
244 | if (delimiter === '|') {
245 | const removeRowsRegex = /^\s*[-+:| ]+\s*$/
246 | const lines = content.split('\n').filter(l => !removeRowsRegex.test(l))
247 | content = lines.join('\n')
248 | }
249 | contentStream.push(content)
250 | contentStream.push(null)
251 | contentStream
252 | .pipe(csv({ headers: false, separator: delimiter }))
253 | .on('data', (data: { [key: string]: string }) => rows.push(Object.values(data)))
254 | .on('end', () => {
255 | rows = trimSides(rows)
256 | // determine if all rows have same number of cells
257 | if (!isConsistent(rows)) {
258 | reject('Table is not consistent')
259 | } else if (rows.length === 1 || rows[0].length === 1) {
260 | reject('Doesn\'t look like a table')
261 | }
262 |
263 | resolve(rows)
264 | })
265 | })
266 | }
267 |
268 | public reformatText(
269 | text: string,
270 | removeBonusWhitespace = true,
271 | maxLineLength: number | null = null,
272 | editor?: vscode.TextEditor
273 | ) {
274 | function doRemoveBonusWhitespace(str: string) {
275 | str = str.replace(/\u200B/g, '') // get rid of zero-width spaces
276 | str = str.replace(/\n{2,}/g, '\uE000') // 'save' multi-newlines to private use character
277 | str = str.replace(/\s+/g, ' ') // replace all whitespace with normal space
278 | str = str.replace(/\uE000/g, '\n\n') // re-insert multi-newlines
279 | str = str.replace(/\uE001/g, '\n') // this has been used as 'saved' whitespace
280 | str = str.replace(/\uE002/g, '\t') // this has been used as 'saved' whitespace
281 | str = str.replace(/^\s+|\s+$/g, '')
282 |
283 | return str
284 | }
285 | function fitToLineLength(lineLength: number, str: string, splitChars = [' ', ',', '.', ':', ';', '?', '!']) {
286 | const lines = []
287 | const indent = editor
288 | ? editor.document.lineAt(editor.selection.start.line).text.replace(/^(\s*).*/, '$1')
289 | : ''
290 | let lastNewlinePosition = editor ? -editor.selection.start.character : 0
291 | let lastSplitCharPosition = 0
292 | let i
293 | for (i = 0; i < str.length; i++) {
294 | if (str[i] === '\n') {
295 | lines.push(
296 | (lines.length > 0 ? indent : '') +
297 | str
298 | .slice(Math.max(0, lastNewlinePosition), i)
299 | .replace(/^[^\S\t]+/, '')
300 | .replace(/\s+$/, '')
301 | )
302 | lastNewlinePosition = i
303 | }
304 | if (splitChars.indexOf(str[i]) !== -1) {
305 | lastSplitCharPosition = i + 1
306 | }
307 | if (i - lastNewlinePosition >= lineLength - indent.length) {
308 | lines.push(
309 | (lines.length > 0 ? indent : '') +
310 | str
311 | .slice(Math.max(0, lastNewlinePosition), lastSplitCharPosition)
312 | .replace(/^[^\S\t]+/, '')
313 | .replace(/\s+$/, '')
314 | )
315 | lastNewlinePosition = lastSplitCharPosition
316 | i = lastSplitCharPosition
317 | }
318 | }
319 | if (lastNewlinePosition < i) {
320 | lines.push(
321 | (lines.length > 0 ? indent : '') +
322 | str
323 | .slice(Math.max(0, lastNewlinePosition), i)
324 | .replace(/^\s+/, '')
325 | .replace(/\s+$/, '')
326 | )
327 | }
328 | console.log(lines.map(l => lineLength - l.length))
329 | return lines.join('\n')
330 | }
331 |
332 | // join hyphenated lines
333 | text = text.replace(/(\w+)-\s?$\s?\n(\w+)/gm, '$1$2\n')
334 |
335 | /* eslint-disable @typescript-eslint/naming-convention */
336 | const textReplacements: { [key: string]: string } = {
337 | // escape latex special characters
338 | '\\\\': '\\textbackslash ',
339 | '&': '\\&',
340 | '%': '\\%',
341 | '\\$': '\\$',
342 | '#': '\\#',
343 | _: '\\_',
344 | '\\^': '\\textasciicircum ',
345 | '{': '\\{',
346 | '}': '\\}',
347 | '~': '\\textasciitilde ',
348 | // dumb quotes
349 | '\\B"([^"]+)"\\B': '``$1\'\'',
350 | '\\B\'([^\']+)\'\\B': '`$1\'',
351 | // 'smart' quotes
352 | '“': '``',
353 | '”': '\'\'',
354 | '‘': '`',
355 | '’': '\'',
356 | // unicode symbols
357 | '—': '---', // em dash
358 | '–': '--', // en dash
359 | ' -- ': ' --- ', // en dash that should be em
360 | '−': '-', // minus sign
361 | '…': '\\ldots ', // elipses
362 | '‐': '-', // hyphen
363 | '™': '\\texttrademark ', // trade mark
364 | '®': '\\textregistered ', // registered trade mark
365 | '©': '\\textcopyright ', // copyright
366 | '¢': '\\cent ', // copyright
367 | '£': '\\pound ', // copyright
368 | // unicode math
369 | '×': '\\(\\times \\)',
370 | '÷': '\\(\\div \\)',
371 | '±': '\\(\\pm \\)',
372 | '→': '\\(\\to \\)',
373 | '(\\d*)° ?(C|F)?': '\\($1^\\circ $2\\)',
374 | '≤': '\\(\\leq \\)',
375 | '≥': '\\(\\geq \\)',
376 | // typographic approximations
377 | '\\.\\.\\.': '\\ldots ',
378 | '-{20,}': '\\hline',
379 | '-{2,3}>': '\\(\\longrightarrow \\)',
380 | '->': '\\(\\to \\)',
381 | '<-{2,3}': '\\(\\longleftarrow \\)',
382 | '<-': '\\(\\leftarrow \\)',
383 | // more latex stuff
384 | '\\b([A-Z]+)\\.\\s([A-Z])': '$1\\@. $2', // sentences that end in a capital letter.
385 | '\\b(etc|ie|i\\.e|eg|e\\.g)\\.\\s(\\w)': '$1.\\ $2', // phrases that should have inter-word spacing
386 | // some funky unicode symbols that come up here and there
387 | '\\s?•\\s?': '\uE001\uE002\\item ',
388 | '\\n?((?:\\s*\uE002\\\\item .*)+)': '\uE001\\begin{itemize}\uE001$1\uE001\\end{itemize}\uE001',
389 | '': '<',
390 | '': '-',
391 | '': '>'
392 | }
393 | /* eslint-enable @typescript-eslint/naming-convention */
394 |
395 | const texText = /\\[A-Za-z]{3,15}/
396 |
397 | if (!texText.test(text)) {
398 | for (const pattern in textReplacements) {
399 | text = text.replace(new RegExp(pattern, 'gm'), textReplacements[pattern])
400 | }
401 | }
402 |
403 | if (removeBonusWhitespace) {
404 | text = doRemoveBonusWhitespace(text)
405 | }
406 |
407 | if (maxLineLength !== null) {
408 | text = fitToLineLength(maxLineLength, text)
409 | }
410 |
411 | return text
412 | }
413 |
414 | // Image pasting code below from https://github.com/mushanshitiancai/vscode-paste-image/
415 | // Copyright 2016 mushanshitiancai
416 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
417 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
418 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
419 |
420 | PATH_VARIABLE_GRAPHICS_PATH = /\$\{graphicsPath\}/g
421 | PATH_VARIABLE_CURRNET_FILE_DIR = /\$\{currentFileDir\}/g
422 |
423 | PATH_VARIABLE_IMAGE_FILE_PATH = /\$\{imageFilePath\}/g
424 | PATH_VARIABLE_IMAGE_FILE_PATH_WITHOUT_EXT = /\$\{imageFilePathWithoutExt\}/g
425 | PATH_VARIABLE_IMAGE_FILE_NAME = /\$\{imageFileName\}/g
426 | PATH_VARIABLE_IMAGE_FILE_NAME_WITHOUT_EXT = /\$\{imageFileNameWithoutExt\}/g
427 |
428 | pasteTemplate = ''
429 | basePathConfig = '${graphicsPath}'
430 | graphicsPathFallback = '${currentFileDir}'
431 |
432 | public async pasteImage(editor: vscode.TextEditor, baseFile: string, imgFile?: string): Promise {
433 | this.extension.logger.addLogMessage(`Pasting: Image. imgFile: ${imgFile}`)
434 |
435 | const folderPath = path.dirname(baseFile)
436 | const projectPath = vscode.workspace.workspaceFolders
437 | ? vscode.workspace.workspaceFolders[0].uri.fsPath
438 | : folderPath
439 |
440 | // get selection as image file name, need check
441 | const selection = editor.selection
442 | const selectText = editor.document.getText(selection)
443 | if (selectText && /\//.test(selectText)) {
444 | vscode.window.showInformationMessage('Your selection is not a valid file name!')
445 |
446 | return false
447 | }
448 |
449 | this.loadImageConfig(projectPath, baseFile)
450 |
451 | if (imgFile && !selectText) {
452 | const imagePath = this.renderImagePaste(path.dirname(baseFile), imgFile)
453 |
454 | if (!vscode.window.activeTextEditor) {
455 | return false
456 | }
457 | vscode.window.activeTextEditor.insertSnippet(new vscode.SnippetString(imagePath), editor.selection.start, {
458 | undoStopBefore: true,
459 | undoStopAfter: true
460 | })
461 |
462 | return false
463 | }
464 |
465 | const imagePath = await this.getImagePath(baseFile, imgFile, selectText, this.basePathConfig)
466 | this.extension.logger.addLogMessage(`Image path: ${imagePath}`)
467 | if (!imagePath) {
468 | this.extension.logger.addLogMessage('Could not get image path.')
469 | return false
470 | }
471 | // does the file exist?
472 | try {
473 | await fs.access(imagePath, fsconstants.F_OK)
474 | const choice = await vscode.window
475 | .showInformationMessage(
476 | `File ${imagePath} exists. Would you want to replace?`,
477 | 'Replace',
478 | 'Cancel'
479 | )
480 | if (choice !== 'Replace') {
481 | this.extension.logger.addLogMessage('User cancelled the image paste.')
482 | return false
483 | }
484 | } catch(e) {
485 | // pass, we save the image if it doesn't exists
486 | }
487 | this.saveAndPaste(editor, imagePath, imgFile)
488 | return true
489 | }
490 |
491 | public loadImageConfig(projectPath: string, filePath: string) {
492 | const config = vscode.workspace.getConfiguration('latex-utilities.formattedPaste.image')
493 |
494 | // load other config
495 | const pasteTemplate: string | string[] | undefined = config.get('template')
496 | if (pasteTemplate === undefined) {
497 | throw new Error('No config value found for latex-utilities.imagePaste.template')
498 | }
499 | if (typeof pasteTemplate === 'string') {
500 | this.pasteTemplate = pasteTemplate
501 | } else {
502 | // is multiline string represented by array
503 | this.pasteTemplate = pasteTemplate.join('\n')
504 | }
505 |
506 | this.graphicsPathFallback = this.replacePathVariables('${currentFileDir}', projectPath, filePath)
507 | this.basePathConfig = this.replacePathVariables('${graphicsPath}', projectPath, filePath)
508 | this.pasteTemplate = this.replacePathVariables(this.pasteTemplate, projectPath, filePath)
509 | }
510 |
511 | public async getImagePath(
512 | filePath: string,
513 | imagePathCurrent = '',
514 | selectText: string,
515 | folderPathFromConfig: string
516 | ) {
517 | const graphicsPath = this.basePathConfig
518 | try {
519 | await this.ensureImgDirExists(graphicsPath)
520 | } catch (e) {
521 | vscode.window.showErrorMessage(`Could not find image directory: ${e.message}`)
522 | return null
523 | }
524 | const imgPostfixNumber =
525 | Math.max(
526 | 0,
527 | ...(await fs.readdir(graphicsPath))
528 | .map(imagePath => parseInt(imagePath.replace(/^image(\d+)\.\w+/, '$1')))
529 | .filter(num => !isNaN(num))
530 | ) + 1
531 | const imgExtension = path.extname(imagePathCurrent) ? path.extname(imagePathCurrent) : '.png'
532 | const imageFileName = selectText ? selectText + imgExtension : `image${imgPostfixNumber}` + imgExtension
533 |
534 | let result = await vscode.window.showInputBox({
535 | prompt: 'Please specify the filename of the image.',
536 | value: imageFileName,
537 | valueSelection: [imageFileName.length - imageFileName.length, imageFileName.length - 4]
538 | })
539 | if (result) {
540 | if (!result.endsWith(imgExtension)) {
541 | result += imgExtension
542 | }
543 |
544 | result = makeImagePath(result)
545 | return result
546 | } else {
547 | return null
548 | }
549 |
550 | function makeImagePath(fileName: string) {
551 | // image output path
552 | const folderPath = path.dirname(filePath)
553 | let imagePath = ''
554 |
555 | // generate image path
556 | if (path.isAbsolute(folderPathFromConfig)) {
557 | imagePath = path.join(folderPathFromConfig, fileName)
558 | } else {
559 | imagePath = path.join(folderPath, folderPathFromConfig, fileName)
560 | }
561 |
562 | return imagePath
563 | }
564 | }
565 |
566 | public async saveAndPaste(editor: vscode.TextEditor, imgPath: string, oldPath?: string) {
567 | this.extension.logger.addLogMessage(`save and paste. imagePath: ${imgPath}`)
568 | if (oldPath) {
569 | await fs.copyFile(oldPath, imgPath)
570 | const imageString = this.renderImagePaste(this.basePathConfig, imgPath)
571 |
572 | const current = editor.selection
573 | if (!current.isEmpty) {
574 | editor.edit(
575 | editBuilder => {
576 | editBuilder.delete(current)
577 | },
578 | { undoStopBefore: true, undoStopAfter: false }
579 | )
580 | }
581 |
582 | if (!vscode.window.activeTextEditor) {
583 | return
584 | }
585 | vscode.window.activeTextEditor.insertSnippet(
586 | new vscode.SnippetString(imageString),
587 | editor.selection.start,
588 | {
589 | undoStopBefore: true,
590 | undoStopAfter: true
591 | }
592 | )
593 | } else {
594 | const imagePathReturnByScript = await this.saveClipboardImageToFileAndGetPath(imgPath)
595 | if (!imagePathReturnByScript) {
596 | return
597 | }
598 | if (imagePathReturnByScript === 'no image') {
599 | vscode.window.showInformationMessage('No image in clipboard')
600 | return
601 | }
602 |
603 | const imageString = this.renderImagePaste(this.basePathConfig, imgPath)
604 |
605 | const current = editor.selection
606 | if (!current.isEmpty) {
607 | editor.edit(
608 | editBuilder => {
609 | editBuilder.delete(current)
610 | },
611 | { undoStopBefore: true, undoStopAfter: false }
612 | )
613 | }
614 |
615 | if (!vscode.window.activeTextEditor) {
616 | return
617 | }
618 | vscode.window.activeTextEditor.insertSnippet(
619 | new vscode.SnippetString(imageString),
620 | editor.selection.start,
621 | {
622 | undoStopBefore: true,
623 | undoStopAfter: true
624 | }
625 | )
626 | }
627 | this.extension.telemetryReporter.sendTelemetryEvent('formattedPaste', { type: 'image' })
628 | }
629 |
630 | private async ensureImgDirExists(imagePath: string){
631 | try {
632 | const stats = await fs.stat(imagePath)
633 | if (stats.isDirectory()) {
634 | return
635 | } else {
636 | throw new Error(`The image destination directory '${imagePath}' is a file.`)
637 | }
638 | } catch (error) {
639 | if (error.code === 'ENOENT') {
640 | this.extension.logger.addLogMessage(`Image directory ${imagePath} doesn't exist. Trying to create it...`)
641 | await fse.ensureDir(imagePath)
642 | } else {
643 | throw error
644 | }
645 | }
646 | }
647 |
648 | private wrapProcessAsPromise(process: ChildProcessWithoutNullStreams): Promise {
649 | return new Promise((resolve, reject) => {
650 | let data = ''
651 | process.stdout.on('data', (newData) => {
652 | data += newData
653 | })
654 | // wslPath-disable-next-line @typescript-eslint/no-unused-vars
655 | process.on('exit', (_code, _signal) => {
656 | if (_code === 0) {
657 | resolve(data)
658 | } else {
659 | reject(new Error(`wslpath failed with code ${_code} and signal ${_signal}`))
660 | }
661 | })
662 | process.on('error', e => {
663 | reject(e)
664 | })
665 | })
666 | }
667 |
668 | // TODO: turn into async function, and raise errors internally
669 | private async saveClipboardImageToFileAndGetPath(
670 | imagePath: string
671 | ): Promise {
672 | if (!imagePath) {
673 | return null
674 | }
675 | try {
676 | const platform = process.platform
677 | if (vscode.env.remoteName === 'wsl') {
678 | // WSL
679 | let scriptPath = path.join(this.extension.extensionRoot, './scripts/saveclipimg-pc.ps1')
680 | // convert scriptPath to windows path
681 | const scriptPathPromise = this.wrapProcessAsPromise(spawn('wslpath', [
682 | '-w',
683 | scriptPath
684 | ]))
685 | scriptPath = (await scriptPathPromise).toString().trim().replace('\\wsl.localhost', '\\wsl$') // see Powershell/powershell#17623
686 | this.extension.logger.addLogMessage(`saveClipimg-pc.ps1: ${scriptPath}`)
687 |
688 | const imagePathPromise = this.wrapProcessAsPromise(spawn('wslpath', [
689 | '-w',
690 | imagePath
691 | ]))
692 | imagePath = (await imagePathPromise).toString().trim()
693 | this.extension.logger.addLogMessage(`imagePath: ${imagePath}`)
694 |
695 | const pastePromise = this.wrapProcessAsPromise(spawn('powershell.exe', [
696 | '-noprofile',
697 | '-noninteractive',
698 | '-nologo',
699 | '-sta',
700 | '-executionpolicy',
701 | 'unrestricted',
702 | '-windowstyle',
703 | 'hidden',
704 | '-file',
705 | scriptPath,
706 | imagePath
707 | ]))
708 | const data = (await pastePromise).toString().trim()
709 | return data
710 | } else if (platform === 'win32') {
711 | // Windows
712 | const scriptPath = path.join(this.extension.extensionRoot, './scripts/saveclipimg-pc.ps1')
713 |
714 | let command = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
715 | try {
716 | fs.access(command, fsconstants.X_OK)
717 | } catch (e) {
718 | // powershell.exe doesn't exist;
719 | command = 'powershell'
720 | }
721 | const pastePromise = this.wrapProcessAsPromise(spawn(command, [
722 | '-noprofile',
723 | '-noninteractive',
724 | '-nologo',
725 | '-sta',
726 | '-executionpolicy',
727 | 'unrestricted',
728 | '-windowstyle',
729 | 'hidden',
730 | '-file',
731 | scriptPath,
732 | imagePath
733 | ]))
734 | const data = (await pastePromise).toString().trim()
735 | return data
736 | } else if (platform === 'darwin') {
737 | // Mac
738 | const scriptPath = path.join(this.extension.extensionRoot, './scripts/saveclipimg-mac.applescript')
739 |
740 | const pastePromise = this.wrapProcessAsPromise(spawn('osascript', [scriptPath, imagePath]))
741 | const data = (await pastePromise).toString().trim()
742 | return data
743 | } else {
744 | // Linux
745 |
746 | const scriptPath = path.join(this.extension.extensionRoot, './scripts/saveclipimg-linux.sh')
747 |
748 | const ascript = spawn('sh', [scriptPath, imagePath])
749 | const data = (await this.wrapProcessAsPromise(ascript)).toString().trim()
750 |
751 | if (data === 'no xclip') {
752 | vscode.window.showErrorMessage('You need to install xclip command first.')
753 | return null
754 | }
755 | return data
756 | }
757 | } catch (e) {
758 | console.log(e)
759 | vscode.window.showErrorMessage(`Error occured while trying to paste image. Name: ${e.name}, Message: ${e.message}`)
760 | return null
761 | }
762 | }
763 |
764 | public renderImagePaste(basePath: string, imageFilePath: string): string {
765 | if (basePath) {
766 | imageFilePath = path.relative(basePath, imageFilePath)
767 | if (process.platform === 'win32') {
768 | imageFilePath = imageFilePath.replace(/\\/g, '/')
769 | }
770 | }
771 |
772 | const ext = path.extname(imageFilePath)
773 | const imageFilePathWithoutExt = imageFilePath.replace(/\.\w+$/, '')
774 | const fileName = path.basename(imageFilePath)
775 | const fileNameWithoutExt = path.basename(imageFilePath, ext)
776 |
777 | let result = this.pasteTemplate
778 |
779 | result = result.replace(this.PATH_VARIABLE_IMAGE_FILE_PATH, imageFilePath)
780 | result = result.replace(this.PATH_VARIABLE_IMAGE_FILE_PATH_WITHOUT_EXT, imageFilePathWithoutExt)
781 | result = result.replace(this.PATH_VARIABLE_IMAGE_FILE_NAME, fileName)
782 | result = result.replace(this.PATH_VARIABLE_IMAGE_FILE_NAME_WITHOUT_EXT, fileNameWithoutExt)
783 |
784 | return result
785 | }
786 |
787 | public replacePathVariables(
788 | pathStr: string,
789 | _projectRoot: string,
790 | curFilePath: string,
791 | postFunction: (str: string) => string = x => x
792 | ): string {
793 | const currentFileDir = path.dirname(curFilePath)
794 | const text = vscode.window.activeTextEditor?.document.getText()
795 | if (!text) {
796 | return pathStr
797 | }
798 | let graphicsPath: string | string[] = this.extension.manager.getGraphicsPath(text)
799 | graphicsPath = graphicsPath.length !== 0 ? graphicsPath[0] : this.graphicsPathFallback
800 | graphicsPath = path.resolve(currentFileDir, graphicsPath)
801 | const override = vscode.workspace.getConfiguration('latex-utilities.formattedPaste').get('imagePathOverride') as string
802 | if (override.length !== 0) {
803 | graphicsPath = override
804 | }
805 |
806 | pathStr = pathStr.replace(this.PATH_VARIABLE_GRAPHICS_PATH, postFunction(graphicsPath))
807 | pathStr = pathStr.replace(this.PATH_VARIABLE_CURRNET_FILE_DIR, postFunction(currentFileDir))
808 |
809 | return pathStr
810 | }
811 | }
812 |
--------------------------------------------------------------------------------
/src/components/typeFinder.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import { stripComments } from '../utils'
3 |
4 | interface IEnvInfo {
5 | [token: string]: {
6 | mode: 'maths' | 'text'
7 | type: 'start' | 'end'
8 | pair: string | null
9 | }
10 | }
11 |
12 | const DEBUG_CONSOLE_LOG = false
13 |
14 | let debuglog: (start: number, lines: number | string, mode: 'text' | 'maths', reason: string) => void
15 | if (DEBUG_CONSOLE_LOG) {
16 | debuglog = function (start, lines, mode, reason) {
17 | console.log(
18 | `⚪ TypeFinder took ${+new Date() - start}ms and ${lines} lines to determine: ${
19 | mode === 'text' ? '𝘁𝗲𝘅𝘁' : '𝗺𝗮𝘁𝗵𝘀'
20 | } ${reason}`
21 | )
22 | }
23 | } else {
24 | // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
25 | debuglog = (_s, _l, _m, _r) => {}
26 | }
27 |
28 | export class TypeFinder {
29 | /* eslint-disable @typescript-eslint/naming-convention */
30 | private envs: IEnvInfo = {
31 | '\\(': {
32 | mode: 'maths',
33 | type: 'start',
34 | pair: '\\)'
35 | },
36 | '\\[': {
37 | mode: 'maths',
38 | type: 'start',
39 | pair: '\\]'
40 | },
41 | '\\begin{equation}': {
42 | mode: 'maths',
43 | type: 'start',
44 | pair: '\\end{equation}'
45 | },
46 | '\\begin{displaymath}': {
47 | mode: 'maths',
48 | type: 'start',
49 | pair: '\\end{displaymath}'
50 | },
51 | '\\begin{align}': {
52 | mode: 'maths',
53 | type: 'start',
54 | pair: '\\end{align}'
55 | },
56 | '\\begin{gather}': {
57 | mode: 'maths',
58 | type: 'start',
59 | pair: '\\end{gather}'
60 | },
61 | '\\begin{flalign}': {
62 | mode: 'maths',
63 | type: 'start',
64 | pair: '\\end{flalign}'
65 | },
66 | '\\begin{multline}': {
67 | mode: 'maths',
68 | type: 'start',
69 | pair: '\\end{multline}'
70 | },
71 | '\\begin{alignat}': {
72 | mode: 'maths',
73 | type: 'start',
74 | pair: '\\end{alignat}'
75 | },
76 | '\\begin{split}': {
77 | mode: 'maths',
78 | type: 'start',
79 | pair: '\\end{split}'
80 | },
81 | '\\text': {
82 | mode: 'text',
83 | type: 'start',
84 | pair: null
85 | },
86 | '\\begin{document}': {
87 | mode: 'text',
88 | type: 'start',
89 | pair: null
90 | },
91 | '\\chapter': {
92 | mode: 'text',
93 | type: 'start',
94 | pair: null
95 | },
96 | '\\section': {
97 | mode: 'text',
98 | type: 'start',
99 | pair: null
100 | },
101 | '\\subsection': {
102 | mode: 'text',
103 | type: 'start',
104 | pair: null
105 | },
106 | '\\subsubsection': {
107 | mode: 'text',
108 | type: 'start',
109 | pair: null
110 | },
111 | '\\paragraph': {
112 | mode: 'text',
113 | type: 'start',
114 | pair: null
115 | },
116 | '\\subparagraph': {
117 | mode: 'text',
118 | type: 'start',
119 | pair: null
120 | }
121 | }
122 | /* eslint-enable @typescript-eslint/naming-convention */
123 | private allEnvRegex: RegExp
124 |
125 | constructor() {
126 | this.allEnvRegex = this.constructEnvRegexs()
127 | }
128 |
129 | private constructEnvRegexs() {
130 | function escapeRegExp(str: string) {
131 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
132 | }
133 |
134 | const regexStrs: string[] = []
135 |
136 | const properStartEnvs: string[] = []
137 | const properCloseEnvs: string[] = []
138 |
139 | const properEnvPattern = /\\begin{(\w+)}/
140 | for (const env of Object.keys(this.envs)) {
141 | const theEnv = this.envs[env]
142 | if (theEnv.type === 'end') {
143 | continue
144 | }
145 | const properEnvName = env.match(properEnvPattern)
146 | if (properEnvName) {
147 | properStartEnvs.push(properEnvName[1])
148 | this.envs[`\\begin{${properEnvName[1]}*}`] = {
149 | mode: theEnv.mode,
150 | type: 'start',
151 | pair: `\\end{${properEnvName[1]}*}`
152 | }
153 | if (theEnv.pair !== null) {
154 | properCloseEnvs.push(properEnvName[1])
155 | this.envs[`\\end{${properEnvName[1]}*}`] = {
156 | mode: theEnv.mode,
157 | type: 'end',
158 | pair: `\\begin{${properEnvName[1]}*}`
159 | }
160 | }
161 | } else {
162 | regexStrs.push(escapeRegExp(env))
163 | if (theEnv.pair !== null) {
164 | regexStrs.push(escapeRegExp(theEnv.pair))
165 | }
166 | }
167 | if (theEnv.pair !== null) {
168 | this.envs[theEnv.pair] = {
169 | mode: theEnv.mode,
170 | type: 'end',
171 | pair: env
172 | }
173 | }
174 | }
175 |
176 | regexStrs.push(`\\\\begin{(?:${properStartEnvs.join('|')})\\*?}`)
177 | regexStrs.push(`\\\\end{(?:${properCloseEnvs.join('|')})\\*?}`)
178 |
179 | return new RegExp(`(?:^|[^\\\\])(${regexStrs.join('|')})`, 'g')
180 | }
181 |
182 | public getTypeAtPosition(
183 | document: vscode.TextDocument,
184 | position: vscode.Position,
185 | lastKnown?: { position: vscode.Position, mode: 'maths' | 'text' }
186 | ): 'maths' | 'text' {
187 | const start = +new Date()
188 |
189 | let lineNo = position.line
190 | const tokenStack: string[] = []
191 |
192 | let minLine = 0
193 | let minChar = -1
194 | if (lastKnown !== undefined && lastKnown.position.isBefore(position)) {
195 | minLine = lastKnown.position.line
196 | minChar = lastKnown.position.character
197 | }
198 |
199 | do {
200 | let lineContents = document.lineAt(lineNo--).text
201 | if (lineNo + 1 === position.line) {
202 | lineContents = lineContents.substr(0, position.character + 1)
203 | }
204 | lineContents = stripComments(lineContents, '%')
205 | // treat inside a comment as text
206 | if (lineNo + 1 === position.line && position.character > lineContents.length) {
207 | debuglog(start, position.line - lineNo, 'text', 'since it\'s a comment')
208 | return 'text'
209 | }
210 |
211 | let tokens: RegExpExecArray[] = []
212 | let match: RegExpExecArray | null
213 | do {
214 | match = this.allEnvRegex.exec(lineContents)
215 | if (match !== null) {
216 | tokens.push(match)
217 | }
218 | } while (match)
219 | if (tokens.length === 0) {
220 | if (lineNo + 1 === minLine && 0 <= minChar && lastKnown) {
221 | // if last seen token closes the 'last known' environment, then we DON'T want to use it
222 | if (tokenStack.length > 0) {
223 | const lastEnv = this.envs[tokenStack[tokenStack.length - 1]]
224 | if (lastEnv.type === 'end' && lastEnv.mode === lastKnown.mode) {
225 | minLine = 0
226 | continue
227 | }
228 | }
229 | debuglog(start, position.line - lineNo, lastKnown.mode, 'using lastknown (1)')
230 | return lastKnown.mode
231 | } else {
232 | continue
233 | }
234 | } else {
235 | tokens = tokens.reverse()
236 | }
237 |
238 | let curlyBracketDepth = 0
239 |
240 | for (let i = 0; i < tokens.length; i++) {
241 | const token = tokens[i]
242 | const env = this.envs[token[1]]
243 | for (let j = lineContents.length - 1; j >= 0; j--) {
244 | if (token[1] === '\\text' && token.index === j) {
245 | if (curlyBracketDepth > 0) {
246 | debuglog(start, position.line - lineNo, env.mode, 'from \\text')
247 | return env.mode
248 | }
249 | }
250 | if (lineContents[j] === '}') {
251 | curlyBracketDepth--
252 | } else if (lineContents[j] === '{') {
253 | curlyBracketDepth++
254 | }
255 | }
256 |
257 | if (env.type === 'end') {
258 | if (env.pair === null) {
259 | debuglog(start, position.line - lineNo, env.mode, 'from env with no pair')
260 | return env.mode
261 | } else {
262 | tokenStack.push(token[1])
263 | }
264 | } else if (
265 | (tokenStack.length === 0 || tokenStack[tokenStack.length - 1] !== env.pair) &&
266 | token[1] !== '\\text'
267 | ) {
268 | debuglog(start, position.line - lineNo, env.mode, 'from unpaired env token')
269 | return env.mode
270 | } else if (tokenStack.length > 0 && token[1] !== '\\text') {
271 | // this token matches the last seen token
272 | tokenStack.pop()
273 | // if it opens the env of last known then lastKnown is suspicious
274 | // this may be unnecessarily strict, think about this later
275 | if (lastKnown && env.mode === lastKnown.mode) {
276 | continue
277 | }
278 | }
279 |
280 | // if before a last known location
281 | if (lineNo + 1 === minLine && token.index < minChar && lastKnown) {
282 | // if last seen token closes the 'last known' environment, then we DON'T want to use it
283 | if (tokenStack.length > 0) {
284 | const lastEnv = this.envs[tokenStack[tokenStack.length - 1]]
285 | if (lastEnv.type === 'end' && lastEnv.mode === lastKnown.mode) {
286 | minLine = 0
287 | continue
288 | }
289 | }
290 | debuglog(start, position.line - lineNo, lastKnown.mode, 'using lastknown (2)')
291 | return lastKnown.mode
292 | }
293 | }
294 | } while (lineNo >= minLine)
295 |
296 | debuglog(start, position.line - lineNo, 'text', 'by default')
297 | return 'text'
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/src/components/wordCounter.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as fs from 'fs'
3 | import * as path from 'path'
4 | import * as cp from 'child_process'
5 | import * as util from 'util'
6 |
7 | import { Extension } from '../main'
8 | import { hasTexId } from '../utils'
9 |
10 | interface TexCount {
11 | words: {
12 | body: number
13 | headers: number
14 | captions: number
15 | }
16 | chars: {
17 | body: number
18 | headers: number
19 | captions: number
20 | }
21 | instances: {
22 | headers: number
23 | floats: number
24 | math: {
25 | inline: number
26 | displayed: number
27 | }
28 | }
29 | }
30 |
31 | export class WordCounter {
32 | extension: Extension
33 | status: vscode.StatusBarItem
34 |
35 | constructor(extension: Extension) {
36 | this.extension = extension
37 | this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -10002)
38 | this.status.command = 'latex-utilities.selectWordcountFormat'
39 | this.setStatus()
40 | }
41 |
42 | async counts(merge = true, file = vscode.window.activeTextEditor?.document.fileName): Promise {
43 | if (file === undefined) {
44 | this.extension.logger.addLogMessage('A valid file was not give for TexCount')
45 | return
46 | }
47 | const configuration = vscode.workspace.getConfiguration('latex-utilities.countWord')
48 | const args = (configuration.get('args') as string[]).slice()
49 | const execFile = util.promisify(cp.execFile)
50 | if (merge) {
51 | args.push('-merge')
52 | }
53 | args.push('-brief')
54 | let command = configuration.get('path') as string
55 | if (configuration.get('docker.enabled')) {
56 | if (process.platform === 'win32') {
57 | command = path.resolve(this.extension.extensionRoot, './scripts/countword-win.bat')
58 | } else {
59 | command = path.resolve(this.extension.extensionRoot, './scripts/countword-linux.sh')
60 | fs.chmodSync(command, 0o755)
61 | }
62 | }
63 | this.extension.logger.addLogMessage(`TexCount args: ${args}`)
64 | let stdout; let stderr
65 | try {
66 | ({stdout, stderr} = await execFile(command, args.concat([path.basename(file)]), {
67 | cwd: path.dirname(file)
68 | }))
69 |
70 | } catch (err) {
71 | this.extension.logger.addLogMessage(`Cannot count words: ${err.message}, ${stderr}`)
72 | this.extension.logger.showErrorMessage(
73 | 'TeXCount failed. Please refer to LaTeX Utilities Output for details.'
74 | )
75 | return undefined
76 | }
77 | // just get the last line, ignoring errors
78 | const stdoutWord = stdout
79 | .replace(/\(errors:\d+\)/, '')
80 | .split('\n')
81 | .map(l => l.trim())
82 | .filter(l => l !== '')
83 | .slice(-1)[0]
84 | this.extension.logger.addLogMessage(`TeXCount output for word: ${stdout}`)
85 | args.push('-char')
86 | this.extension.logger.addLogMessage(`TexCount args: ${args}`)
87 | try {
88 | ({stdout, stderr} = await execFile(command, args.concat([path.basename(file)]), {
89 | cwd: path.dirname(file)
90 | }))
91 | } catch (err) {
92 | this.extension.logger.addLogMessage(`Cannot count words: ${err.message}, ${stderr}`)
93 | this.extension.logger.showErrorMessage(
94 | 'TeXCount failed. Please refer to LaTeX Utilities Output for details.'
95 | )
96 | return undefined
97 | }
98 | const stdoutChar = stdout
99 | .replace(/\(errors:\d+\)/, '')
100 | .split('\n')
101 | .map(l => l.trim())
102 | .filter(l => l !== '')
103 | .slice(-1)[0]
104 | this.extension.logger.addLogMessage(`TeXCount output for char: ${stdout}`)
105 | return this.parseTexCount(stdoutWord, stdoutChar)
106 | }
107 |
108 | parseTexCount(word: string, char: string): TexCount {
109 | const reMatchWord = /^(?\d+)\+(?\d+)\+(?\d+) \((?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\)/.exec(
110 | word
111 | )
112 | const reMatchChar = /^(?\d+)\+(?\d+)\+(?\d+) \((?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\)/.exec(
113 | char
114 | )
115 | if (reMatchWord !== null && reMatchChar !== null) {
116 | const {
117 | groups: {
118 | /* eslint-disable @typescript-eslint/ban-ts-comment */
119 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
120 | wordsBody,
121 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
122 | wordsHeaders,
123 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
124 | wordsCaptions,
125 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
126 | instancesHeaders,
127 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
128 | instancesFloats,
129 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
130 | mathInline,
131 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
132 | mathDisplayed
133 | /* eslint-enable @typescript-eslint/ban-ts-comment */
134 | }
135 | } = reMatchWord
136 |
137 | const {
138 | groups: {
139 | /* eslint-disable @typescript-eslint/ban-ts-comment */
140 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
141 | charsBody,
142 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
143 | charsHeaders,
144 | // @ts-ignore: ts _should_ be better with regex groups, but it isn't (yet)
145 | charsCaptions,
146 | /* eslint-enable @typescript-eslint/ban-ts-comment */
147 | }
148 | } = reMatchChar
149 |
150 | return {
151 | words: {
152 | body: parseInt(wordsBody),
153 | headers: parseInt(wordsHeaders),
154 | captions: parseInt(wordsCaptions)
155 | },
156 | chars: {
157 | body: parseInt(charsBody),
158 | headers: parseInt(charsHeaders),
159 | captions: parseInt(charsCaptions)
160 | },
161 | instances: {
162 | headers: parseInt(instancesHeaders),
163 | floats: parseInt(instancesFloats),
164 | math: {
165 | inline: parseInt(mathInline),
166 | displayed: parseInt(mathDisplayed)
167 | }
168 | }
169 | }
170 | } else {
171 | throw new Error('String was not valid TexCount output')
172 | }
173 | }
174 |
175 | async setStatus() {
176 | if (
177 | vscode.window.activeTextEditor === undefined ||
178 | !hasTexId(vscode.window.activeTextEditor.document.languageId)
179 | ) {
180 | this.status.hide()
181 | return
182 | } else {
183 | const template = vscode.workspace.getConfiguration('latex-utilities.countWord').get('format') as string
184 | if (template === '') {
185 | this.status.hide()
186 | return
187 | }
188 | const texCount = await this.counts(undefined, vscode.window.activeTextEditor.document.fileName)
189 | this.status.show()
190 | this.status.text = this.formatString(texCount, template)
191 | }
192 | }
193 |
194 | async pickFormat() {
195 | const texCount = await this.counts()
196 |
197 | const templates = [
198 | '${words} Words', '${wordsBody} Words', '${chars} Chars', '${charsBody} Chars', '${headers} Headers', '${floats} Floats', '${math} Equations', 'custom']
199 | const options: { [template: string]: string } = {}
200 | for (const template of templates) {
201 | options[template] = this.formatString(texCount, template)
202 | if (template.startsWith('${wordsBody}') || template.startsWith('${charsBody}')) {
203 | options[template] += ' (body only)'
204 | }
205 | }
206 |
207 | const choice = await vscode.window.showQuickPick(Object.values(options), {
208 | placeHolder: 'Select format to use'
209 | })
210 |
211 | let format = choice
212 | if (choice === 'custom') {
213 | const currentFormat = vscode.workspace.getConfiguration('latex-utilities.countWord').get('format') as string
214 | format = await vscode.window.showInputBox({
215 | placeHolder: 'Template',
216 | value: currentFormat,
217 | valueSelection: [0, currentFormat.length],
218 | prompt:
219 | 'The Template. Feel free to use the following placeholders: \
220 | ${wordsBody}, ${wordsHeaders}, ${wordsCaptions}, ${words}, \
221 | ${charsBody}, ${charsHeaders}, ${charsCaptions}, ${chars}, \
222 | ${headers}, ${floats}, ${mathInline}, ${mathDisplayed}, ${math}'
223 | })
224 | } else {
225 | for (const template in options) {
226 | if (options[template] === choice) {
227 | format = template
228 | break
229 | }
230 | }
231 | }
232 |
233 | if (format !== undefined) {
234 | vscode.workspace
235 | .getConfiguration('latex-utilities.countWord')
236 | .update('format', format, vscode.ConfigurationTarget.Global)
237 | .then(() => {
238 | setTimeout(() => {
239 | this.status.text = this.formatString(texCount, format as string)
240 | }, 300)
241 | })
242 | }
243 | }
244 |
245 | formatString(texCount: TexCount | undefined, template: string) {
246 | if (texCount === undefined) {
247 | return '...'
248 | }
249 | /* eslint-disable @typescript-eslint/naming-convention */
250 | const replacements: { [placeholder: string]: number } = {
251 | '${wordsBody}': texCount.words.body,
252 | '${wordsHeaders}': texCount.words.headers,
253 | '${wordsCaptions}': texCount.words.captions,
254 | '${charsBody}': texCount.chars.body,
255 | '${charsHeaders}': texCount.chars.headers,
256 | '${charsCaptions}': texCount.chars.captions,
257 | '${words}': texCount.words.body + texCount.words.headers + texCount.words.captions,
258 | '${chars}': texCount.chars.body + texCount.chars.headers + texCount.chars.captions,
259 | '${headers}': texCount.instances.headers,
260 | '${floats}': texCount.instances.floats,
261 | '${mathInline}': texCount.instances.math.inline,
262 | '${mathDisplayed}': texCount.instances.math.displayed,
263 | '${math}': texCount.instances.math.inline + texCount.instances.math.displayed
264 | }
265 | /* eslint-enable @typescript-eslint/naming-convention */
266 | for (const placeholder in replacements) {
267 | template = template.replace(placeholder, replacements[placeholder].toString())
268 | }
269 | return template
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/components/zotero.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import axios from 'axios'
3 |
4 | import { Extension } from '../main'
5 |
6 | export class Zotero {
7 | extension: Extension
8 |
9 | constructor(extension: Extension) {
10 | this.extension = extension
11 | }
12 |
13 | // Get a citation via the Zotero Cite as you Write popup
14 | private async caywCite() {
15 | const configuration = vscode.workspace.getConfiguration('latex-utilities.zotero')
16 |
17 | const zoteroUrl = configuration.get('zoteroUrl') as string
18 |
19 | const options = {
20 | format: 'biblatex',
21 | command: configuration.get('latexCommand')
22 | }
23 |
24 | try {
25 | const res = await axios.get(`${zoteroUrl}/better-bibtex/cayw`, {
26 | params: options
27 | })
28 |
29 | return res.data
30 | } catch (error) {
31 | if (error.code === 'ECONNREFUSED') {
32 | this.extension.logger.showErrorMessage(
33 | 'Could not connect to Zotero. Is it running with the Better BibTeX extension installed?'
34 | )
35 | } else {
36 | this.extension.logger.addLogMessage(`Cannot insert citation: ${error.message}`)
37 | this.extension.logger.showErrorMessage(
38 | 'Cite as you write failed. Please refer to LaTeX Utilities Output for details.'
39 | )
40 | }
41 |
42 | return null
43 | }
44 | }
45 |
46 | // Search the Zotero library for entries matching `terms`.
47 | // Returns a promise for search results and a function to cancel the search
48 | private search(terms: string): [Promise, () => void] {
49 | const configuration = vscode.workspace.getConfiguration('latex-utilities.zotero')
50 | const zoteroUrl = configuration.get('zoteroUrl') as string
51 |
52 | this.extension.logger.addLogMessage(`Searching Zotero for "${terms}"`)
53 | const CancelToken = axios.CancelToken
54 | const source = CancelToken.source()
55 | const req = axios(`${zoteroUrl}/better-bibtex/json-rpc`, {
56 | method: 'post',
57 | data: {
58 | jsonrpc: '2.0',
59 | method: 'item.search',
60 | params: [
61 | [['ignore_feeds'], ['quicksearch-titleCreatorYear', 'contains', terms]]
62 | ],
63 | },
64 | responseType: 'json',
65 | cancelToken: source.token
66 | })
67 |
68 | return [
69 | req.then(response => {
70 | console.log(response)
71 | const results = response.data.result as SearchResult[]
72 | this.extension.logger.addLogMessage(`Got ${results.length} search results from Zotero for "${terms}"`)
73 | return results
74 | }).catch(error => {
75 | this.extension.logger.showErrorMessage(`Searching Zotero failed: ${error.message}`)
76 | throw error
77 | }),
78 | () => source.cancel('request canceled')
79 | ]
80 | }
81 |
82 | // Get a citation from a built-in quick picker
83 | private async vscodeCite() {
84 | const disposables: vscode.Disposable[] = []
85 |
86 | try {
87 | const entries = await new Promise((resolve, _) => {
88 | const input = vscode.window.createQuickPick()
89 | input.matchOnDescription = true
90 | input.matchOnDetail = true
91 | input.canSelectMany = true
92 | input.placeholder = 'Type to insert citations'
93 |
94 | let cancel: (() => void) | undefined
95 |
96 | disposables.push(
97 | input.onDidChangeValue((value: any) => {
98 | if (value) {
99 | this.extension.logger.addLogMessage(`${input.busy}`)
100 | input.busy = true
101 |
102 | if (cancel) {
103 | cancel()
104 | cancel = undefined
105 | }
106 |
107 | const [r, c] = this.search(value)
108 | cancel = c
109 | r.then(results => {
110 | console.log('results')
111 | input.items = results.map(result => new EntryItem(result))
112 | input.busy = false
113 | })
114 | .catch(error => {
115 | if (!error.isCanceled) {
116 | if (error.code === 'ECONNREFUSED') {
117 | this.extension.logger.showErrorMessage(
118 | 'Could not connect to Zotero. Is it running with the Better BibTeX extension installed?'
119 | )
120 | } else {
121 | this.extension.logger.addLogMessage(
122 | `Searching Zotero failed: ${error.message}`
123 | )
124 | input.items = [new ErrorItem(error)]
125 | }
126 | }
127 | })
128 | .finally(() => {
129 | })
130 | } else {
131 | input.items = []
132 | }
133 | })
134 | )
135 |
136 | disposables.push(
137 | input.onDidAccept(() => {
138 | const items = input.selectedItems.length > 0 ? input.selectedItems : input.activeItems
139 | input.hide()
140 | resolve(items.filter((i: any) => i instanceof EntryItem).map((i: any) => (i as EntryItem).result))
141 | })
142 | )
143 |
144 | disposables.push(
145 | input.onDidHide(() => {
146 | if (cancel) {
147 | cancel()
148 | }
149 |
150 | resolve([])
151 | input.dispose()
152 | })
153 | )
154 |
155 | input.show()
156 | })
157 |
158 | if (entries && entries.length > 0) {
159 | const configuration = vscode.workspace.getConfiguration('latex-utilities.zotero')
160 | const latexCommand = configuration.get('latexCommand') as string
161 |
162 | const keys = entries.map(e => e.citekey).join(',')
163 | return latexCommand.length > 0 ?`\\${latexCommand}{${keys}}` : `${keys}`
164 | } else {
165 | return null
166 | }
167 | } finally {
168 | disposables.forEach(d => d.dispose())
169 | }
170 | }
171 |
172 | private async insertCitation(citation: string) {
173 | const editor = vscode.window.activeTextEditor
174 | if (editor) {
175 | await editor.edit((edit: any) => {
176 | if (editor.selection.isEmpty) {
177 | edit.insert(editor.selection.active, citation)
178 | } else {
179 | edit.delete(editor.selection)
180 | edit.insert(editor.selection.start, citation)
181 | }
182 | })
183 |
184 | this.extension.logger.addLogMessage(`Added citation: ${citation}`)
185 | } else {
186 | this.extension.logger.addLogMessage('Could not insert citation: no active text editor')
187 | }
188 | }
189 |
190 | async cite() {
191 | const configuration = vscode.workspace.getConfiguration('latex-utilities.zotero')
192 | const citeMethod = configuration.get('citeMethod')
193 |
194 | if (!(await this.checkZotero())) {
195 | return
196 | }
197 |
198 | let citation = null
199 | if (citeMethod === 'zotero') {
200 | citation = await this.caywCite()
201 | } else if (citeMethod === 'vscode') {
202 | citation = await this.vscodeCite()
203 | } else {
204 | this.extension.logger.showErrorMessage(`Unknown cite method: ${citeMethod}`)
205 | }
206 |
207 | if (citation) {
208 | this.insertCitation(citation)
209 | }
210 |
211 | this.extension.telemetryReporter.sendTelemetryEvent('zoteroCite')
212 | }
213 |
214 | private extractCiteKey(editor: vscode.TextEditor) {
215 | if (editor.selection.isEmpty) {
216 | const range = editor.document.getWordRangeAtPosition(editor.selection.active)
217 | return editor.document.getText(range)
218 | } else {
219 | return editor.document.getText(new vscode.Range(editor.selection.start, editor.selection.end))
220 | }
221 | }
222 |
223 | async openCitation() {
224 | if (!(await this.checkZotero())) {
225 | return
226 | }
227 |
228 | const editor = vscode.window.activeTextEditor
229 | if (!editor) {
230 | return
231 | }
232 |
233 | const citeKey = this.extractCiteKey(editor)
234 | this.extension.logger.addLogMessage(`Opening ${citeKey} in Zotero`)
235 |
236 | const uri = vscode.Uri.parse(`zotero://select/items/bbt:${citeKey}`)
237 | await vscode.env.openExternal(uri)
238 | }
239 |
240 | private async checkZotero() {
241 | const configuration = vscode.workspace.getConfiguration('latex-utilities.zotero')
242 | const zoteroUrl = configuration.get('zoteroUrl') as string
243 |
244 | try {
245 | await axios.get(`${zoteroUrl}/connector/ping`)
246 | return true
247 | } catch (e) {
248 | if (e.code === 'ECONNREFUSED') {
249 | vscode.window.showWarningMessage('Zotero doesn\'t appear to be running.')
250 | return false
251 | }
252 | }
253 | return false
254 | }
255 | }
256 |
257 | // Better BibTeX search result
258 | interface SearchResult {
259 | type: string
260 | citekey: string
261 | title: string
262 | author?: [{ family: string, given: string }]
263 | [field: string]: any
264 | }
265 |
266 | class EntryItem implements vscode.QuickPickItem {
267 | label: string
268 | detail: string
269 | description: string
270 |
271 | constructor(public result: SearchResult) {
272 | this.label = result.title
273 | this.detail = result.citekey
274 |
275 | if (result.author) {
276 | const names = result.author.map(a => `${a.given} ${a.family}`)
277 |
278 | if (names.length < 2) {
279 | this.description = names.join(' ')
280 | } else if (names.length === 2) {
281 | this.description = names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1]
282 | } else {
283 | this.description = names.slice(0, -1).join(', ') + ', and ' + names[names.length - 1]
284 | }
285 | } else {
286 | this.description = ''
287 | }
288 | }
289 | }
290 |
291 | class ErrorItem implements vscode.QuickPickItem {
292 | label: string
293 |
294 | constructor(public message: string) {
295 | this.label = message.replace(/\r?\n/g, ' ')
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as path from 'path'
3 | import * as fs from 'fs'
4 |
5 | import { Logger } from './components/logger'
6 | import { CompletionWatcher, Completer } from './components/completionWatcher'
7 | import { Paster } from './components/paster'
8 | import { WordCounter } from './components/wordCounter'
9 | import { MacroDefinitions } from './providers/macroDefinitions'
10 | import { Zotero } from './components/zotero'
11 |
12 | import TelemetryReporter from 'vscode-extension-telemetry'
13 | import { Manager } from './workshop/manager'
14 |
15 | let extension: Extension
16 |
17 | export function activate(context: vscode.ExtensionContext) {
18 | extension = new Extension()
19 |
20 | extension.logger.addLogMessage('LaTeX Utilities Started')
21 |
22 | context.subscriptions.push(
23 | vscode.commands.registerCommand('latex-utilities.loadPlugin', () =>
24 | vscode.window.showInformationMessage(
25 | 'LaTeX Utilities loaded'
26 | )
27 | ),
28 | vscode.commands.registerCommand('latex-utilities.editLiveSnippetsFile', () =>
29 | extension.withTelemetry('editLiveSnippetsFile', () => {
30 | extension.completionWatcher.editSnippetsFile()
31 | })
32 | ),
33 | vscode.commands.registerCommand('latex-utilities.formattedPaste', () =>
34 | extension.withTelemetry('formattedPaste', () => {
35 | extension.paster.paste()
36 | })
37 | ),
38 | vscode.commands.registerCommand('latex-utilities.citeZotero', () =>
39 | extension.withTelemetry('citeZotero', () => {
40 | extension.zotero.cite()
41 | })
42 | ),
43 | vscode.commands.registerCommand('latex-utilities.openInZotero', () =>
44 | extension.withTelemetry('openInZotero', () => {
45 | extension.zotero.openCitation()
46 | })
47 | ),
48 | vscode.commands.registerCommand('latex-utilities.selectWordcountFormat', () =>
49 | extension.withTelemetry('selectWordcountFormat', () => {
50 | extension.wordCounter.pickFormat()
51 | })
52 | ),
53 | )
54 |
55 | context.subscriptions.push(
56 | vscode.workspace.onDidChangeTextDocument(
57 | (e: vscode.TextDocumentChangeEvent) => {
58 | extension.withTelemetry('onDidChangeTextDocument', () => extension.completionWatcher.watcher(e), false)
59 | }
60 | ),
61 | vscode.workspace.onDidSaveTextDocument(() => {
62 | extension.withTelemetry('onDidSaveTextDocument_tex_wordcounter', () => extension.wordCounter.setStatus())
63 | }),
64 | vscode.window.onDidChangeActiveTextEditor((_e: vscode.TextEditor | undefined) => {
65 | extension.withTelemetry('onDidChangeActiveTextEditor_tex_wordcounter', () => extension.wordCounter.setStatus())
66 | })
67 | )
68 |
69 | context.subscriptions.push(
70 | vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'tex' }, extension.completer),
71 | vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'latex' }, extension.completer),
72 | vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'doctex' }, extension.completer),
73 | vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'rsweave' }, extension.completer),
74 | vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'jlweave' }, extension.completer),
75 | vscode.languages.registerDefinitionProvider(
76 | { language: 'latex', scheme: 'file' },
77 | new MacroDefinitions(extension)
78 | )
79 | )
80 |
81 | newVersionMessage(context.extensionPath)
82 | context.subscriptions.push(extension.telemetryReporter)
83 | }
84 |
85 | export function deactivate() {
86 | extension.telemetryReporter.dispose()
87 | }
88 |
89 | function newVersionMessage(extensionPath: string) {
90 | fs.readFile(`${extensionPath}${path.sep}package.json`, (err, data) => {
91 | if (err) {
92 | extension.logger.addLogMessage('Cannot read package information.')
93 | return
94 | }
95 | extension.packageInfo = JSON.parse(data.toString())
96 | extension.logger.addLogMessage(`LaTeX Utilities version: ${extension.packageInfo.version}`)
97 | if (
98 | fs.existsSync(`${extensionPath}${path.sep}VERSION`) &&
99 | fs.readFileSync(`${extensionPath}${path.sep}VERSION`).toString() === extension.packageInfo.version
100 | ) {
101 | return
102 | }
103 | fs.writeFileSync(`${extensionPath}${path.sep}VERSION`, extension.packageInfo.version)
104 | const configuration = vscode.workspace.getConfiguration('latex-utilities')
105 | if (!(configuration.get('message.update.show') as boolean)) {
106 | return
107 | }
108 | vscode.window
109 | .showInformationMessage(
110 | `LaTeX Utilities updated to version ${extension.packageInfo.version}.`,
111 | 'Change log',
112 | 'Star the project',
113 | 'Disable this message forever'
114 | )
115 | .then(option => {
116 | switch (option) {
117 | case 'Change log':
118 | vscode.commands.executeCommand(
119 | 'markdown.showPreview',
120 | vscode.Uri.file(`${extensionPath}${path.sep}CHANGELOG.md`)
121 | )
122 | break
123 | case 'Star the project':
124 | vscode.commands.executeCommand(
125 | 'vscode.open',
126 | vscode.Uri.parse('https://github.com/tecosaur/LaTeX-Utilities/')
127 | )
128 | break
129 | case 'Disable this message forever':
130 | configuration.update('message.update.show', false, true)
131 | break
132 | default:
133 | break
134 | }
135 | })
136 | })
137 | }
138 |
139 | export class Extension {
140 | extensionRoot: string
141 | packageInfo: any
142 | telemetryReporter: TelemetryReporter
143 | // workshop: LaTeXWorkshopAPI
144 | logger: Logger
145 | completionWatcher: CompletionWatcher
146 | completer: Completer
147 | paster: Paster
148 | wordCounter: WordCounter
149 | zotero: Zotero
150 | manager: Manager
151 |
152 | constructor() {
153 | this.extensionRoot = path.resolve(`${__dirname}/../`)
154 | const self = vscode.extensions.getExtension('tecosaur.latex-utilities') as vscode.Extension
155 | this.telemetryReporter = new TelemetryReporter(
156 | 'tecosaur.latex-utilities',
157 | self.packageJSON.version,
158 | '11a955d7-02dc-4c1a-85e4-053858f88af0'
159 | )
160 | // const workshop = vscode.extensions.getExtension('james-yu.latex-workshop') as vscode.Extension
161 | // this.workshop = workshop.exports
162 | // if (workshop.isActive === false) {
163 | // workshop.activate().then(() => (this.workshop = workshop.exports))
164 | // }
165 | this.logger = new Logger(this)
166 | this.completionWatcher = new CompletionWatcher(this)
167 | this.completer = new Completer(this)
168 | this.paster = new Paster(this)
169 | this.wordCounter = new WordCounter(this)
170 | this.zotero = new Zotero(this)
171 | this.manager = new Manager(this)
172 | }
173 |
174 | withTelemetry(command: string, callback: () => void, log: boolean = true) {
175 | if (log){
176 | this.logger.addLogMessage('withTelemetry: ' + command)
177 | }
178 | try {
179 | callback()
180 | } catch (error) {
181 | this.logger.addLogMessage(error)
182 | this.telemetryReporter.sendTelemetryException(error, {
183 | command
184 | })
185 | this.logger.addLogMessage('Error reported.')
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/providers/macroDefinitions.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import { Extension } from '../main'
3 | import { checkCommandExists } from '../utils'
4 | import { spawn } from 'child_process'
5 |
6 | export class MacroDefinitions implements vscode.DefinitionProvider {
7 | extension: Extension
8 |
9 | constructor(extension: Extension) {
10 | this.extension = extension
11 | }
12 |
13 | async provideDefinition(
14 | document: vscode.TextDocument,
15 | position: vscode.Position,
16 | _token: vscode.CancellationToken
17 | ) {
18 | try {
19 | const enabled = vscode.workspace.getConfiguration('latex-utilities.texdef').get('enabled')
20 | if (!enabled) {
21 | return
22 | }
23 |
24 | const line = document.lineAt(position.line)
25 | let command: vscode.Range | undefined
26 |
27 | const pattern = /\\[\w@]+/g
28 | let match = pattern.exec(line.text)
29 | while (match !== null) {
30 | const matchStart = line.range.start.translate(0, match.index)
31 | const matchEnd = matchStart.translate(0, match[0].length)
32 | const matchRange = new vscode.Range(matchStart, matchEnd)
33 |
34 | if (matchRange.contains(position)) {
35 | command = matchRange
36 | break
37 | }
38 | match = pattern.exec(line.text)
39 | }
40 |
41 | if (command === undefined) {
42 | return
43 | }
44 |
45 | checkCommandExists('texdef')
46 |
47 | const texdefOptions = ['--source', '--Find', '--tex', 'latex']
48 | const packages = this.extension.manager.usedPackages(document)
49 | if (/\.sty$/.test(document.uri.fsPath)) {
50 | texdefOptions.push(document.uri.fsPath.replace(/\.sty$/, ''))
51 | }
52 | texdefOptions.push(...[...packages].map(p => ['-p', p]).reduce((prev, next) => prev.concat(next), []))
53 | const documentClass = this.getDocumentClass(document)
54 | texdefOptions.push('--class', documentClass !== null ? documentClass : 'article')
55 | texdefOptions.push(document.getText(command))
56 |
57 | const texdefResult = await this.getFirstLineOfOutput('texdef', texdefOptions)
58 |
59 | const resultPattern = /% (.+), line (\d+):/
60 | let result: RegExpMatchArray | null
61 | if ((result = texdefResult.match(resultPattern)) !== null) {
62 | this.extension.telemetryReporter.sendTelemetryEvent('texdef')
63 | return new vscode.Location(vscode.Uri.file(result[1]), new vscode.Position(parseInt(result[2]) - 1, 0))
64 | } else {
65 | vscode.window.showWarningMessage(`Could not find definition for ${document.getText(command)}`)
66 | this.extension.logger.addLogMessage(`Could not find definition for ${document.getText(command)}`)
67 | return
68 | }
69 | } catch (error) {
70 | this.extension.logger.addLogMessage(error)
71 | this.extension.telemetryReporter.sendTelemetryException(error, {
72 | 'command': 'MacroDefinitions.provideDefinition',
73 | })
74 | this.extension.logger.addLogMessage('Error reported.')
75 | }
76 | }
77 |
78 | private getDocumentClass(document: vscode.TextDocument): string | null {
79 | const documentClassPattern = /\\documentclass((?:\[[\w-,]*\])?{[\w-]+)}/
80 | let documentClass: RegExpMatchArray | null
81 | let line = 0
82 | while (line < 50 && line < document.lineCount) {
83 | const lineContents = document.lineAt(line++).text
84 | if ((documentClass = lineContents.match(documentClassPattern)) !== null) {
85 | return documentClass[1].replace(/{([\w-]+)$/, '$1')
86 | }
87 | }
88 | return null
89 | }
90 |
91 | private async getFirstLineOfOutput(command: string, options: string[]): Promise {
92 | return new Promise(resolve => {
93 | const startTime = +new Date()
94 | this.extension.logger.addLogMessage(`Running command ${command} ${options.join(' ')}`)
95 | try {
96 | const cmdProcess = spawn(command, options)
97 |
98 | cmdProcess.stdout.on('data', data => {
99 | this.extension.logger.addLogMessage(
100 | `Took ${+new Date() - startTime}ms to find definition for ${options[options.length - 1]}`
101 | )
102 | cmdProcess.kill()
103 | resolve(data.toString())
104 | })
105 | cmdProcess.stdout.on('error', () => {
106 | this.extension.logger.addLogMessage(`Error running texdef for ${options[options.length - 1]}}`)
107 | resolve('')
108 | })
109 | cmdProcess.stdout.on('end', () => {
110 | resolve('')
111 | })
112 | setTimeout(() => {
113 | cmdProcess.kill()
114 | }, 6000)
115 | } catch (error) {
116 | this.extension.logger.showErrorMessage(`Got ${error} while running texdef. Is texdef installed?`)
117 | }
118 | })
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import { execSync } from 'child_process'
3 |
4 | /**
5 | * Remove the comments if any
6 | */
7 | export function stripComments(text: string, commentSign: string): string {
8 | const pattern = '([^\\\\]|^)' + commentSign + '.*$'
9 | const reg = RegExp(pattern, 'gm')
10 | return text.replace(reg, '$1')
11 | }
12 |
13 | /**
14 | * @param id document languageId
15 | */
16 | export function hasTexId(id: string) {
17 | return id === 'tex' || id === 'latex' || id === 'doctex' || id === 'rsweave' || id === 'jlweave'
18 | }
19 |
20 | export function checkCommandExists(command: string) {
21 | try {
22 | execSync(`${command} --version`)
23 | } catch (error) {
24 | if (error.status === 127) {
25 | vscode.window.showErrorMessage(`Command ${command} not found`)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/workshop/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 James Yu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/workshop/finderutils.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as path from 'path'
3 | import * as utils from './utils'
4 | import * as fs from 'fs'
5 |
6 | import type {Extension} from '../main'
7 |
8 | export class FinderUtils {
9 | private readonly extension: Extension
10 |
11 | readFileSyncGracefully(filepath: string): string | undefined {
12 | try {
13 | const ret = fs.readFileSync(filepath).toString()
14 | return ret
15 | } catch (err) {
16 | if (err instanceof Error) {
17 | this.extension.logger.logError(err)
18 | }
19 | return undefined
20 | }
21 | }
22 |
23 | constructor(extension: Extension) {
24 | this.extension = extension
25 | }
26 |
27 | findRootFromMagic(): string | undefined {
28 | if (!vscode.window.activeTextEditor) {
29 | return undefined
30 | }
31 | const regex = /^(?:%\s*!\s*T[Ee]X\sroot\s*=\s*(.*\.(?:tex|[jrsRS]nw|[rR]tex|jtexw))$)/m
32 | let content: string | undefined = vscode.window.activeTextEditor.document.getText()
33 |
34 | let result = content.match(regex)
35 | const fileStack: string[] = []
36 | if (result) {
37 | let file = path.resolve(path.dirname(vscode.window.activeTextEditor.document.fileName), result[1])
38 | content = this.readFileSyncGracefully(file)
39 | if (content === undefined) {
40 | const msg = `Not found root file specified in the magic comment: ${file}`
41 | this.extension.logger.addLogMessage(msg)
42 | throw new Error(msg)
43 | }
44 | fileStack.push(file)
45 | this.extension.logger.addLogMessage(`Found root file by magic comment: ${file}`)
46 |
47 | result = content.match(regex)
48 | while (result) {
49 | file = path.resolve(path.dirname(file), result[1])
50 | if (fileStack.includes(file)) {
51 | this.extension.logger.addLogMessage(`Looped root file by magic comment found: ${file}, stop here.`)
52 | return file
53 | } else {
54 | fileStack.push(file)
55 | this.extension.logger.addLogMessage(`Recursively found root file by magic comment: ${file}`)
56 | }
57 |
58 | content = this.readFileSyncGracefully(file)
59 | if (content === undefined) {
60 | const msg = `Not found root file specified in the magic comment: ${file}`
61 | this.extension.logger.addLogMessage(msg)
62 | throw new Error(msg)
63 |
64 | }
65 | result = content.match(regex)
66 | }
67 | return file
68 | }
69 | return undefined
70 | }
71 |
72 | findSubFiles(content: string): string | undefined {
73 | if (!vscode.window.activeTextEditor) {
74 | return undefined
75 | }
76 | const regex = /(?:\\documentclass\[(.*)\]{subfiles})/
77 | const result = content.match(regex)
78 | if (result) {
79 | const file = utils.resolveFile([path.dirname(vscode.window.activeTextEditor.document.fileName)], result[1])
80 | if (file) {
81 | this.extension.logger.addLogMessage(`Found root file of this subfile from active editor: ${file}`)
82 | } else {
83 | this.extension.logger.addLogMessage(`Cannot find root file of this subfile from active editor: ${result[1]}`)
84 | }
85 | return file
86 | }
87 | return undefined
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/workshop/manager.ts:
--------------------------------------------------------------------------------
1 | // from James-Yu/LaTeX-Workshop
2 |
3 | import { Extension } from '../main'
4 | import * as vscode from 'vscode'
5 | import * as path from 'path'
6 | import * as fs from 'fs'
7 | import * as utils from './utils'
8 | import * as tmp from 'tmp'
9 | import {FinderUtils} from './finderutils'
10 | import type {MatchPath} from './pathutils'
11 | import {PathUtils, PathRegExp} from './pathutils'
12 |
13 |
14 | /**
15 | * The content cache for each LaTeX file `filepath`.
16 | */
17 | interface Content {
18 | [filepath: string]: { // The path of a LaTeX file.
19 | /**
20 | * The dirty (under editing) content of the LaTeX file if opened in vscode,
21 | * the content on disk otherwise.
22 | */
23 | content: string | undefined
24 | /**
25 | * The sub-files of the LaTeX file. They should be tex or plain files.
26 | */
27 | children: {
28 | /**
29 | * The index of character sub-content is inserted
30 | */
31 | index: number
32 | /**
33 | * The path of the sub-file
34 | */
35 | file: string
36 | }[]
37 | /**
38 | * The array of the paths of `.bib` files referenced from the LaTeX file.
39 | */
40 | bibs: string[]
41 | }
42 | }
43 |
44 |
45 | type RootFileType = {
46 | type: 'filePath'
47 | filePath: string
48 | } | {
49 | type: 'uri'
50 | uri: vscode.Uri
51 | }
52 |
53 | export class Manager {
54 | /**
55 | * The content cache for each LaTeX file.
56 | */
57 | private readonly cachedContent = Object.create(null) as Content
58 |
59 | private readonly localRootFiles = Object.create(null) as { [key: string]: string | undefined }
60 | private readonly rootFilesLanguageIds = Object.create(null) as { [key: string]: string | undefined }
61 | // Store one root file for each workspace.
62 | private readonly rootFiles = Object.create(null) as { [key: string]: RootFileType | undefined }
63 | private workspaceRootDirUri = ''
64 |
65 | private readonly extension: Extension
66 | private readonly rsweaveExt: string[] = ['.rnw', '.Rnw', '.rtex', '.Rtex', '.snw', '.Snw']
67 | private readonly jlweaveExt: string[] = ['.jnw', '.jtexw']
68 | private readonly finderUtils: FinderUtils
69 | private readonly pathUtils: PathUtils
70 | private tmpDir: string
71 |
72 | constructor(extension: Extension) {
73 | this.extension = extension
74 | this.finderUtils = new FinderUtils(extension)
75 | this.pathUtils = new PathUtils(extension)
76 | this.tmpDir = tmp.dirSync({unsafeCleanup: true}).name.split(path.sep).join('/')
77 | }
78 |
79 | /**
80 | * Returns the output directory developed according to the input tex path
81 | * and 'latex.outDir' config. If `texPath` is `undefined`, the default root
82 | * file is used. If there is not root file, returns './'.
83 | * The returned path always uses `/` even on Windows.
84 | *
85 | * @param texPath The path of a LaTeX file.
86 | */
87 | getOutDir(texPath?: string) {
88 | if (texPath === undefined) {
89 | texPath = this.rootFile
90 | }
91 | // rootFile is also undefined
92 | if (texPath === undefined) {
93 | return './'
94 | }
95 |
96 | const configuration = vscode.workspace.getConfiguration('latex-workshop')
97 | const outDir = configuration.get('latex.outDir') as string
98 | const out = utils.replaceArgumentPlaceholders(texPath, this.tmpDir)(outDir)
99 | return path.normalize(out).split(path.sep).join('/')
100 | }
101 |
102 |
103 | /**
104 | * The path of the directory of the root file.
105 | */
106 | get rootDir() {
107 | return this.rootFile ? path.dirname(this.rootFile) : undefined
108 | }
109 |
110 | /**
111 | * The path of the root LaTeX file of the current workspace.
112 | * It is `undefined` before `findRoot` called.
113 | */
114 | get rootFile(): string | undefined {
115 | const ret = this.rootFiles[this.workspaceRootDirUri]
116 | if (ret) {
117 | if (ret.type === 'filePath') {
118 | return ret.filePath
119 | } else {
120 | if (ret.uri.scheme === 'file') {
121 | return ret.uri.fsPath
122 | } else {
123 | this.extension.logger.addLogMessage(`The file cannot be used as the root file: ${ret.uri.toString(true)}`)
124 | return
125 | }
126 | }
127 | } else {
128 | return
129 | }
130 | }
131 |
132 | set rootFile(root: string | undefined) {
133 | if (root) {
134 | this.rootFiles[this.workspaceRootDirUri] = { type: 'filePath', filePath: root }
135 | } else {
136 | this.rootFiles[this.workspaceRootDirUri] = undefined
137 | }
138 | }
139 |
140 | get rootFileUri(): vscode.Uri | undefined {
141 | const root = this.rootFiles[this.workspaceRootDirUri]
142 | if (root) {
143 | if (root.type === 'filePath') {
144 | return vscode.Uri.file(root.filePath)
145 | } else {
146 | return root.uri
147 | }
148 | } else {
149 | return
150 | }
151 | }
152 |
153 | set rootFileUri(root: vscode.Uri | undefined) {
154 | let rootFile: RootFileType | undefined
155 | if (root) {
156 | if (root.scheme === 'file') {
157 | rootFile = { type: 'filePath', filePath: root.fsPath }
158 | } else {
159 | rootFile = { type: 'uri', uri: root }
160 | }
161 | }
162 | this.rootFiles[this.workspaceRootDirUri] = rootFile
163 | }
164 |
165 | get localRootFile() {
166 | return this.localRootFiles[this.workspaceRootDirUri]
167 | }
168 |
169 | set localRootFile(localRoot: string | undefined) {
170 | this.localRootFiles[this.workspaceRootDirUri] = localRoot
171 | }
172 |
173 | get rootFileLanguageId() {
174 | return this.rootFilesLanguageIds[this.workspaceRootDirUri]
175 | }
176 |
177 | set rootFileLanguageId(id: string | undefined) {
178 | this.rootFilesLanguageIds[this.workspaceRootDirUri] = id
179 | }
180 |
181 | /**
182 | * Return a string array which holds all imported tex files
183 | * from the given `file` including the `file` itself.
184 | * If `file` is `undefined`, trace from the * root file,
185 | * or return empty array if the root file is `undefined`
186 | *
187 | * @param file The path of a LaTeX file
188 | */
189 | getIncludedTeX(file?: string, includedTeX: string[] = []): string[] {
190 | if (file === undefined) {
191 | file = this.rootFile
192 | }
193 | if (file === undefined) {
194 | return []
195 | }
196 | if (!(file in this.extension.manager.cachedContent)) {
197 | return []
198 | }
199 | includedTeX.push(file)
200 | for (const child of this.extension.manager.cachedContent[file].children) {
201 | if (includedTeX.includes(child.file)) {
202 | // Already included
203 | continue
204 | }
205 | this.getIncludedTeX(child.file, includedTeX)
206 | }
207 | return includedTeX
208 | }
209 |
210 | usedPackages(document: vscode.TextDocument) {
211 | // slower but will do the work for now
212 | const text = document.getText()
213 | const allPkgs: Set = new Set()
214 | // use regex to find all \usepackage{}
215 | const pkgs = text.match(/\\usepackage\{(.*?)\}/g)
216 | if (pkgs) {
217 | for (const pkg of pkgs) {
218 | const pkgName = pkg.replace(/\\usepackage\{(.*?)\}/, '$1')
219 | allPkgs.add(pkgName)
220 | }
221 | this.extension.logger.addLogMessage(`${pkgs}`)
222 | }
223 | return allPkgs
224 | }
225 |
226 | getGraphicsPath(content: string): string[] {
227 | const graphicsPath: string[] = []
228 | const regex = /\\graphicspath{[\s\n]*((?:{[^{}]*}[\s\n]*)*)}/g
229 | const noVerbContent = utils.stripCommentsAndVerbatim(content)
230 | let result: string[] | null
231 | do {
232 | result = regex.exec(noVerbContent)
233 | if (result) {
234 | for (const dir of result[1].split(/\{|\}/).filter(s => s.replace(/^\s*$/, ''))) {
235 | if (graphicsPath.includes(dir)) {
236 | continue
237 | }
238 | graphicsPath.push(dir)
239 | }
240 | }
241 | } while (result)
242 | return graphicsPath
243 | }
244 |
245 | getCachedContent(filePath: string): Content[string] | undefined {
246 | return this.cachedContent[filePath]
247 | }
248 |
249 | private inferLanguageId(filename: string): string | undefined {
250 | const ext = path.extname(filename).toLocaleLowerCase()
251 | if (ext === '.tex') {
252 | return 'latex'
253 | } else if (this.jlweaveExt.includes(ext)) {
254 | return 'jlweave'
255 | } else if (this.rsweaveExt.includes(ext)) {
256 | return 'rsweave'
257 | } else {
258 | return undefined
259 | }
260 | }
261 |
262 | /**
263 | * Returns `true` if the language of `id` is one of supported languages.
264 | *
265 | * @param id The identifier of language.
266 | */
267 | hasTexId(id: string) {
268 | return ['tex', 'latex', 'latex-expl3', 'doctex', 'jlweave', 'rsweave'].includes(id)
269 | }
270 |
271 | private findWorkspace() {
272 | const firstDir = vscode.workspace.workspaceFolders?.[0]
273 | // If no workspace is opened.
274 | if (!firstDir) {
275 | this.workspaceRootDirUri = ''
276 | return
277 | }
278 | // If we don't have an active text editor, we can only make a guess.
279 | // Let's guess the first one.
280 | if (!vscode.window.activeTextEditor) {
281 | this.workspaceRootDirUri = firstDir.uri.toString(true)
282 | return
283 | }
284 | // Get the workspace folder which contains the active document.
285 | const activeFileUri = vscode.window.activeTextEditor.document.uri
286 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeFileUri)
287 | if (workspaceFolder) {
288 | this.workspaceRootDirUri = workspaceFolder.uri.toString(true)
289 | return
290 | }
291 | // Guess that the first workspace is the chosen one.
292 | this.workspaceRootDirUri = firstDir.uri.toString(true)
293 | }
294 |
295 | /**
296 | * Finds the root file with respect to the current workspace and returns it.
297 | * The found root is also set to `rootFile`.
298 | */
299 | async findRoot(): Promise {
300 | this.findWorkspace()
301 | const wsfolders = vscode.workspace.workspaceFolders?.map(e => e.uri.toString(true))
302 | this.extension.logger.addLogMessage(`Current workspace folders: ${JSON.stringify(wsfolders)}`)
303 | this.extension.logger.addLogMessage(`Current workspaceRootDir: ${this.workspaceRootDirUri}`)
304 | this.localRootFile = undefined
305 | const findMethods = [
306 | () => {
307 | if (!vscode.window.activeTextEditor) {
308 | return undefined
309 | }
310 | const regex = /^(?:%\s*!\s*T[Ee]X\sroot\s*=\s*(.*\.(?:tex|[jrsRS]nw|[rR]tex|jtexw))$)/m
311 | let content: string | undefined = vscode.window.activeTextEditor.document.getText()
312 |
313 | let result = content.match(regex)
314 | const fileStack: string[] = []
315 | if (result) {
316 | let file = path.resolve(path.dirname(vscode.window.activeTextEditor.document.fileName), result[1])
317 | content = fs.readFileSync(file).toString()
318 | if (content === undefined) {
319 | const msg = `Not found root file specified in the magic comment: ${file}`
320 | this.extension.logger.addLogMessage(msg)
321 | throw new Error(msg)
322 | }
323 | fileStack.push(file)
324 | this.extension.logger.addLogMessage(`Found root file by magic comment: ${file}`)
325 |
326 | result = content.match(regex)
327 | while (result) {
328 | file = path.resolve(path.dirname(file), result[1])
329 | if (fileStack.includes(file)) {
330 | this.extension.logger.addLogMessage(`Looped root file by magic comment found: ${file}, stop here.`)
331 | return file
332 | } else {
333 | fileStack.push(file)
334 | this.extension.logger.addLogMessage(`Recursively found root file by magic comment: ${file}`)
335 | }
336 |
337 | content = fs.readFileSync(file).toString()
338 | if (content === undefined) {
339 | const msg = `Not found root file specified in the magic comment: ${file}`
340 | this.extension.logger.addLogMessage(msg)
341 | throw new Error(msg)
342 |
343 | }
344 | result = content.match(regex)
345 | }
346 | return file
347 | }
348 | return undefined
349 | },
350 | () => this.findRootFromActive(),
351 | () => this.findRootInWorkspace()
352 | ]
353 | for (const method of findMethods) {
354 | const rootFile = await method()
355 | if (rootFile === undefined) {
356 | continue
357 | }
358 | if (this.rootFile !== rootFile) {
359 | this.extension.logger.addLogMessage(`Root file changed: from ${this.rootFile} to ${rootFile}`)
360 | this.extension.logger.addLogMessage('Start to find all dependencies.')
361 | this.rootFile = rootFile
362 | this.rootFileLanguageId = this.inferLanguageId(rootFile)
363 | this.extension.logger.addLogMessage(`Root file languageId: ${this.rootFileLanguageId}`)
364 | } else {
365 | this.extension.logger.addLogMessage(`Keep using the same root file: ${this.rootFile}`)
366 | }
367 | return rootFile
368 | }
369 | return undefined
370 | }
371 |
372 | private findRootFromActive(): string | undefined {
373 | if (!vscode.window.activeTextEditor) {
374 | return undefined
375 | }
376 | if (vscode.window.activeTextEditor.document.uri.scheme !== 'file') {
377 | this.extension.logger.addLogMessage(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`)
378 | return undefined
379 | }
380 | const regex = /\\begin{document}/m
381 | const content = utils.stripCommentsAndVerbatim(vscode.window.activeTextEditor.document.getText())
382 | const result = content.match(regex)
383 | if (result) {
384 | const rootSubFile = this.finderUtils.findSubFiles(content)
385 | const file = vscode.window.activeTextEditor.document.fileName
386 | if (rootSubFile) {
387 | this.localRootFile = file
388 | return rootSubFile
389 | } else {
390 | this.extension.logger.addLogMessage(`Found root file from active editor: ${file}`)
391 | return file
392 | }
393 | }
394 | return undefined
395 | }
396 |
397 | private async findRootInWorkspace(): Promise {
398 | const regex = /\\begin{document}/m
399 |
400 | if (!this.workspaceRootDirUri) {
401 | return undefined
402 | }
403 |
404 | const configuration = vscode.workspace.getConfiguration('latex-workshop')
405 | const rootFilesIncludePatterns = configuration.get('latex.search.rootFiles.include') as string[]
406 | const rootFilesIncludeGlob = '{' + rootFilesIncludePatterns.join(',') + '}'
407 | const rootFilesExcludePatterns = configuration.get('latex.search.rootFiles.exclude') as string[]
408 | const rootFilesExcludeGlob = rootFilesExcludePatterns.length > 0 ? '{' + rootFilesExcludePatterns.join(',') + '}' : undefined
409 | try {
410 | const files = await vscode.workspace.findFiles(rootFilesIncludeGlob, rootFilesExcludeGlob)
411 | const candidates: string[] = []
412 | for (const file of files) {
413 | if (file.scheme !== 'file') {
414 | this.extension.logger.addLogMessage(`Skip the file: ${file.toString(true)}`)
415 | continue
416 | }
417 | const flsChildren = this.getTeXChildrenFromFls(file.fsPath)
418 | if (vscode.window.activeTextEditor && flsChildren.includes(vscode.window.activeTextEditor.document.fileName)) {
419 | this.extension.logger.addLogMessage(`Found root file from '.fls': ${file.fsPath}`)
420 | return file.fsPath
421 | }
422 | const content = utils.stripCommentsAndVerbatim(fs.readFileSync(file.fsPath).toString())
423 | const result = content.match(regex)
424 | if (result) {
425 | // Can be a root
426 | const children = this.getTeXChildren(file.fsPath, file.fsPath, [], content)
427 | if (vscode.window.activeTextEditor && children.includes(vscode.window.activeTextEditor.document.fileName)) {
428 | this.extension.logger.addLogMessage(`Found root file from parent: ${file.fsPath}`)
429 | return file.fsPath
430 | }
431 | // Not including the active file, yet can still be a root candidate
432 | candidates.push(file.fsPath)
433 | }
434 | }
435 | if (candidates.length > 0) {
436 | this.extension.logger.addLogMessage(`Found files that might be root, choose the first one: ${candidates}`)
437 | return candidates[0]
438 | }
439 | } catch (e) {
440 | this.extension.logger.addLogMessage(`Cannot find root file: ${e.message}`)
441 | }
442 | return undefined
443 | }
444 |
445 | private getTeXChildrenFromFls(texFile: string) {
446 | const flsFile = this.pathUtils.getFlsFilePath(texFile)
447 | if (flsFile === undefined) {
448 | return []
449 | }
450 | const rootDir = path.dirname(texFile)
451 | const ioFiles = this.pathUtils.parseFlsContent(fs.readFileSync(flsFile).toString(), rootDir)
452 | return ioFiles.input
453 | }
454 |
455 | /**
456 | * Return the list of files (recursively) included in `file`
457 | *
458 | * @param file The file in which children are recursively computed
459 | * @param baseFile The file currently considered as the rootFile
460 | * @param children The list of already computed children
461 | * @param content The content of `file`. If undefined, it is read from disk
462 | */
463 | private getTeXChildren(file: string, baseFile: string, children: string[], content?: string): string[] {
464 | if (content === undefined) {
465 | content = utils.stripCommentsAndVerbatim(fs.readFileSync(file).toString())
466 | }
467 |
468 | // Update children of current file
469 | if (this.cachedContent[file] === undefined) {
470 | this.cachedContent[file] = {content, bibs: [], children: []}
471 | const pathRegexp = new PathRegExp()
472 | // eslint-disable-next-line no-constant-condition
473 | while (true) {
474 | const result: MatchPath | undefined = pathRegexp.exec(content)
475 | if (!result) {
476 | break
477 | }
478 |
479 | const inputFile = pathRegexp.parseInputFilePath(result, file, baseFile)
480 |
481 | if (!inputFile ||
482 | !fs.existsSync(inputFile) ||
483 | path.relative(inputFile, baseFile) === '') {
484 | continue
485 | }
486 |
487 | this.cachedContent[file].children.push({
488 | index: result.index,
489 | file: inputFile
490 | })
491 | }
492 | }
493 |
494 | this.cachedContent[file].children.forEach(child => {
495 | if (children.includes(child.file)) {
496 | // Already included
497 | return
498 | }
499 | children.push(child.file)
500 | this.getTeXChildren(child.file, baseFile, children)
501 | })
502 | return children
503 | }
504 | }
505 |
--------------------------------------------------------------------------------
/src/workshop/pathutils.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as path from 'path'
3 | import * as fs from 'fs'
4 | import * as utils from './utils'
5 |
6 | import type {Extension} from '../main'
7 |
8 | export enum MatchType {
9 | Input,
10 | Child
11 | }
12 |
13 | export interface MatchPath {
14 | type: MatchType
15 | path: string
16 | directory: string
17 | matchedString: string
18 | index: number
19 | }
20 |
21 | export class PathRegExp {
22 | private readonly inputRegexp: RegExp
23 | private readonly childRegexp: RegExp
24 |
25 | constructor() {
26 | this.inputRegexp = /\\(?:input|InputIfFileExists|include|SweaveInput|subfile|loadglsentries|(?:(?:sub)?(?:import|inputfrom|includefrom)\*?{([^}]*)}))(?:\[[^[\]{}]*\])?{([^}]*)}/g
27 | this.childRegexp = /<<(?:[^,]*,)*\s*child='([^']*)'\s*(?:,[^,]*)*>>=/g
28 | }
29 |
30 | resetLastIndex() {
31 | this.inputRegexp.lastIndex = 0
32 | this.childRegexp.lastIndex = 0
33 | }
34 |
35 | /**
36 | * Return the matched input or child path. If there is no match, return undefined
37 | *
38 | * @param content the string to match the regex on
39 | */
40 | exec(content: string): MatchPath | undefined {
41 | let result = this.inputRegexp.exec(content)
42 | if (result) {
43 | return {
44 | type: MatchType.Input,
45 | path: result[2],
46 | directory: result[1],
47 | matchedString: result[0],
48 | index: result.index
49 | }
50 | }
51 | result = this.childRegexp.exec(content)
52 | if (result) {
53 | return {
54 | type: MatchType.Child,
55 | path: result[1],
56 | directory: '',
57 | matchedString: result[0],
58 | index: result.index
59 | }
60 | }
61 | return undefined
62 | }
63 | /**
64 | * Compute the resolved file path from matches of this.inputReg or this.childReg
65 | *
66 | * @param regResult is the the result of this.inputReg.exec() or this.childReg.exec()
67 | * @param currentFile is the name of file in which the match has been obtained
68 | * @param rootFile
69 | */
70 | parseInputFilePath(match: MatchPath, currentFile: string, rootFile: string): string | undefined {
71 | const texDirs = vscode.workspace.getConfiguration('latex-workshop').get('latex.texDirs') as string[]
72 | /* match of this.childReg */
73 | if (match.type === MatchType.Child) {
74 | return utils.resolveFile([path.dirname(currentFile), path.dirname(rootFile), ...texDirs], match.path)
75 | }
76 |
77 | /* match of this.inputReg */
78 | if (match.type === MatchType.Input) {
79 | if (match.matchedString.startsWith('\\subimport') || match.matchedString.startsWith('\\subinputfrom') || match.matchedString.startsWith('\\subincludefrom')) {
80 | return utils.resolveFile([path.dirname(currentFile)], path.join(match.directory, match.path))
81 | } else if (match.matchedString.startsWith('\\import') || match.matchedString.startsWith('\\inputfrom') || match.matchedString.startsWith('\\includefrom')) {
82 | return utils.resolveFile([match.directory, path.join(path.dirname(rootFile), match.directory)], match.path)
83 | } else {
84 | return utils.resolveFile([path.dirname(currentFile), path.dirname(rootFile), ...texDirs], match.path)
85 | }
86 | }
87 | return undefined
88 | }
89 |
90 | }
91 |
92 | export class PathUtils {
93 | private readonly extension: Extension
94 |
95 | constructor(extension: Extension) {
96 | this.extension = extension
97 | }
98 |
99 | private getOutDir(texFile: string) {
100 | return this.extension.manager.getOutDir(texFile)
101 | }
102 |
103 | /**
104 | * Search for a `.fls` file associated to a tex file
105 | * @param texFile The path of LaTeX file
106 | * @return The path of the .fls file or undefined
107 | */
108 | getFlsFilePath(texFile: string): string | undefined {
109 | const rootDir = path.dirname(texFile)
110 | const outDir = this.getOutDir(texFile)
111 | const baseName = path.parse(texFile).name
112 | const flsFile = path.resolve(rootDir, path.join(outDir, baseName + '.fls'))
113 | if (!fs.existsSync(flsFile)) {
114 | this.extension.logger.addLogMessage(`Cannot find fls file: ${flsFile}`)
115 | return undefined
116 | }
117 | this.extension.logger.addLogMessage(`Fls file found: ${flsFile}`)
118 | return flsFile
119 | }
120 |
121 | parseFlsContent(content: string, rootDir: string): {input: string[], output: string[]} {
122 | const inputFiles: Set = new Set()
123 | const outputFiles: Set = new Set()
124 | const regex = /^(?:(INPUT)\s*(.*))|(?:(OUTPUT)\s*(.*))$/gm
125 | // regex groups
126 | // #1: an INPUT entry --> #2 input file path
127 | // #3: an OUTPUT entry --> #4: output file path
128 | // eslint-disable-next-line no-constant-condition
129 | while (true) {
130 | const result = regex.exec(content)
131 | if (!result) {
132 | break
133 | }
134 | if (result[1]) {
135 | const inputFilePath = path.resolve(rootDir, result[2])
136 | if (inputFilePath) {
137 | inputFiles.add(inputFilePath)
138 | }
139 | } else if (result[3]) {
140 | const outputFilePath = path.resolve(rootDir, result[4])
141 | if (outputFilePath) {
142 | outputFiles.add(outputFilePath)
143 | }
144 | }
145 | }
146 |
147 | return {input: Array.from(inputFiles), output: Array.from(outputFiles)}
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/workshop/utils.ts:
--------------------------------------------------------------------------------
1 | // from James-Yu/LaTeX-Workshop.
2 |
3 | import * as vscode from 'vscode'
4 | import * as path from 'path'
5 | import * as fs from 'fs'
6 |
7 | import type {latexParser} from 'latex-utensils'
8 |
9 |
10 | export function sleep(ms: number) {
11 | return new Promise(resolve => setTimeout(resolve, ms))
12 | }
13 |
14 | export function escapeHtml(s: string): string {
15 | return s.replace(/&/g, '&')
16 | .replace(/"/g, '"')
17 | .replace(//g, '>')
19 | }
20 |
21 | export function escapeRegExp(str: string) {
22 | return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
23 | }
24 |
25 | /**
26 | * Remove comments
27 | *
28 | * @param text A string in which comments get removed.
29 | * @return the input text with comments removed.
30 | * Note the number lines of the output matches the input
31 | */
32 | export function stripComments(text: string): string {
33 | const reg = /(^|[^\\]|(?:(? {
51 | const len = Math.max(match.split('\n').length, 1)
52 | return '\n'.repeat(len - 1)
53 | })
54 | }
55 |
56 | /**
57 | * Remove comments and verbatim content
58 | * Note the number lines of the output matches the input
59 | *
60 | * @param text A multiline string to be stripped
61 | * @return the input text with comments and verbatim content removed.
62 | */
63 | export function stripCommentsAndVerbatim(text: string): string {
64 | let content = stripComments(text)
65 | content = content.replace(/\\verb\*?([^a-zA-Z0-9]).*?\1/g, '')
66 | const configuration = vscode.workspace.getConfiguration('latex-workshop')
67 | const verbatimEnvs = configuration.get('latex.verbatimEnvs') as string[]
68 | return stripEnvironments(content, verbatimEnvs)
69 | }
70 |
71 | /**
72 | * Trim leading and ending spaces on every line
73 | * See https://blog.stevenlevithan.com/archives/faster-trim-javascript for
74 | * possible ways of implementing trimming
75 | *
76 | * @param text a multiline string
77 | */
78 | export function trimMultiLineString(text: string): string {
79 | return text.replace(/^\s\s*/gm, '').replace(/\s\s*$/gm, '')
80 | }
81 |
82 | /**
83 | * Find the longest substring containing balanced curly braces {...}
84 | * The string `s` can either start on the opening `{` or at the next character
85 | *
86 | * @param s A string to be searched.
87 | */
88 | export function getLongestBalancedString(s: string): string {
89 | let nested = s[0] === '{' ? 0 : 1
90 | let i = 0
91 | for (i = 0; i < s.length; i++) {
92 | switch (s[i]) {
93 | case '{':
94 | nested++
95 | break
96 | case '}':
97 | nested--
98 | break
99 | case '\\':
100 | // skip an escaped character
101 | i++
102 | break
103 | default:
104 | }
105 | if (nested === 0) {
106 | break
107 | }
108 | }
109 | return s.substring(s[0] === '{' ? 1 : 0, i)
110 | }
111 |
112 |
113 | export type CommandArgument = {
114 | arg: string // The argument we are looking for
115 | index: number // the starting position of the argument
116 | }
117 |
118 | /**
119 | * @param text a string starting with a command call
120 | * @param nth the index of the argument to return
121 | */
122 | export function getNthArgument(text: string, nth: number): CommandArgument | undefined {
123 | let arg = ''
124 | let index = 0 // start of the nth argument
125 | let offset = 0 // current offset of the new text to consider
126 | for (let i=0; i string {
176 | return (arg: string) => {
177 | const configuration = vscode.workspace.getConfiguration('latex-workshop')
178 | const docker = configuration.get('docker.enabled')
179 |
180 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
181 | const workspaceDir = workspaceFolder?.uri.fsPath.split(path.sep).join('/') || ''
182 | const rootFileParsed = path.parse(rootFile)
183 | const docfile = rootFileParsed.name
184 | const docfileExt = rootFileParsed.base
185 | const dirW32 = path.normalize(rootFileParsed.dir)
186 | const dir = dirW32.split(path.sep).join('/')
187 | const docW32 = path.join(dirW32, docfile)
188 | const doc = docW32.split(path.sep).join('/')
189 | const docExtW32 = path.join(dirW32, docfileExt)
190 | const docExt = docExtW32.split(path.sep).join('/')
191 |
192 | const expandPlaceHolders = (a: string): string => {
193 | return a.replace(/%DOC%/g, docker ? docfile : doc)
194 | .replace(/%DOC_W32%/g, docker ? docfile : docW32)
195 | .replace(/%DOC_EXT%/g, docker ? docfileExt : docExt)
196 | .replace(/%DOC_EXT_W32%/g, docker ? docfileExt : docExtW32)
197 | .replace(/%DOCFILE_EXT%/g, docfileExt)
198 | .replace(/%DOCFILE%/g, docfile)
199 | .replace(/%DIR%/g, docker ? './' : dir)
200 | .replace(/%DIR_W32%/g, docker ? './' : dirW32)
201 | .replace(/%TMPDIR%/g, tmpDir)
202 | .replace(/%WORKSPACE_FOLDER%/g, docker ? './' : workspaceDir)
203 | .replace(/%RELATIVE_DIR%/, docker ? './' : path.relative(workspaceDir, dir))
204 | .replace(/%RELATIVE_DOC%/, docker ? docfile : path.relative(workspaceDir, doc))
205 |
206 | }
207 | const outDirW32 = path.normalize(expandPlaceHolders(configuration.get('latex.outDir') as string))
208 | const outDir = outDirW32.split(path.sep).join('/')
209 | return expandPlaceHolders(arg).replace(/%OUTDIR%/g, outDir).replace(/%OUTDIR_W32%/g, outDirW32)
210 | }
211 | }
212 |
213 | export type NewCommand = {
214 | kind: 'command'
215 | name: 'renewcommand|newcommand|providecommand|DeclareMathOperator|renewcommand*|newcommand*|providecommand*|DeclareMathOperator*'
216 | args: (latexParser.OptionalArg | latexParser.Group)[]
217 | location: latexParser.Location
218 | }
219 |
220 | export function isNewCommand(node: latexParser.Node | undefined): node is NewCommand {
221 | const regex = /^(renewcommand|newcommand|providecommand|DeclareMathOperator)(\*)?$/
222 | if (!!node && node.kind === 'command' && node.name.match(regex)) {
223 | return true
224 | }
225 | return false
226 | }
227 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "alwaysStrict": true,
4 | "forceConsistentCasingInFileNames": true,
5 | "lib": ["es2018"],
6 | "module": "commonjs",
7 | "noFallthroughCasesInSwitch": true,
8 | "noImplicitAny": true,
9 | "noImplicitReturns": true,
10 | "noImplicitThis": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "outDir": "out",
14 | "rootDir": ".",
15 | "sourceMap": true,
16 | "strictBindCallApply": true,
17 | "strictFunctionTypes": true,
18 | "strictNullChecks": true,
19 | "strictPropertyInitialization": true,
20 | "esModuleInterop": true,
21 | "target": "es2018",
22 | "baseUrl": "./",
23 | "typeRoots": ["./types", "./node_modules/@types"]
24 | },
25 | "include": ["src/**/*.ts"]
26 | }
27 |
--------------------------------------------------------------------------------