Using URI across the board for better Windows support (#391)

* add windows-2019 to CI os matrix
* use actual URI object instead of strings to represent paths/uris
* added datastore tests
* chore: make Foam IDisposable
This commit is contained in:
Riccardo
2020-12-16 17:47:31 +01:00
committed by GitHub
parent 39bc7ec0be
commit 6baeec4db6
43 changed files with 1852 additions and 199 deletions

View File

@@ -36,7 +36,7 @@ jobs:
name: Build and Test
strategy:
matrix:
os: [macos-10.15, ubuntu-18.04] # add windows-2019 after fixing tests for it
os: [macos-10.15, ubuntu-18.04, windows-2019]
runs-on: ${{ matrix.os }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
env:

View File

@@ -8,6 +8,7 @@ import {
applyTextEdit,
Services,
FileDataStore,
URI,
} from 'foam-core';
import { writeFileToDisk } from '../utils/write-file-to-disk';
import { isValidDirectory } from '../utils';
@@ -40,7 +41,7 @@ export default class Janitor extends Command {
const { workspacePath = './' } = args;
if (isValidDirectory(workspacePath)) {
const config = createConfigFromFolders([workspacePath]);
const config = createConfigFromFolders([URI.file(workspacePath)]);
const services: Services = {
dataStore: new FileDataStore(config),
};

View File

@@ -1,15 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import { URI } from 'foam-core';
/**
*
* @param fileUri absolute path for the file that needs to renamed
* @param newFileName "new file name" without the extension
*/
export const renameFile = async (fileUri: string, newFileName: string) => {
const dirName = path.dirname(fileUri);
const extension = path.extname(fileUri);
export const renameFile = async (fileUri: URI, newFileName: string) => {
const filePath = fileUri.fsPath;
const dirName = path.dirname(filePath);
const extension = path.extname(filePath);
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
return fs.promises.rename(fileUri, newFileUri);
return fs.promises.rename(filePath, newFileUri);
};

View File

@@ -1,5 +1,6 @@
import * as fs from 'fs';
import { URI } from 'foam-core';
export const writeFileToDisk = async (fileUri: string, data: string) => {
return fs.promises.writeFile(fileUri, data);
export const writeFileToDisk = async (fileUri: URI, data: string) => {
return fs.promises.writeFile(fileUri.fsPath, data);
};

View File

@@ -1,6 +1,7 @@
import { renameFile } from '../src/utils/rename-file';
import * as fs from 'fs';
import mockFS from 'mock-fs';
import { URI } from 'foam-core';
const doesFileExist = path =>
fs.promises
@@ -9,10 +10,10 @@ const doesFileExist = path =>
.catch(() => false);
describe('renameFile', () => {
const fileUri = './test/oldFileName.md';
const fileUri = URI.file('/test/oldFileName.md');
beforeAll(() => {
mockFS({ [fileUri]: '' });
mockFS({ [fileUri.fsPath]: '' });
});
afterAll(() => {
@@ -20,11 +21,11 @@ describe('renameFile', () => {
});
it('should rename existing file', async () => {
expect(await doesFileExist(fileUri)).toBe(true);
expect(await doesFileExist(fileUri.fsPath)).toBe(true);
renameFile(fileUri, 'new-file-name');
expect(await doesFileExist(fileUri)).toBe(false);
expect(await doesFileExist('./test/new-file-name.md')).toBe(true);
expect(await doesFileExist(fileUri.fsPath)).toBe(false);
expect(await doesFileExist('/test/new-file-name.md')).toBe(true);
});
});

View File

@@ -1,23 +1,26 @@
import { writeFileToDisk } from '../src/utils/write-file-to-disk';
import * as fs from 'fs';
import mockFS from 'mock-fs';
import { URI } from 'foam-core';
describe('writeFileToDisk', () => {
const fileUri = './test-file.md';
const fileUri = URI.file('./test-file.md');
beforeAll(() => {
mockFS({ [fileUri]: 'content in the existing file' });
mockFS({ [fileUri.fsPath]: 'content in the existing file' });
});
afterAll(() => {
fs.unlinkSync(fileUri);
fs.unlinkSync(fileUri.fsPath);
mockFS.restore();
});
it('should overrwrite existing file in the disk with the new data', async () => {
const expected = `content in the new file`;
await writeFileToDisk(fileUri, expected);
const actual = await fs.promises.readFile(fileUri, { encoding: 'utf8' });
const actual = await fs.promises.readFile(fileUri.fsPath, {
encoding: 'utf8',
});
expect(actual).toBe(expected);
});
});

View File

@@ -4,6 +4,7 @@ import { FoamConfig, Foam, Services } from './index';
import { loadPlugins } from './plugins';
import { isSome } from './utils';
import { isDisposable } from './common/lifecycle';
import { Logger } from './utils/log';
export const bootstrap = async (config: FoamConfig, services: Services) => {
const plugins = await loadPlugins(config);
@@ -17,9 +18,12 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
const files = await services.dataStore.listFiles();
await Promise.all(
files.map(async uri => {
const content = await services.dataStore.read(uri);
if (isSome(content)) {
graph.setNote(parser.parse(uri, content));
Logger.info('Found: ' + uri);
if (uri.path.endsWith('md')) {
const content = await services.dataStore.read(uri);
if (isSome(content)) {
graph.setNote(parser.parse(uri, content));
}
}
})
);

View File

@@ -0,0 +1,436 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export const enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent
U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent
U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent
U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde
U_Combining_Macron = 0x0304, // U+0304 Combining Macron
U_Combining_Overline = 0x0305, // U+0305 Combining Overline
U_Combining_Breve = 0x0306, // U+0306 Combining Breve
U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above
U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis
U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above
U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above
U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent
U_Combining_Caron = 0x030c, // U+030C Combining Caron
U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above
U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above
U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent
U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu
U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve
U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above
U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above
U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above
U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right
U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below
U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below
U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below
U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below
U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above
U_Combining_Horn = 0x031b, // U+031B Combining Horn
U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below
U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below
U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below
U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below
U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below
U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below
U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below
U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below
U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below
U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below
U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below
U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla
U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek
U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below
U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below
U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below
U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below
U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below
U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below
U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below
U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below
U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below
U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line
U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line
U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay
U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay
U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay
U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay
U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay
U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below
U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below
U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below
U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below
U_Combining_X_Above = 0x033d, // U+033D Combining X Above
U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde
U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline
U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark
U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark
U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni
U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis
U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos
U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni
U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above
U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below
U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below
U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below
U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above
U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above
U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above
U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below
U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below
U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner
U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above
U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above
U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata
U_Combining_X_Below = 0x0353, // U+0353 Combining X Below
U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below
U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below
U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below
U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above
U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right
U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below
U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below
U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above
U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below
U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve
U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron
U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below
U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde
U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve
U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below
U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A
U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E
U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I
U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O
U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U
U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C
U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D
U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H
U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M
U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R
U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T
U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V
U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X
/**
* Unicode Character 'LINE SEPARATOR' (U+2028)
* http://www.fileformat.info/info/unicode/char/2028/index.htm
*/
LINE_SEPARATOR = 0x2028,
/**
* Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
* http://www.fileformat.info/info/unicode/char/2029/index.htm
*/
PARAGRAPH_SEPARATOR = 0x2029,
/**
* Unicode Character 'NEXT LINE' (U+0085)
* http://www.fileformat.info/info/unicode/char/0085/index.htm
*/
NEXT_LINE = 0x0085,
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX
U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT
U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS
U_MACRON = 0x00af, // U+00AF MACRON
U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT
U_CEDILLA = 0x00b8, // U+00B8 CEDILLA
U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD
U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD
U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD
U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD
U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING
U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING
U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK
U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK
U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN
U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN
U_BREVE = 0x02d8, // U+02D8 BREVE
U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE
U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE
U_OGONEK = 0x02db, // U+02DB OGONEK
U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE
U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT
U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK
U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT
U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR
U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR
U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR
U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR
U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR
U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK
U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK
U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED
U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD
U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD
U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD
U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD
U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING
U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT
U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE
U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON
U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE
U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE
U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE
U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE
U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF
U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF
U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW
U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN
U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS
U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS
U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS
U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI
U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI
U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI
U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA
U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA
U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI
U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA
U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA
U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI
U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA
U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA
U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA
U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA
U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA
U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE'
/**
* UTF-8 BOM
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
* http://www.fileformat.info/info/unicode/char/feff/index.htm
*/
UTF8_BOM = 65279,
}

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
const LANGUAGE_DEFAULT = 'en';
let _isWindows = false;
let _isMacintosh = false;
let _isLinux = false;
let _isNative = false;
let _isWeb = false;
let _isIOS = false;
let _locale: string | undefined = undefined;
let _language: string = LANGUAGE_DEFAULT;
let _translationsConfigFile: string | undefined = undefined;
let _userAgent: string | undefined = undefined;
interface NLSConfig {
locale: string;
availableLanguages: { [key: string]: string };
_translationsConfigFile: string;
}
export interface IProcessEnvironment {
[key: string]: string;
}
export interface INodeProcess {
platform: 'win32' | 'linux' | 'darwin';
env: IProcessEnvironment;
nextTick: Function;
versions?: {
electron?: string;
};
sandboxed?: boolean; // Electron
type?: string;
cwd(): string;
}
declare const process: INodeProcess;
declare const global: any;
interface INavigator {
userAgent: string;
language: string;
maxTouchPoints?: number;
}
declare const navigator: INavigator;
declare const self: any;
const _globals =
typeof self === 'object'
? self
: typeof global === 'object'
? global
: ({} as any);
let nodeProcess: INodeProcess | undefined = undefined;
if (typeof process !== 'undefined') {
// Native environment (non-sandboxed)
nodeProcess = process;
} else if (typeof _globals.vscode !== 'undefined') {
// Native environment (sandboxed)
nodeProcess = _globals.vscode.process;
}
const isElectronRenderer =
typeof nodeProcess?.versions?.electron === 'string' &&
nodeProcess.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
// Web environment
if (typeof navigator === 'object' && !isElectronRenderer) {
_userAgent = navigator.userAgent;
_isWindows = _userAgent.indexOf('Windows') >= 0;
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
_isIOS =
(_userAgent.indexOf('Macintosh') >= 0 ||
_userAgent.indexOf('iPad') >= 0 ||
_userAgent.indexOf('iPhone') >= 0) &&
!!navigator.maxTouchPoints &&
navigator.maxTouchPoints > 0;
_isLinux = _userAgent.indexOf('Linux') >= 0;
_isWeb = true;
_locale = navigator.language;
_language = _locale;
}
// Native environment
else if (typeof nodeProcess === 'object') {
_isWindows = nodeProcess.platform === 'win32';
_isMacintosh = nodeProcess.platform === 'darwin';
_isLinux = nodeProcess.platform === 'linux';
_locale = LANGUAGE_DEFAULT;
_language = LANGUAGE_DEFAULT;
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
if (rawNlsConfig) {
try {
const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig);
const resolved = nlsConfig.availableLanguages['*'];
_locale = nlsConfig.locale;
// VSCode's default language is 'en'
_language = resolved ? resolved : LANGUAGE_DEFAULT;
_translationsConfigFile = nlsConfig._translationsConfigFile;
} catch (e) {}
}
_isNative = true;
}
// Unknown environment
else {
console.error('Unable to resolve platform.');
}
export const enum Platform {
Web,
Mac,
Linux,
Windows,
}
export function PlatformToString(platform: Platform) {
switch (platform) {
case Platform.Web:
return 'Web';
case Platform.Mac:
return 'Mac';
case Platform.Linux:
return 'Linux';
case Platform.Windows:
return 'Windows';
}
}
let _platform: Platform = Platform.Web;
if (_isMacintosh) {
_platform = Platform.Mac;
} else if (_isWindows) {
_platform = Platform.Windows;
} else if (_isLinux) {
_platform = Platform.Linux;
}
export const isWindows = _isWindows;
export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isNative = _isNative;
export const isWeb = _isWeb;
export const isIOS = _isIOS;
export const platform = _platform;
export const userAgent = _userAgent;
/**
* The language used for the user interface. The format of
* the string is all lower case (e.g. zh-tw for Traditional
* Chinese)
*/
export const language = _language;
export namespace Language {
export function value(): string {
return language;
}
export function isDefaultVariant(): boolean {
if (language.length === 2) {
return language === 'en';
} else if (language.length >= 3) {
return language[0] === 'e' && language[1] === 'n' && language[2] === '-';
} else {
return false;
}
}
export function isDefault(): boolean {
return language === 'en';
}
}
/**
* The OS locale or the locale specified by --locale. The format of
* the string is all lower case (e.g. zh-tw for Traditional
* Chinese). The UI is not necessarily shown in the provided locale.
*/
export const locale = _locale;
/**
* The translatios that are available through language packs.
*/
export const translationsConfigFile = _translationsConfigFile;
export const globals: any = _globals;
interface ISetImmediate {
(callback: (...args: any[]) => void): void;
}

View File

@@ -0,0 +1,748 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
import { isWindows } from './platform';
import { CharCode } from './charCode';
import * as paths from 'path';
const _schemePattern = /^\w[\w\d+.-]*$/;
const _singleSlashStart = /^\//;
const _doubleSlashStart = /^\/\//;
function _validateUri(ret: URI, _strict?: boolean): void {
// scheme, must be set
if (!ret.scheme && _strict) {
throw new Error(
`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`
);
}
// scheme, https://tools.ietf.org/html/rfc3986#section-3.1
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
if (ret.scheme && !_schemePattern.test(ret.scheme)) {
throw new Error('[UriError]: Scheme contains illegal characters.');
}
// path, http://tools.ietf.org/html/rfc3986#section-3.3
// If a URI contains an authority component, then the path component
// must either be empty or begin with a slash ("/") character. If a URI
// does not contain an authority component, then the path cannot begin
// with two slash characters ("//").
if (ret.path) {
if (ret.authority) {
if (!_singleSlashStart.test(ret.path)) {
throw new Error(
'[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'
);
}
} else {
if (_doubleSlashStart.test(ret.path)) {
throw new Error(
'[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'
);
}
}
}
}
// for a while we allowed uris *without* schemes and this is the migration
// for them, e.g. an uri without scheme and without strict-mode warns and falls
// back to the file-scheme. that should cause the least carnage and still be a
// clear warning
function _schemeFix(scheme: string, _strict: boolean): string {
if (!scheme && !_strict) {
return 'file';
}
return scheme;
}
// implements a bit of https://tools.ietf.org/html/rfc3986#section-5
function _referenceResolution(scheme: string, path: string): string {
// the slash-character is our 'default base' as we don't
// support constructing URIs relative to other URIs. This
// also means that we alter and potentially break paths.
// see https://tools.ietf.org/html/rfc3986#section-5.1.4
switch (scheme) {
case 'https':
case 'http':
case 'file':
if (!path) {
path = _slash;
} else if (path[0] !== _slash) {
path = _slash + path;
}
break;
}
return path;
}
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
/**
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
* This class is a simple parser which creates the basic component parts
* (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
* and encoding.
*
* ```txt
* foo://example.com:8042/over/there?name=ferret#nose
* \_/ \______________/\_________/ \_________/ \__/
* | | | | |
* scheme authority path query fragment
* | _____________________|__
* / \ / \
* urn:example:animal:ferret:nose
* ```
*/
export class URI implements UriComponents {
static isUri(thing: any): thing is URI {
if (thing instanceof URI) {
return true;
}
if (!thing) {
return false;
}
return (
typeof (thing as URI).authority === 'string' &&
typeof (thing as URI).fragment === 'string' &&
typeof (thing as URI).path === 'string' &&
typeof (thing as URI).query === 'string' &&
typeof (thing as URI).scheme === 'string' &&
typeof (thing as URI).fsPath === 'function' &&
typeof (thing as URI).with === 'function' &&
typeof (thing as URI).toString === 'function'
);
}
/**
* scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'.
* The part before the first colon.
*/
readonly scheme: string;
/**
* authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'.
* The part between the first double slashes and the next slash.
*/
readonly authority: string;
/**
* path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly path: string;
/**
* query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly query: string;
/**
* fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'.
*/
readonly fragment: string;
/**
* @internal
*/
protected constructor(
scheme: string,
authority?: string,
path?: string,
query?: string,
fragment?: string,
_strict?: boolean
);
/**
* @internal
*/
protected constructor(components: UriComponents);
/**
* @internal
*/
protected constructor(
schemeOrData: string | UriComponents,
authority?: string,
path?: string,
query?: string,
fragment?: string,
_strict: boolean = false
) {
if (typeof schemeOrData === 'object') {
this.scheme = schemeOrData.scheme || _empty;
this.authority = schemeOrData.authority || _empty;
this.path = schemeOrData.path || _empty;
this.query = schemeOrData.query || _empty;
this.fragment = schemeOrData.fragment || _empty;
// no validation because it's this URI
// that creates uri components.
// _validateUri(this);
} else {
this.scheme = _schemeFix(schemeOrData, _strict);
this.authority = authority || _empty;
this.path = _referenceResolution(this.scheme, path || _empty);
this.query = query || _empty;
this.fragment = fragment || _empty;
_validateUri(this, _strict);
}
}
// ---- filesystem path -----------------------
/**
* Returns a string representing the corresponding file system path of this URI.
* Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
* platform specific path separator.
*
* * Will *not* validate the path for invalid characters and semantics.
* * Will *not* look at the scheme of this URI.
* * The result shall *not* be used for display purposes but for accessing a file on disk.
*
*
* The *difference* to `URI#path` is the use of the platform specific separator and the handling
* of UNC paths. See the below sample of a file-uri with an authority (UNC path).
*
* ```ts
const u = URI.parse('file://server/c$/folder/file.txt')
u.authority === 'server'
u.path === '/shares/c$/file.txt'
u.fsPath === '\\server\c$\folder\file.txt'
```
*
* Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
* namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
* with URIs that represent files on disk (`file` scheme).
*/
get fsPath(): string {
// if (this.scheme !== 'file') {
// console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
// }
return uriToFsPath(this, false);
}
// ---- modify to new -------------------------
with(change: {
scheme?: string;
authority?: string | null;
path?: string | null;
query?: string | null;
fragment?: string | null;
}): URI {
if (!change) {
return this;
}
let { scheme, authority, path, query, fragment } = change;
if (scheme === undefined) {
scheme = this.scheme;
} else if (scheme === null) {
scheme = _empty;
}
if (authority === undefined) {
authority = this.authority;
} else if (authority === null) {
authority = _empty;
}
if (path === undefined) {
path = this.path;
} else if (path === null) {
path = _empty;
}
if (query === undefined) {
query = this.query;
} else if (query === null) {
query = _empty;
}
if (fragment === undefined) {
fragment = this.fragment;
} else if (fragment === null) {
fragment = _empty;
}
if (
scheme === this.scheme &&
authority === this.authority &&
path === this.path &&
query === this.query &&
fragment === this.fragment
) {
return this;
}
return new Uri(scheme, authority, path, query, fragment);
}
// ---- parse & validate ------------------------
/**
* Creates a new URI from a string, e.g. `http://www.msft.com/some/path`,
* `file:///usr/home`, or `scheme:with/path`.
*
* @param value A string which represents an URI (see `URI#toString`).
*/
static parse(value: string, _strict: boolean = false): URI {
const match = _regexp.exec(value);
if (!match) {
return new Uri(_empty, _empty, _empty, _empty, _empty);
}
return new Uri(
match[2] || _empty,
percentDecode(match[4] || _empty),
percentDecode(match[5] || _empty),
percentDecode(match[7] || _empty),
percentDecode(match[9] || _empty),
_strict
);
}
/**
* Creates a new URI from a file system path, e.g. `c:\my\files`,
* `/usr/home`, or `\\server\share\some\path`.
*
* The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
* as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
* `URI.parse('file://' + path)` because the path might contain characters that are
* interpreted (# and ?). See the following sample:
* ```ts
const good = URI.file('/coding/c#/project1');
good.scheme === 'file';
good.path === '/coding/c#/project1';
good.fragment === '';
const bad = URI.parse('file://' + '/coding/c#/project1');
bad.scheme === 'file';
bad.path === '/coding/c'; // path is now broken
bad.fragment === '/project1';
```
*
* @param path A file system path (see `URI#fsPath`)
*/
static file(path: string): URI {
let authority = _empty;
// normalize to fwd-slashes on windows,
// on other systems bwd-slashes are valid
// filename character, eg /f\oo/ba\r.txt
if (isWindows) {
path = path.replace(/\\/g, _slash);
}
// check for authority as used in UNC shares
// or use the path as given
if (path[0] === _slash && path[1] === _slash) {
const idx = path.indexOf(_slash, 2);
if (idx === -1) {
authority = path.substring(2);
path = _slash;
} else {
authority = path.substring(2, idx);
path = path.substring(idx) || _slash;
}
}
return new Uri('file', authority, path, _empty, _empty);
}
static from(components: {
scheme: string;
authority?: string;
path?: string;
query?: string;
fragment?: string;
}): URI {
return new Uri(
components.scheme,
components.authority,
components.path,
components.query,
components.fragment
);
}
/**
* Join a URI path with path fragments and normalizes the resulting path.
*
* @param uri The input URI.
* @param pathFragment The path fragment to add to the URI path.
* @returns The resulting URI.
*/
static joinPath(uri: URI, ...pathFragment: string[]): URI {
if (!uri.path) {
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
}
let newPath: string;
if (isWindows && uri.scheme === 'file') {
newPath = URI.file(
paths.win32.join(uriToFsPath(uri, true), ...pathFragment)
).path;
} else {
newPath = paths.posix.join(uri.path, ...pathFragment);
}
return uri.with({ path: newPath });
}
// ---- printing/externalize ---------------------------
/**
* Creates a string representation for this URI. It's guaranteed that calling
* `URI.parse` with the result of this function creates an URI which is equal
* to this URI.
*
* * The result shall *not* be used for display purposes but for externalization or transport.
* * The result will be encoded using the percentage encoding and encoding happens mostly
* ignore the scheme-specific encoding rules.
*
* @param skipEncoding Do not encode the result, default is `false`
*/
toString(skipEncoding: boolean = false): string {
return _asFormatted(this, skipEncoding);
}
toJSON(): UriComponents {
return this;
}
static revive(data: UriComponents | URI): URI;
static revive(data: UriComponents | URI | undefined): URI | undefined;
static revive(data: UriComponents | URI | null): URI | null;
static revive(
data: UriComponents | URI | undefined | null
): URI | undefined | null;
static revive(
data: UriComponents | URI | undefined | null
): URI | undefined | null {
if (!data) {
return data;
} else if (data instanceof URI) {
return data;
} else {
const result = new Uri(data);
result._formatted = (data as UriState).external;
result._fsPath =
(data as UriState)._sep === _pathSepMarker
? (data as UriState).fsPath
: null;
return result;
}
}
}
export interface UriComponents {
scheme: string;
authority: string;
path: string;
query: string;
fragment: string;
}
interface UriState extends UriComponents {
$mid: number;
external: string;
fsPath: string;
_sep: 1 | undefined;
}
const _pathSepMarker = isWindows ? 1 : undefined;
// This class exists so that URI is compatibile with vscode.Uri (API).
class Uri extends URI {
_formatted: string | null = null;
_fsPath: string | null = null;
get fsPath(): string {
if (!this._fsPath) {
this._fsPath = uriToFsPath(this, false);
}
return this._fsPath;
}
toString(skipEncoding: boolean = false): string {
if (!skipEncoding) {
if (!this._formatted) {
this._formatted = _asFormatted(this, false);
}
return this._formatted;
} else {
// we don't cache that
return _asFormatted(this, true);
}
}
toJSON(): UriComponents {
const res = {
$mid: 1,
} as UriState;
// cached state
if (this._fsPath) {
res.fsPath = this._fsPath;
res._sep = _pathSepMarker;
}
if (this._formatted) {
res.external = this._formatted;
}
// uri components
if (this.path) {
res.path = this.path;
}
if (this.scheme) {
res.scheme = this.scheme;
}
if (this.authority) {
res.authority = this.authority;
}
if (this.query) {
res.query = this.query;
}
if (this.fragment) {
res.fragment = this.fragment;
}
return res;
}
}
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
const encodeTable: { [ch: number]: string } = {
[CharCode.Colon]: '%3A', // gen-delims
[CharCode.Slash]: '%2F',
[CharCode.QuestionMark]: '%3F',
[CharCode.Hash]: '%23',
[CharCode.OpenSquareBracket]: '%5B',
[CharCode.CloseSquareBracket]: '%5D',
[CharCode.AtSign]: '%40',
[CharCode.ExclamationMark]: '%21', // sub-delims
[CharCode.DollarSign]: '%24',
[CharCode.Ampersand]: '%26',
[CharCode.SingleQuote]: '%27',
[CharCode.OpenParen]: '%28',
[CharCode.CloseParen]: '%29',
[CharCode.Asterisk]: '%2A',
[CharCode.Plus]: '%2B',
[CharCode.Comma]: '%2C',
[CharCode.Semicolon]: '%3B',
[CharCode.Equals]: '%3D',
[CharCode.Space]: '%20',
};
function encodeURIComponentFast(
uriComponent: string,
allowSlash: boolean
): string {
let res: string | undefined = undefined;
let nativeEncodePos = -1;
for (let pos = 0; pos < uriComponent.length; pos++) {
const code = uriComponent.charCodeAt(pos);
// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
if (
(code >= CharCode.a && code <= CharCode.z) ||
(code >= CharCode.A && code <= CharCode.Z) ||
(code >= CharCode.Digit0 && code <= CharCode.Digit9) ||
code === CharCode.Dash ||
code === CharCode.Period ||
code === CharCode.Underline ||
code === CharCode.Tilde ||
(allowSlash && code === CharCode.Slash)
) {
// check if we are delaying native encode
if (nativeEncodePos !== -1) {
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
nativeEncodePos = -1;
}
// check if we write into a new string (by default we try to return the param)
if (res !== undefined) {
res += uriComponent.charAt(pos);
}
} else {
// encoding needed, we need to allocate a new string
if (res === undefined) {
res = uriComponent.substr(0, pos);
}
// check with default table first
const escaped = encodeTable[code];
if (escaped !== undefined) {
// check if we are delaying native encode
if (nativeEncodePos !== -1) {
res += encodeURIComponent(
uriComponent.substring(nativeEncodePos, pos)
);
nativeEncodePos = -1;
}
// append escaped variant to result
res += escaped;
} else if (nativeEncodePos === -1) {
// use native encode only when needed
nativeEncodePos = pos;
}
}
}
if (nativeEncodePos !== -1) {
res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
}
return res !== undefined ? res : uriComponent;
}
function encodeURIComponentMinimal(path: string): string {
let res: string | undefined = undefined;
for (let pos = 0; pos < path.length; pos++) {
const code = path.charCodeAt(pos);
if (code === CharCode.Hash || code === CharCode.QuestionMark) {
if (res === undefined) {
res = path.substr(0, pos);
}
res += encodeTable[code];
} else {
if (res !== undefined) {
res += path[pos];
}
}
}
return res !== undefined ? res : path;
}
/**
* Compute `fsPath` for the given uri
*/
export function uriToFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
let value: string;
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
// unc path: file://shares/c$/far/boo
value = `//${uri.authority}${uri.path}`;
} else if (
uri.path.charCodeAt(0) === CharCode.Slash &&
((uri.path.charCodeAt(1) >= CharCode.A &&
uri.path.charCodeAt(1) <= CharCode.Z) ||
(uri.path.charCodeAt(1) >= CharCode.a &&
uri.path.charCodeAt(1) <= CharCode.z)) &&
uri.path.charCodeAt(2) === CharCode.Colon
) {
if (!keepDriveLetterCasing) {
// windows drive letter: file:///c:/far/boo
value = uri.path[1].toLowerCase() + uri.path.substr(2);
} else {
value = uri.path.substr(1);
}
} else {
// other path
value = uri.path;
}
if (isWindows) {
value = value.replace(/\//g, '\\');
}
return value;
}
/**
* Create the external version of a uri
*/
function _asFormatted(uri: URI, skipEncoding: boolean): string {
const encoder = !skipEncoding
? encodeURIComponentFast
: encodeURIComponentMinimal;
let res = '';
let { scheme, authority, path, query, fragment } = uri;
if (scheme) {
res += scheme;
res += ':';
}
if (authority || scheme === 'file') {
res += _slash;
res += _slash;
}
if (authority) {
let idx = authority.indexOf('@');
if (idx !== -1) {
// <user>@<auth>
const userinfo = authority.substr(0, idx);
authority = authority.substr(idx + 1);
idx = userinfo.indexOf(':');
if (idx === -1) {
res += encoder(userinfo, false);
} else {
// <user>:<pass>@<auth>
res += encoder(userinfo.substr(0, idx), false);
res += ':';
res += encoder(userinfo.substr(idx + 1), false);
}
res += '@';
}
authority = authority.toLowerCase();
idx = authority.indexOf(':');
if (idx === -1) {
res += encoder(authority, false);
} else {
// <auth>:<port>
res += encoder(authority.substr(0, idx), false);
res += authority.substr(idx);
}
}
if (path) {
// lower-case windows drive letters in /C:/fff or C:/fff
if (
path.length >= 3 &&
path.charCodeAt(0) === CharCode.Slash &&
path.charCodeAt(2) === CharCode.Colon
) {
const code = path.charCodeAt(1);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
}
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
const code = path.charCodeAt(0);
if (code >= CharCode.A && code <= CharCode.Z) {
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
}
}
// encode the rest of the path
res += encoder(path, true);
}
if (query) {
res += '?';
res += encoder(query, false);
}
if (fragment) {
res += '#';
res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment;
}
return res;
}
// --- decode
function decodeURIComponentGraceful(str: string): string {
try {
return decodeURIComponent(str);
} catch {
if (str.length > 3) {
return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3));
} else {
return str;
}
}
}
const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
function percentDecode(str: string): string {
if (!str.match(_rEncodedAsHex)) {
return str;
}
return str.replace(_rEncodedAsHex, match =>
decodeURIComponentGraceful(match)
);
}

View File

@@ -1,8 +1,10 @@
import { readFileSync } from 'fs';
import { merge } from 'lodash';
import { Logger } from './utils/log';
import { URI } from './common/uri';
export interface FoamConfig {
workspaceFolders: string[];
workspaceFolders: URI[];
includeGlobs: string[];
ignoreGlobs: string[];
get<T>(path: string): T | undefined;
@@ -14,7 +16,7 @@ const DEFAULT_INCLUDES = ['**/*'];
const DEFAULT_IGNORES = ['**/node_modules/**'];
export const createConfigFromObject = (
workspaceFolders: string[],
workspaceFolders: URI[],
include: string[],
ignore: string[],
settings: any
@@ -33,7 +35,7 @@ export const createConfigFromObject = (
};
export const createConfigFromFolders = (
workspaceFolders: string[] | string,
workspaceFolders: URI[] | URI,
options: {
include?: string[];
ignore?: string[];
@@ -43,7 +45,7 @@ export const createConfigFromFolders = (
workspaceFolders = [workspaceFolders];
}
const workspaceConfig: any = workspaceFolders.reduce(
(acc, f) => merge(acc, parseConfig(`${f}/config.json`)),
(acc, f) => merge(acc, parseConfig(URI.joinPath(f, 'config.json'))),
{}
);
// For security reasons local plugins can only be
@@ -52,7 +54,7 @@ export const createConfigFromFolders = (
delete workspaceConfig['experimental']['localPlugins'];
}
const userConfig = parseConfig(`~/.foam/config.json`);
const userConfig = parseConfig(URI.file(`~/.foam/config.json`));
const settings = merge(workspaceConfig, userConfig);
@@ -64,10 +66,10 @@ export const createConfigFromFolders = (
);
};
const parseConfig = (path: string) => {
const parseConfig = (path: URI) => {
try {
return JSON.parse(readFileSync(path, 'utf8'));
return JSON.parse(readFileSync(path.fsPath, 'utf8'));
} catch {
console.warn('Could not read configuration from ' + path);
Logger.debug('Could not read configuration from ' + path);
}
};

View File

@@ -1,16 +1,19 @@
import { Note, NoteLink, URI } from './types';
import { Note, NoteLink } from './types';
import { URI } from './common/uri';
import { NoteGraph, NoteGraphAPI } from './note-graph';
import { FoamConfig } from './config';
import { IDataStore, FileDataStore } from './services/datastore';
import { ILogger } from './utils/log';
import { IDisposable, isDisposable } from './common/lifecycle';
export { IDataStore, FileDataStore };
export { ILogger };
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
export { IDisposable, isDisposable } from './common/lifecycle';
export { Event, Emitter } from './common/event';
export { FoamConfig };
export { IDisposable, isDisposable };
export {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
@@ -40,7 +43,7 @@ export interface Services {
dataStore: IDataStore;
}
export interface Foam {
export interface Foam extends IDisposable {
notes: NoteGraphAPI;
config: FoamConfig;
parse: (uri: URI, text: string, eol: string) => Note;

View File

@@ -7,18 +7,14 @@ import visit from 'unist-util-visit';
import { Parent, Point } from 'unist';
import detectNewline from 'detect-newline';
import os from 'os';
import * as path from 'path';
import { NoteGraphAPI } from './note-graph';
import { NoteLinkDefinition, Note, NoteParser } from './types';
import {
dropExtension,
uriToSlug,
extractHashtags,
extractTagsFromProp,
} from './utils';
import { dropExtension, extractHashtags, extractTagsFromProp } from './utils';
import { uriToSlug, computeRelativePath, getBasename } from './utils/uri';
import { ID } from './types';
import { ParserPlugin } from './plugins';
import { Logger } from './utils/log';
import { URI } from './common/uri';
const tagsPlugin: ParserPlugin = {
name: 'tags',
@@ -45,7 +41,7 @@ const titlePlugin: ParserPlugin = {
},
onDidVisitTree: (tree, note) => {
if (note.title == null) {
note.title = path.parse(note.source.uri).name;
note.title = getBasename(note.source.uri);
}
},
};
@@ -83,12 +79,12 @@ const definitionsPlugin: ParserPlugin = {
const handleError = (
plugin: ParserPlugin,
fnName: string,
uri: string | undefined,
uri: URI | undefined,
e: Error
): void => {
const name = plugin.name || '';
Logger.warn(
`Error while executing [${fnName}] in plugin [${name}] for file [${uri}]`,
`Error while executing [${fnName}] in plugin [${name}] for file [${uri?.path}]`,
e
);
};
@@ -116,7 +112,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
});
const foamParser: NoteParser = {
parse: (uri: string, markdown: string): Note => {
parse: (uri: URI, markdown: string): Note => {
Logger.debug('Parsing:', uri);
markdown = plugins.reduce((acc, plugin) => {
try {
@@ -277,8 +273,8 @@ export function createMarkdownReferences(
return null;
}
const relativePath = path.relative(
path.dirname(source.source.uri),
const relativePath = computeRelativePath(
source.source.uri,
target.source.uri
);

View File

@@ -1,5 +1,6 @@
import { Graph } from 'graphlib';
import { URI, ID, Note, NoteLink } from './types';
import { URI } from './common/uri';
import { ID, Note, NoteLink } from './types';
import { computeRelativeURI, nameToSlug, isSome } from './utils';
import { Event, Emitter } from './common/event';
@@ -54,7 +55,7 @@ export class NoteGraph implements NoteGraphAPI {
this.onDidAddNote = this.onDidAddNoteEmitter.event;
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
this.createIdFromURI = uri => uri;
this.createIdFromURI = uri => uri.path;
}
public setNote(note: Note): GraphNote {

View File

@@ -7,6 +7,7 @@ import { Note } from '../types';
import unified from 'unified';
import { FoamConfig } from '../config';
import { Logger } from '../utils/log';
import { URI } from '../common/uri';
export interface FoamPlugin {
name: string;
@@ -38,15 +39,16 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
if (!isFeatureEnabled) {
return [];
}
const pluginDirs: string[] =
pluginConfig.pluginFolders ?? findPluginDirs(config.workspaceFolders);
const pluginDirs: URI[] =
pluginConfig.pluginFolders?.map(URI.file) ??
findPluginDirs(config.workspaceFolders);
const plugins = await Promise.all(
pluginDirs
.filter(dir => fs.statSync(dir).isDirectory)
.filter(dir => fs.statSync(dir.fsPath).isDirectory)
.map(async dir => {
try {
const pluginFile = path.join(dir, 'index.js');
const pluginFile = path.join(dir.fsPath, 'index.js');
fs.accessSync(pluginFile);
Logger.info(`Found plugin at [${pluginFile}]. Loading..`);
const plugin = validate(await import(pluginFile));
@@ -60,19 +62,22 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
return plugins.filter(isNotNull);
}
function findPluginDirs(workspaceFolders: string[]) {
function findPluginDirs(workspaceFolders: URI[]) {
return workspaceFolders
.map(root => path.join(root, '.foam', 'plugins'))
.map(root => URI.joinPath(root, '.foam', 'plugins'))
.reduce((acc, pluginDir) => {
try {
const content = fs
.readdirSync(pluginDir)
.map(dir => path.join(pluginDir, dir));
return [...acc, ...content.filter(c => fs.statSync(c).isDirectory())];
.readdirSync(pluginDir.fsPath)
.map(dir => URI.joinPath(pluginDir, dir));
return [
...acc,
...content.filter(c => fs.statSync(c.fsPath).isDirectory()),
];
} catch {
return acc;
}
}, [] as string[]);
}, [] as URI[]);
}
function validate(plugin: any): FoamPlugin {

View File

@@ -3,12 +3,31 @@ import { promisify } from 'util';
import micromatch from 'micromatch';
import fs from 'fs';
import { Event, Emitter } from '../common/event';
import { URI } from '../types';
import { URI } from '../common/uri';
import { FoamConfig } from '../config';
import { Logger } from '../utils/log';
import { isSome } from '../utils';
import { IDisposable } from '../common/lifecycle';
const findAllFiles = promisify(glob);
export interface IWatcher {
/**
* An event which fires on file creation.
*/
onDidCreate: Event<URI>;
/**
* An event which fires on file change.
*/
onDidChange: Event<URI>;
/**
* An event which fires on file deletion.
*/
onDidDelete: Event<URI>;
}
/**
* Represents a source of files and content
*/
@@ -29,12 +48,6 @@ export interface IDataStore {
*/
isMatch: (uri: URI) => boolean;
/**
* Filters a list of URIs based on whether they are a match
* in this data store
*/
match: (uris: URI[]) => string[];
/**
* An event which fires on file creation.
*/
@@ -54,26 +67,28 @@ export interface IDataStore {
/**
* File system based data store
*/
export class FileDataStore implements IDataStore {
export class FileDataStore implements IDataStore, IDisposable {
readonly onDidChangeEmitter = new Emitter<URI>();
readonly onDidCreateEmitter = new Emitter<URI>();
readonly onDidDeleteEmitter = new Emitter<URI>();
readonly onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
readonly onDidChange: Event<URI> = this.onDidChangeEmitter.event;
readonly onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
readonly isMatch: (uri: URI) => boolean;
readonly match: (uris: URI[]) => string[];
private _folders: readonly string[];
private _includeGlobs: string[] = [];
private _ignoreGlobs: string[] = [];
private _disposables: IDisposable[] = [];
constructor(config: FoamConfig) {
this._folders = config.workspaceFolders;
constructor(config: FoamConfig, watcher?: IWatcher) {
this._folders = config.workspaceFolders.map(f =>
f.fsPath.replace(/\\/g, '/')
);
Logger.info('Workspace folders: ', this._folders);
let includeGlobs: string[] = [];
let ignoreGlobs: string[] = [];
config.workspaceFolders.forEach(folder => {
this._folders.forEach(folder => {
const withFolder = folderPlusGlob(folder);
includeGlobs.push(
this._includeGlobs.push(
...config.includeGlobs.map(glob => {
if (glob.endsWith('*')) {
glob = `${glob}\\.(md|mdx|markdown)`;
@@ -81,27 +96,59 @@ export class FileDataStore implements IDataStore {
return withFolder(glob);
})
);
ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
this._ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
});
Logger.info('Glob patterns', {
includeGlobs: this._includeGlobs,
ignoreGlobs: this._ignoreGlobs,
});
Logger.debug('Glob patterns', {
includeGlobs,
ignoreGlobs,
});
this.match = (files: URI[]) => {
return micromatch(files, includeGlobs, {
ignore: ignoreGlobs,
if (isSome(watcher)) {
this._disposables.push(
watcher.onDidCreate(async uri => {
if (this.isMatch(uri)) {
Logger.info(`Created: ${uri.path}`);
this.onDidCreateEmitter.fire(uri);
}
}),
watcher.onDidChange(uri => {
if (this.isMatch(uri)) {
Logger.info(`Updated: ${uri.path}`);
this.onDidChangeEmitter.fire(uri);
}
}),
watcher.onDidDelete(uri => {
if (this.isMatch(uri)) {
Logger.info(`Deleted: ${uri.path}`);
this.onDidDeleteEmitter.fire(uri);
}
})
);
}
}
match(files: URI[]) {
const matches = micromatch(
files.map(f => f.fsPath),
this._includeGlobs,
{
ignore: this._ignoreGlobs,
nocase: true,
});
};
this.isMatch = uri => this.match([uri]).length > 0;
}
);
return matches.map(URI.file);
}
isMatch(uri: URI) {
return this.match([uri]).length > 0;
}
async listFiles() {
const files = (
await Promise.all(
this._folders.map(folder => {
return findAllFiles(folderPlusGlob(folder)('**/*'));
this._folders.map(async folder => {
const res = await findAllFiles(folderPlusGlob(folder)('**/*'));
return res.map(URI.file);
})
)
).flat();
@@ -109,7 +156,11 @@ export class FileDataStore implements IDataStore {
}
async read(uri: URI) {
return (await fs.promises.readFile(uri)).toString();
return (await fs.promises.readFile(uri.fsPath)).toString();
}
dispose() {
this._disposables.forEach(d => d.dispose());
}
}

View File

@@ -1,9 +1,9 @@
// this file can't simply be .d.ts because the TS compiler wouldn't copy it to the dist directory
// see https://stackoverflow.com/questions/56018167/typescript-does-not-copy-d-ts-files-to-build
import { Position, Point } from 'unist';
import { URI } from './common/uri';
export { Position, Point };
export type URI = string;
export type ID = string;
export interface NoteSource {
@@ -42,5 +42,5 @@ export interface Note {
}
export interface NoteParser {
parse: (uri: string, text: string) => Note;
parse: (uri: URI, text: string) => Note;
}

View File

@@ -1,10 +1,11 @@
import path from 'path';
import { posix } from 'path';
import GithubSlugger from 'github-slugger';
import { URI, ID } from '../types';
import { ID } from '../types';
import { hash } from './core';
import { URI } from '../common/uri';
export const uriToSlug = (noteUri: URI): string => {
return GithubSlugger.slug(path.parse(noteUri).name);
return GithubSlugger.slug(posix.parse(noteUri.path).name);
};
export const nameToSlug = (noteName: string): string => {
@@ -12,17 +13,26 @@ export const nameToSlug = (noteName: string): string => {
};
export const hashURI = (uri: URI): ID => {
return hash(path.normalize(uri));
return hash(posix.normalize(uri.path));
};
export const computeRelativePath = (source: URI, target: URI): string => {
const relativePath = posix.relative(posix.dirname(source.path), target.path);
return relativePath;
};
export const getBasename = (uri: URI) => posix.parse(uri.path).name;
export const computeRelativeURI = (
reference: URI,
relativeSlug: string
): URI => {
// if no extension is provided, use the same extension as the source file
const slug =
path.extname(relativeSlug) !== ''
posix.extname(relativeSlug) !== ''
? relativeSlug
: `${relativeSlug}${path.extname(reference)}`;
return path.normalize(path.join(path.dirname(reference), slug));
: `${relativeSlug}${posix.extname(reference.path)}`;
return reference.with({
path: posix.join(posix.dirname(reference.path), slug),
});
};

View File

@@ -1,10 +1,16 @@
import * as path from 'path';
import { createConfigFromFolders } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/common/uri';
Logger.setLevel('error');
const testFolder = URI.joinPath(URI.file(__dirname), 'test-config');
const testFolder = path.join(__dirname, 'test-config');
describe('Foam configuration', () => {
it('can read settings from config.json', () => {
const config = createConfigFromFolders([path.join(testFolder, 'folder1')]);
const config = createConfigFromFolders([
URI.joinPath(testFolder, 'folder1'),
]);
expect(config.get('feature1.setting1.value')).toBeTruthy();
expect(config.get('feature2.value')).toEqual(12);
@@ -14,8 +20,8 @@ describe('Foam configuration', () => {
it('can merge settings from multiple foam folders', () => {
const config = createConfigFromFolders([
path.join(testFolder, 'folder1'),
path.join(testFolder, 'folder2'),
URI.joinPath(testFolder, 'folder1'),
URI.joinPath(testFolder, 'folder2'),
]);
// override value
@@ -31,7 +37,7 @@ describe('Foam configuration', () => {
it('cannot activate local plugins from workspace config', () => {
const config = createConfigFromFolders([
path.join(testFolder, 'enable-plugins'),
URI.joinPath(testFolder, 'enable-plugins'),
]);
expect(config.get('experimental.localPlugins.enabled')).toBeUndefined();
});

View File

@@ -1,6 +1,10 @@
import { NoteGraph, createGraph } from '../src/note-graph';
import { NoteLinkDefinition, Note } from '../src/types';
import { uriToSlug } from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const position = {
start: { line: 1, column: 1 },
@@ -21,7 +25,7 @@ export const createTestNote = (params: {
return {
properties: {},
title: params.title ?? null,
slug: uriToSlug(params.uri),
slug: uriToSlug(URI.file(params.uri)),
definitions: params.definitions ?? [],
tags: new Set(),
links: params.links
@@ -36,7 +40,7 @@ export const createTestNote = (params: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
uri: params.uri,
uri: URI.file(params.uri),
text: params.text ?? '',
},
};

View File

@@ -0,0 +1,72 @@
import { createConfigFromObject } from '../src/config';
import { Logger } from '../src/utils/log';
import { URI } from '../src/common/uri';
import { FileDataStore } from '../src';
Logger.setLevel('error');
const testFolder = URI.joinPath(URI.file(__dirname), 'test-datastore');
function makeConfig(params: { include: string[]; ignore: string[] }) {
return createConfigFromObject(
[testFolder],
params.include,
params.ignore,
{}
);
}
describe('Datastore', () => {
it('defaults to including nothing and exclude nothing', async () => {
const ds = new FileDataStore(
makeConfig({
include: [],
ignore: [],
})
);
expect(await ds.listFiles()).toHaveLength(0);
});
it('returns only markdown files', async () => {
const ds = new FileDataStore(
makeConfig({
include: ['**/*'],
ignore: [],
})
);
const res = toStringSet(await ds.listFiles());
expect(res).toEqual(
makeAbsolute([
'/file-a.md',
'/info/file-b.md',
'/docs/file-in-nm.md',
'/info/docs/file-in-sub-nm.md',
])
);
});
it('supports excludes', async () => {
const ds = new FileDataStore(
makeConfig({
include: ['**/*'],
ignore: ['**/docs/**'],
})
);
const res = toStringSet(await ds.listFiles());
expect(res).toEqual(makeAbsolute(['/file-a.md', '/info/file-b.md']));
});
});
function toStringSet(uris: URI[]) {
return new Set(uris.map(uri => uri.path.toLocaleLowerCase()));
}
function makeAbsolute(files: string[]) {
return new Set(
files.map(f =>
URI.joinPath(testFolder, f)
.path.toLocaleLowerCase()
.replace(/\\/g, '/')
)
);
}

View File

@@ -1,4 +1,7 @@
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
import { Logger } from '../../src/utils/log';
Logger.setLevel('error');
describe('applyTextEdit', () => {
it('should return text with applied TextEdit in the end of the string', () => {

View File

@@ -4,13 +4,17 @@ import { generateHeading } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Services } from '../../src';
import { URI } from '../../src/common/uri';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
Logger.setLevel('error');
describe('generateHeadings', () => {
let _graph: NoteGraphAPI;
beforeAll(async () => {
const config = createConfigFromFolders([
path.join(__dirname, '../__scaffold__'),
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const services: Services = {
dataStore: new FileDataStore(config),

View File

@@ -1,17 +1,21 @@
import * as path from 'path';
import { NoteGraphAPI } from '../../src/note-graph';
import { NoteGraphAPI, GraphNote } from '../../src/note-graph';
import { generateLinkReferences } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Services } from '../../src';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { URI } from '../../src/common/uri';
Logger.setLevel('error');
describe('generateLinkReferences', () => {
let _graph: NoteGraphAPI;
beforeAll(async () => {
const config = createConfigFromFolders([
path.join(__dirname, '../__scaffold__'),
URI.file(path.join(__dirname, '..', '__scaffold__')),
]);
const services: Services = {
dataStore: new FileDataStore(config),
@@ -26,23 +30,26 @@ describe('generateLinkReferences', () => {
it('should add link references to a file that does not have them', () => {
const note = _graph.getNotes({ slug: 'index' })[0];
const expected = {
newText: `
newText: textForNote(
note,
`
[//begin]: # "Autogenerated link references for markdown compatibility"
[first-document]: first-document "First Document"
[second-document]: second-document "Second Document"
[file-without-title]: file-without-title "file-without-title"
[//end]: # "Autogenerated link references"`,
[//end]: # "Autogenerated link references"`
),
range: {
start: {
start: pointForNote(note, {
line: 10,
column: 1,
offset: 140,
},
end: {
}),
end: pointForNote(note, {
line: 10,
column: 1,
offset: 140,
},
}),
},
};
@@ -59,16 +66,16 @@ describe('generateLinkReferences', () => {
const expected = {
newText: '',
range: {
start: {
start: pointForNote(note, {
line: 7,
column: 1,
offset: 105,
},
end: {
}),
end: pointForNote(note, {
line: 9,
column: 43,
offset: 269,
},
}),
},
};
@@ -83,20 +90,23 @@ describe('generateLinkReferences', () => {
const note = _graph.getNotes({ slug: 'first-document' })[0];
const expected = {
newText: `[//begin]: # "Autogenerated link references for markdown compatibility"
newText: textForNote(
note,
`[//begin]: # "Autogenerated link references for markdown compatibility"
[file-without-title]: file-without-title "file-without-title"
[//end]: # "Autogenerated link references"`,
[//end]: # "Autogenerated link references"`
),
range: {
start: {
start: pointForNote(note, {
line: 9,
column: 1,
offset: 145,
},
end: {
}),
end: pointForNote(note, {
line: 11,
column: 43,
offset: 312,
},
}),
},
};
@@ -117,3 +127,34 @@ describe('generateLinkReferences', () => {
expect(actual).toEqual(expected);
});
});
/**
* Will adjust a text line separator to match
* what is used by the note
* Necessary when running tests on windows
*
* @param note the note we are adjusting for
* @param text starting text, using a \n line separator
*/
function textForNote(note: GraphNote, text: string): string {
return text.split('\n').join(note.source.eol);
}
/**
* Will adjust a point to take into account the EOL length
* of the note
* Necessary when running tests on windows
*
* @param note the note we are adjusting for
* @param pos starting position
*/
function pointForNote(
note: GraphNote,
pos: { line: number; column: number; offset: number }
) {
const rows = pos.line - 1;
return {
...pos,
offset: pos.offset - rows + rows * note.source.eol.length,
};
}

View File

@@ -4,6 +4,10 @@ import {
} from '../src/markdown-provider';
import { NoteGraph } from '../src/note-graph';
import { ParserPlugin } from '../src/plugins';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const pageA = `
# Page A
@@ -32,7 +36,8 @@ const pageE = `
# Page E
`;
const createNoteFromMarkdown = createMarkdownParser([]).parse;
const createNoteFromMarkdown = (path: string, content: string) =>
createMarkdownParser([]).parse(URI.file(path), content);
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
@@ -295,7 +300,7 @@ describe('parser plugins', () => {
it('can augment the parsing of the file', async () => {
const note1 = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
This is a test note without headings.
But with some content.
@@ -304,7 +309,7 @@ But with some content.
expect(note1.properties.hasHeading).toBeUndefined();
const note2 = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
# This is a note with header
and some content`

View File

@@ -4,6 +4,10 @@ import { createMarkdownParser } from '../src/markdown-provider';
import { createGraph } from '../src/note-graph';
import { createTestNote } from './core.test';
import { FoamConfig, createConfigFromObject } from '../src/config';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
const config: FoamConfig = createConfigFromObject([], [], [], {
experimental: {
@@ -60,7 +64,7 @@ describe('Foam plugins', () => {
const parser = createMarkdownParser([parserPlugin!]);
const note = parser.parse(
'/path/to/a',
URI.file('/path/to/a'),
`
# This is a note with header
and some content`

View File

@@ -5,14 +5,22 @@ import {
computeRelativeURI,
extractHashtags,
} from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
Logger.setLevel('error');
describe('URI utils', () => {
it('supports various cases', () => {
expect(uriToSlug('/this/is/a/path.md')).toEqual('path');
expect(uriToSlug('../a/relative/path.md')).toEqual('path');
expect(uriToSlug('another/relative/path.md')).toEqual('path');
expect(uriToSlug('no-directory.markdown')).toEqual('no-directory');
expect(uriToSlug('many.dots.name.markdown')).toEqual('manydotsname');
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
'no-directory'
);
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
'manydotsname'
);
});
it('converts a name to a slug', () => {
@@ -23,22 +31,24 @@ describe('URI utils', () => {
});
it('normalizes URI before hashing', () => {
expect(hashURI('/this/is/a/path.md')).toEqual(
hashURI('/this/has/../is/a/path.md')
expect(hashURI(URI.file('/this/is/a/path.md'))).toEqual(
hashURI(URI.file('/this/has/../is/a/path.md'))
);
expect(hashURI('this/is/a/path.md')).toEqual(
hashURI('this/has/../is/a/path.md')
expect(hashURI(URI.file('this/is/a/path.md'))).toEqual(
hashURI(URI.file('this/has/../is/a/path.md'))
);
});
it('computes a relative uri using a slug', () => {
expect(computeRelativeURI('/my/file.md', '../hello.md')).toEqual(
'/hello.md'
expect(computeRelativeURI(URI.file('/my/file.md'), '../hello.md')).toEqual(
URI.file('/hello.md')
);
expect(computeRelativeURI('/my/file.md', '../hello')).toEqual('/hello.md');
expect(computeRelativeURI('/my/file.markdown', '../hello')).toEqual(
'/hello.markdown'
expect(computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
URI.file('/hello.md')
);
expect(
computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
).toEqual(URI.file('/hello.markdown'));
});
});

View File

@@ -170,7 +170,7 @@
"build": "tsc -p ./",
"test": "jest",
"lint": "eslint src --ext ts",
"clean": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --clean",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
"vscode:prepublish": "yarn npm-install && yarn run build",

View File

@@ -24,7 +24,7 @@ async function openDailyNoteFor(date?: Date) {
await focusNote(dailyNotePath, isNew);
}
function getDailyNotePath(configuration: WorkspaceConfiguration, date: Date) {
const rootDirectory = workspace.workspaceFolders[0].uri.fsPath;
const rootDirectory = workspace.workspaceFolders[0].uri.path;
const dailyNoteDirectory: string =
configuration.get("openDailyNote.directory") ?? ".";
const dailyNoteFilename = getDailyNoteFileName(configuration, date);

View File

@@ -1,22 +1,19 @@
"use strict";
import { workspace, ExtensionContext, window } from "vscode";
import { workspace, ExtensionContext, window, Uri } from "vscode";
import {
bootstrap,
FoamConfig,
Foam,
FileDataStore,
Services,
isDisposable,
Logger
Logger,
FileDataStore
} from "foam-core";
import { features } from "./features";
import { getConfigFromVscode } from "./services/config";
import { VsCodeOutputLogger, exposeLogger } from "./services/logging";
let foam: Foam | null = null;
import { VsCodeDataStore } from "./services/datastore";
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -27,24 +24,8 @@ export async function activate(context: ExtensionContext) {
Logger.info("Starting Foam");
const config: FoamConfig = getConfigFromVscode();
const dataStore = new FileDataStore(config);
const watcher = workspace.createFileSystemWatcher("**/*");
watcher.onDidCreate(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidCreateEmitter.fire(uri.fsPath);
}
});
watcher.onDidChange(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidChangeEmitter.fire(uri.fsPath);
}
});
watcher.onDidDelete(uri => {
if (dataStore.isMatch(uri.fsPath)) {
dataStore.onDidDeleteEmitter.fire(uri.fsPath);
}
});
const dataStore = new FileDataStore(config, watcher);
const services: Services = {
dataStore: dataStore
@@ -55,8 +36,10 @@ export async function activate(context: ExtensionContext) {
f.activate(context, foamPromise);
});
foam = await foamPromise;
const foam = await foamPromise;
Logger.info(`Loaded ${foam.notes.getNotes().length} notes`);
context.subscriptions.push(dataStore, foam, watcher);
} catch (e) {
Logger.error("An error occurred while bootstrapping Foam", e);
window.showErrorMessage(
@@ -64,9 +47,3 @@ export async function activate(context: ExtensionContext) {
);
}
}
export function deactivate() {
if (isDisposable(foam)) {
foam?.dispose();
}
}

View File

@@ -11,12 +11,12 @@ import { FoamFeature } from "../types";
import { TextEncoder } from "util";
import { focusNote } from "../utils";
const templatesDir = `${workspace.workspaceFolders[0].uri.fsPath}/.foam/templates`;
const templatesDir = `${workspace.workspaceFolders[0].uri.path}/.foam/templates`;
async function getTemplates(): Promise<string[]> {
const templates = await workspace.findFiles(".foam/templates/**.md");
// parse title, not whole file!
return templates.map(template => path.basename(template.fsPath));
return templates.map(template => path.basename(template.path));
}
const feature: FoamFeature = {
@@ -30,7 +30,7 @@ const feature: FoamFeature = {
const currentDir =
activeFile !== undefined
? path.dirname(activeFile)
: workspace.workspaceFolders[0].uri.fsPath;
: workspace.workspaceFolders[0].uri.path;
const selectedTemplate = await window.showQuickPick(templates);
const folder = await window.showInputBox({
prompt: `Where should the template be created?`,

View File

@@ -27,7 +27,7 @@ const feature: FoamFeature = {
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === "file") {
const note = foam.notes.getNoteByURI(e.document.uri.fsPath);
const note = foam.notes.getNoteByURI(e.document.uri);
if (isSome(note)) {
panel.webview.postMessage({
type: "didSelectNote",
@@ -113,8 +113,7 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
case "webviewDidSelectNode":
const noteId = message.payload;
const noteUri = foam.notes.getNote(noteId).source.uri;
const openPath = vscode.Uri.file(noteUri);
const openPath = foam.notes.getNote(noteId).source.uri;
vscode.workspace.openTextDocument(openPath).then(doc => {
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);

View File

@@ -81,15 +81,15 @@ async function runJanitor(foam: Foam) {
);
const dirtyEditorsFileName = dirtyTextDocuments.map(
dirtyTextDocument => dirtyTextDocument.fileName
dirtyTextDocument => dirtyTextDocument.uri.path
);
const dirtyNotes = notes.filter(note =>
dirtyEditorsFileName.includes(note.source.uri)
dirtyEditorsFileName.includes(note.source.uri.path)
);
const nonDirtyNotes = notes.filter(
note => !dirtyEditorsFileName.includes(note.source.uri)
note => !dirtyEditorsFileName.includes(note.source.uri.path)
);
const wikilinkSetting = getWikilinkDefinitionSetting();
@@ -125,7 +125,7 @@ async function runJanitor(foam: Foam) {
text = definitions ? applyTextEdit(text, definitions) : text;
text = heading ? applyTextEdit(text, heading) : text;
return fs.promises.writeFile(note.source.uri, text);
return fs.promises.writeFile(note.source.uri.path, text);
});
await Promise.all(fileWritePromises);
@@ -135,7 +135,7 @@ async function runJanitor(foam: Foam) {
for (const doc of dirtyTextDocuments) {
const editor = await window.showTextDocument(doc);
const note = dirtyNotes.find(
n => n.source.uri === editor.document.fileName
n => n.source.uri.path === editor.document.uri.path
)!;
// Get edits

View File

@@ -123,9 +123,9 @@ export class TagReference extends vscode.TreeItem {
constructor(tag: string, note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.title = note.title;
this.description = note.source.uri;
this.description = note.source.uri.path;
this.tooltip = this.description;
const resourceUri = vscode.Uri.file(note.source.uri);
const resourceUri = note.source.uri;
let selection: vscode.Range | null = null;
// TODO move search fn to core
const lines = note.source.text.split(/\r?\n/);

View File

@@ -72,7 +72,7 @@ const feature: FoamFeature = {
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
foam.notes.setNote(
foam.parse(document.fileName, document.getText(), docConfig.eol)
foam.parse(document.uri, document.getText(), docConfig.eol)
);
}
@@ -138,15 +138,13 @@ function generateReferenceList(
return [];
}
const filePath = doc.fileName;
const note = foam.getNoteByURI(filePath);
const note = foam.getNoteByURI(doc.uri);
// Should never happen as `doc` is usually given by `editor.document`, which
// binds to an opened note.
if (!note) {
console.warn(
`Can't find note for URI ${filePath} before attempting to generate its markdown reference list`
`Can't find note for URI ${doc.uri.path} before attempting to generate its markdown reference list`
);
return [];
}

View File

@@ -6,12 +6,10 @@ import { getIgnoredFilesSetting } from "../settings";
// not be dependent on vscode but at the moment it's convenient
// to leverage it
export const getConfigFromVscode = (): FoamConfig => {
const workspaceFolders = workspace.workspaceFolders.map(
dir => dir.uri.fsPath
);
const excludeGlobs: string[] = getIgnoredFilesSetting();
const workspaceFolders = workspace.workspaceFolders.map(dir => dir.uri);
const excludeGlobs = getIgnoredFilesSetting();
return createConfigFromFolders(workspaceFolders, {
ignore: excludeGlobs
ignore: excludeGlobs.map(g => g.toString())
});
};

View File

@@ -0,0 +1,68 @@
import {
IDataStore,
Event,
URI,
FoamConfig,
IDisposable,
Logger
} from "foam-core";
import { workspace, FileSystemWatcher, EventEmitter } from "vscode";
import { TextDecoder } from "util";
import { isSome } from "../utils";
export class VsCodeDataStore implements IDataStore, IDisposable {
onDidCreateEmitter = new EventEmitter<URI>();
onDidChangeEmitter = new EventEmitter<URI>();
onDidDeleteEmitter = new EventEmitter<URI>();
onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
onDidChange: Event<URI> = this.onDidChangeEmitter.event;
onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
watcher: FileSystemWatcher;
files: URI[];
constructor(private config: FoamConfig) {
this.watcher = workspace.createFileSystemWatcher("**/*");
this.watcher.onDidCreate(async uri => {
await this.listFiles();
if (this.isMatch(uri)) {
Logger.info("Created: ", uri);
this.onDidCreateEmitter.fire(uri);
}
});
this.watcher.onDidChange(uri => {
if (this.isMatch(uri)) {
Logger.info("Updated: ", uri);
this.onDidChangeEmitter.fire(uri);
}
});
this.watcher.onDidDelete(uri => {
if (this.isMatch(uri)) {
Logger.info("Deleted: ", uri);
this.files = this.files.filter(f => f.path !== uri.path);
this.onDidDeleteEmitter.fire(uri);
}
});
}
async listFiles(): Promise<URI[]> {
this.files = await workspace.findFiles(
`{${this.config.includeGlobs.join(",")}}`,
`{${this.config.ignoreGlobs.join(",")}}`
);
return this.files;
}
isMatch(uri: URI): boolean {
return isSome(this.files.find(f => f.path === uri.path));
}
async read(uri: URI): Promise<string> {
return new TextDecoder().decode(await workspace.fs.readFile(uri));
}
dispose(): void {
this.watcher.dispose();
}
}

View File

@@ -1,4 +1,4 @@
import { workspace } from "vscode";
import { workspace, GlobPattern } from "vscode";
import { LogLevel } from "foam-core";
export enum LinkReferenceDefinitionsSetting {
@@ -17,8 +17,11 @@ export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting
}
/** Retrieve the list of file ignoring globs. */
export function getIgnoredFilesSetting(): string[] {
return workspace.getConfiguration("foam.files").get("ignore");
export function getIgnoredFilesSetting(): GlobPattern[] {
return [
...workspace.getConfiguration().get("foam.files.ignore", []),
...Object.keys(workspace.getConfiguration().get("files.exclude", {}))
];
}
/** Retrieves the maximum length for a Graph node title. */