diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b21196..994f194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 1.0.2 (2017-07-13) + +- Got to 100% code coverage + ### 1.0.0 (2017-06-29) - Initial release. \ No newline at end of file diff --git a/package.json b/package.json index ff68690..0bf82ea 100644 --- a/package.json +++ b/package.json @@ -40,20 +40,20 @@ }, "homepage": "https://github.com/danschultequb/qub-typescript-vscode#readme", "engines": { - "vscode": "^1.5.0" + "vscode": "^1.14.0" }, "devDependencies": { - "@types/mocha": "^2.2.41", - "@types/node": "^8.0.7", - "coveralls": "^2.13.1", - "mocha": "^3.4.2", - "nyc": "^11.0.3", - "open-cli": "^1.0.5", - "source-map-support": "^0.4.15", - "typescript": "^2.4.1" + "@types/mocha": "2.2.41", + "@types/node": "8.0.12", + "coveralls": "2.13.1", + "mocha": "3.4.2", + "nyc": "11.0.3", + "open-cli": "1.0.5", + "source-map-support": "0.4.15", + "typescript": "2.4.1" }, "dependencies": { - "qub": "^1.2.2", - "vscode": "^1.1.2" + "qub": "1.2.5", + "vscode": "1.1.4" } } diff --git a/sources/Interfaces.ts b/sources/Interfaces.ts index 7604bcc..af276fb 100644 --- a/sources/Interfaces.ts +++ b/sources/Interfaces.ts @@ -185,6 +185,22 @@ export class TextDocumentChange { } } +/** + * A change that occurred to a recognized parsed document. + */ +export class ParsedDocumentChange extends TextDocumentChange { + constructor(private _parsedDocument: ParsedDocumentType, textEditor: TextEditor, span: qub.Span, text: string) { + super(textEditor, span, text); + } + + /** + * The recognized parsed document that this change occurred to. + */ + public get parsedDocument(): ParsedDocumentType { + return this._parsedDocument; + } +} + /** * A generic interface for the VS Code application platform. @@ -205,7 +221,7 @@ export interface Platform extends Disposable { * Set the cursor index of the active text editor. If no text editor is active, then this * function will do nothing. */ - setCursorIndex(cursorIndex: number): void; + setCursorIndex(cursorCharacterIndex: number): void; /** * Set the function to call when the active text editor in VSCode is changed. @@ -241,13 +257,13 @@ export interface Platform extends Disposable { * Set the function to call when the cursor hovers over a text document with the provided * language identifier. */ - setProvideHoverCallback(languageId: string, callback: (textDocument: TextDocument, index: number) => Hover): Disposable; + setProvideHoverCallback(languageId: string, callback: (textDocument: TextDocument, characterIndex: number) => Hover): Disposable; /** * Set the function to call when one of the completion trigger characters is pressed on a text * document with provided language identifier. */ - setProvideCompletionsCallback(languageId: string, completionTriggerCharacters: string[], callback: (textDocument: TextDocument, index: number) => qub.Iterable): Disposable; + setProvideCompletionsCallback(languageId: string, completionTriggerCharacters: string[], callback: (textDocument: TextDocument, characterIndex: number) => qub.Iterable): Disposable; setProvideFormattedDocumentTextCallback(languageId: string, callback: (textDocument: TextDocument) => string): Disposable; @@ -282,9 +298,339 @@ export interface Platform extends Disposable { * Get a string identifier for the operating system that VS Code is running on. */ getOperatingSystem(): string; +} + +/** + * A generic LanguageExtension class that wraps the VS Code functions and classes into an easier + * interface. All VS Code extensions that parse and edit a textDocument should implement this class. + */ +export abstract class LanguageExtension implements Disposable { + private _disposed: boolean; + private _basicSubscriptions = new qub.SingleLinkList(); + private _languageSubscriptions = new qub.SingleLinkList(); + + private _parsedDocuments = new qub.Map(); + + private _onProvideIssues: (textDocument: ParsedDocumentType) => qub.Iterable; + private _onProvideHoverFunction: (textDocument: ParsedDocumentType, index: number) => Hover; + private _onProvideCompletionsTriggerCharacters: string[]; + private _onProvideCompletionsFunction: (textDocument: ParsedDocumentType, index: number) => qub.Iterable; + private _onProvideFormattedDocumentFunction: (textDocument: ParsedDocumentType) => string; + + private _onParsedDocumentOpened: (parsedDocument: ParsedDocumentType) => void; + private _onParsedDocumentChanged: (parsedDocumentChange: ParsedDocumentChange) => void; + private _onParsedDocumentSaved: (parsedDocument: ParsedDocumentType) => void; + private _onParsedDocumentClosed: (parsedDocument: ParsedDocumentType) => void; + + constructor(private _extensionName: string, private _extensionVersion: string, private _language: string, private _platform: Platform) { + if (this._platform) { + this._basicSubscriptions.add(this._platform.setActiveEditorChangedCallback((activeEditor: TextEditor) => { + if (activeEditor) { + this.updateDocumentParse(activeEditor.getDocument()); + } + })); + + this._basicSubscriptions.add(this._platform.setConfigurationChangedCallback(() => { + const activeEditor: TextEditor = this._platform.getActiveTextEditor(); + if (activeEditor) { + this.updateDocumentParse(activeEditor.getDocument()); + } + })); + + this._basicSubscriptions.add(this._platform.setTextDocumentOpenedCallback((openedTextDocument: TextDocument) => { + this.onDocumentOpened(openedTextDocument); + })); + + this._basicSubscriptions.add(this._platform.setTextDocumentSavedCallback((savedTextDocument: TextDocument) => { + this.onDocumentSaved(savedTextDocument); + })); + } + } + + public dispose(): void { + if (!this._disposed) { + this._disposed = true; + + for (const languageSubscription of this._languageSubscriptions) { + languageSubscription.dispose(); + } + this._languageSubscriptions.clear(); + + for (const basicEventSubscription of this._basicSubscriptions) { + basicEventSubscription.dispose(); + } + this._basicSubscriptions.clear(); + + if (this._platform) { + this._platform.dispose(); + } + } + } + + public get name(): string { + return this._extensionName; + } + + public get version(): string { + return this._extensionVersion; + } + + public getConfigurationValue(propertyPath: string, defaultValue?: T): T { + const configuration: Configuration = this.getConfiguration(); + return configuration ? configuration.get(`${this._extensionName}.${propertyPath}`, defaultValue) : defaultValue; + } + + /** + * Set the function that will be called when VS Code requests for the issues associated with the + * provided textDocument. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnProvideIssues(onProvideIssues: (parsedDocument: ParsedDocumentType) => qub.Iterable): void { + this._onProvideIssues = onProvideIssues; + } + + /** + * Set the function that will be called when VS Code requests for hover information. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnProvideHover(onProvideHover: (parsedDocument: ParsedDocumentType, index: number) => Hover): void { + this._onProvideHoverFunction = onProvideHover; + } + + /** + * Set the function that will be called when a document that this language extension can parse + * is opened. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnParsedDocumentOpened(onParsedDocumentOpened: (parsedDocument: ParsedDocumentType) => void): void { + this._onParsedDocumentOpened = onParsedDocumentOpened; + } + + /** + * Set the function that will be called when a document that this language extension can parse + * is saved. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnParsedDocumentSaved(onParsedDocumentSaved: (parsedDocument: ParsedDocumentType) => void): void { + this._onParsedDocumentSaved = onParsedDocumentSaved; + } + + /** + * Set the function that will be called when a document that this language extension can parse + * is changed. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnParsedDocumentChanged(onParsedDocumentChanged: (parsedDocumentChange: ParsedDocumentChange) => void): void { + this._onParsedDocumentChanged = onParsedDocumentChanged; + } + + /** + * Set the function that will be called when a document that this language extension can parse + * is closed. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnParsedDocumentClosed(onParsedDocumentClosed: (parsedDocument: ParsedDocumentType) => void): void { + this._onParsedDocumentClosed = onParsedDocumentClosed; + } /** - * Write the provided message to the console. + * Get whether an extension with the provided name is installed. */ - consoleLog(message: string): void; + public isExtensionInstalled(publisher: string, extensionName: string): boolean { + return this._platform.isExtensionInstalled(publisher, extensionName); + } + + /** + * Get the configuration that is associated with this extension. + */ + protected getConfiguration(): Configuration { + return this._platform ? this._platform.getConfiguration() : undefined; + } + + /** + * Set the function that will be called when VS Code requests for auto-completion information. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnProvideCompletions(triggerCharacters: string[], onProvideCompletions: (parsedDocument: ParsedDocumentType, index: number) => qub.Iterable): void { + this._onProvideCompletionsTriggerCharacters = triggerCharacters; + this._onProvideCompletionsFunction = onProvideCompletions; + } + + /** + * Set the function that will be called when VS Code requests for the current textDocument to be + * formatted. + * NOTE: The provided function must be a function, and not a method that is dependant on a + * caller object. If you want to call a method, call this setter with a lamda function that + * wraps the method invocation. + */ + public setOnProvideFormattedDocument(onProvideFormattedDocument: (parsedDocument: ParsedDocumentType) => string): void { + this._onProvideFormattedDocumentFunction = onProvideFormattedDocument; + } + + private updateDocumentParse(textDocument: TextDocument): void { + if (textDocument && this.isParsable(textDocument)) { + if (!this._parsedDocuments.any()) { + this._languageSubscriptions.add(this._platform.setTextDocumentChangedCallback((change: TextDocumentChange) => { + this.onTextDocumentChanged(change); + })); + + this._languageSubscriptions.add(this._platform.setTextDocumentClosedCallback((closedTextDocument: TextDocument) => { + this.onDocumentClosed(closedTextDocument); + })); + + if (this._onProvideHoverFunction) { + this._languageSubscriptions.add(this._platform.setProvideHoverCallback(this._language, (textDocument: TextDocument, index: number) => { + return this.onProvideHover(textDocument, index); + })); + } + + if (this._onProvideCompletionsFunction) { + this._languageSubscriptions.add(this._platform.setProvideCompletionsCallback(this._language, this._onProvideCompletionsTriggerCharacters, (textDocument: TextDocument, index: number) => { + return this.onProvideCompletions(textDocument, index); + })); + } + + if (this._onProvideFormattedDocumentFunction) { + this._languageSubscriptions.add(this._platform.setProvideFormattedDocumentTextCallback(this._language, (textDocument: TextDocument) => { + return this.onProvideFormattedText(textDocument); + })); + } + } + + const parsedDocument: ParsedDocumentType = this.parseDocument(textDocument.getText()); + this._parsedDocuments.add(textDocument.getURI(), parsedDocument); + + if (this._onProvideIssues) { + const documentIssues: qub.Iterable = this._onProvideIssues(parsedDocument); + this._platform.setTextDocumentIssues(this._extensionName, textDocument, documentIssues); + } + } + } + + /** + * Determine if the provided textDocument is parsable by this language extension. + */ + protected abstract isParsable(textDocument: TextDocument): boolean; + + /** + * Parse the provided textDocument and return the language specific representation of the textDocument'savedDocument + * parse results. + */ + protected abstract parseDocument(documentText: string): ParsedDocumentType; + + private getParsedDocument(textDocument: TextDocument): ParsedDocumentType { + let parsedDocument: ParsedDocumentType; + + if (textDocument && this.isParsable(textDocument)) { + const documentUri: string = textDocument.getURI(); + parsedDocument = this._parsedDocuments.get(documentUri); + } + + return parsedDocument; + } + + private onTextDocumentChanged(change: TextDocumentChange): void { + this.updateDocumentParse(change.textDocument); + if (this._onParsedDocumentChanged) { + const parsedDocument: ParsedDocumentType = this.getParsedDocument(change.textDocument); + if (parsedDocument) { + const parsedDocumentChange = new ParsedDocumentChange(parsedDocument, change.editor, change.span, change.text); + this._onParsedDocumentChanged(parsedDocumentChange); + } + } + } + + private onDocumentOpened(textDocument: TextDocument): void { + this.updateDocumentParse(textDocument); + + if (this._onParsedDocumentOpened) { + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + this._onParsedDocumentOpened(parsedDocument); + } + } + } + + private onDocumentSaved(textDocument: TextDocument): void { + this.updateDocumentParse(textDocument); + + if (this._onParsedDocumentSaved) { + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + this._onParsedDocumentSaved(parsedDocument); + } + } + } + + private onDocumentClosed(textDocument: TextDocument): void { + if (this._onParsedDocumentClosed) { + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + this._onParsedDocumentClosed(parsedDocument); + } + } + + const hasParsedDocumentsBeforeRemove: boolean = this._parsedDocuments.any(); + + this._parsedDocuments.remove(textDocument.getURI()); + if (this._onProvideIssues) { + this._platform.setTextDocumentIssues(this._extensionName, textDocument, new qub.SingleLinkList()); + } + + const hasParsedDocumentsAfterRemove: boolean = this._parsedDocuments.any(); + + if (hasParsedDocumentsBeforeRemove && !hasParsedDocumentsAfterRemove) { + for (const languageSubscription of this._languageSubscriptions) { + languageSubscription.dispose(); + } + this._languageSubscriptions.clear(); + } + } + + private onProvideHover(textDocument: TextDocument, index: number): Hover { + let result: Hover; + + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + result = this._onProvideHoverFunction(parsedDocument, index); + } + + return result; + } + + private onProvideCompletions(textDocument: TextDocument, index: number): qub.Iterable { + let result: qub.Iterable; + + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + result = this._onProvideCompletionsFunction(parsedDocument, index); + } + + return result; + } + + private onProvideFormattedText(textDocument: TextDocument): string { + let formattedText: string; + + const parsedDocument: ParsedDocumentType = this.getParsedDocument(textDocument); + if (parsedDocument) { + formattedText = this._onProvideFormattedDocumentFunction(parsedDocument); + } + + return formattedText; + } } \ No newline at end of file diff --git a/sources/Mocks.ts b/sources/Mocks.ts index 641ae19..5d70eaf 100644 --- a/sources/Mocks.ts +++ b/sources/Mocks.ts @@ -113,7 +113,7 @@ export class TextEditor implements interfaces.TextEditor { this._cursorIndex = cursorIndex; } - public insert(startIndex: number, text: string): Thenable { + public insert(startIndex: number, text: string): interfaces.Thenable { const documentText: string = this._document.getText(); const beforeInsert: string = startIndex < 0 ? "" : documentText.substr(0, startIndex); const afterInsert: string = startIndex < documentText.length ? documentText.substr(startIndex) : ""; @@ -139,4 +139,264 @@ export class TextEditor implements interfaces.TextEditor { public setNewLine(newline: string): void { this._newline = newline; } +} + +export class Platform implements interfaces.Platform { + private _activeTextEditor: interfaces.TextEditor; + private _configuration: interfaces.Configuration; + private _installedExtensions = new qub.Map>(); + private _textDocumentIssues = new qub.Map>(); + + private _configurationChanged: (newConfiguration: interfaces.Configuration) => void; + private _activeEditorChanged: (editor: interfaces.TextEditor) => void; + private _textDocumentOpened: (openedTextDocument: interfaces.TextDocument) => void; + private _textDocumentSaved: (savedTextDocument: interfaces.TextDocument) => void; + private _textDocumentChanged: (textDocumentChange: interfaces.TextDocumentChange) => void; + private _textDocumentClosed: (closedTextDocument: interfaces.TextDocument) => void; + private _provideHover: (textDocument: interfaces.TextDocument, index: number) => interfaces.Hover; + private _provideCompletions: (textDocument: interfaces.TextDocument, index: number) => qub.Iterable; + private _provideFormattedDocument: (textDocument: interfaces.TextDocument) => string; + + public dispose(): void { + } + + /** + * Invoke a hover action at the provided index of the active text editor. + */ + public getHoverAt(characterIndex: number): interfaces.Hover { + let result: interfaces.Hover; + + if (this._provideHover && qub.isDefined(characterIndex) && this._activeTextEditor) { + const activeDocument: interfaces.TextDocument = this._activeTextEditor.getDocument(); + if (activeDocument) { + result = this._provideHover(activeDocument, characterIndex); + } + } + + return result; + } + + /** + * Invoke a get completions action at the provided index of the active text editor. + */ + public getCompletionsAt(index: number): qub.Iterable { + let result: qub.Iterable; + + if (this._provideCompletions && qub.isDefined(index) && this._activeTextEditor) { + const activeDocument: interfaces.TextDocument = this._activeTextEditor.getDocument(); + if (activeDocument) { + result = this._provideCompletions(activeDocument, index); + } + } + + if (!result) { + result = new qub.SingleLinkList(); + } + + return result; + } + + public getFormattedDocument(): string { + let result: string; + + if (this._provideFormattedDocument && this._activeTextEditor) { + const activeDocument: interfaces.TextDocument = this._activeTextEditor.getDocument(); + if (activeDocument) { + result = this._provideFormattedDocument(activeDocument); + } + } + + return result; + } + + /** + * Add an entry to this mock application's registry of installed extensions. + */ + public addInstalledExtension(publisherName: string, extensionName: string): void { + let publisherExtensions: qub.List = this._installedExtensions.get(publisherName); + if (!publisherExtensions) { + publisherExtensions = new qub.SingleLinkList(); + this._installedExtensions.add(publisherName, publisherExtensions); + } + + if (!publisherExtensions.contains(extensionName)) { + publisherExtensions.add(extensionName); + } + } + + public getActiveTextEditor(): interfaces.TextEditor { + return this._activeTextEditor; + } + + public setActiveTextEditor(activeTextEditor: interfaces.TextEditor): void { + if (this._activeTextEditor !== activeTextEditor) { + this._activeTextEditor = activeTextEditor; + + if (this._activeEditorChanged) { + this._activeEditorChanged(activeTextEditor); + } + } + } + + public getCursorIndex(): number { + return this._activeTextEditor ? this._activeTextEditor.getCursorIndex() : undefined; + } + + public setCursorIndex(cursorIndex: number): void { + if (this._activeTextEditor) { + this._activeTextEditor.setCursorIndex(cursorIndex); + } + } + + public openTextDocument(textDocument: TextDocument): void { + this.setActiveTextEditor(new TextEditor(textDocument)); + + if (this._textDocumentOpened) { + this._textDocumentOpened(textDocument); + } + } + + public saveTextDocument(textDocument: TextDocument): void { + if (this._textDocumentSaved) { + this._textDocumentSaved(textDocument); + } + } + + public closeTextDocument(textDocument: TextDocument): void { + if (this._textDocumentClosed) { + this._textDocumentClosed(textDocument); + } + + const activeTextEditor: interfaces.TextEditor = this.getActiveTextEditor(); + if (activeTextEditor && activeTextEditor.getDocument() === textDocument) { + this.setActiveTextEditor(undefined); + } + } + + /** + * Insert the provided text at the provided startIndex in the active text editor. + */ + public insertText(startIndex: number, text: string): void { + if (this._activeTextEditor && this._activeTextEditor.getDocument()) { + this._activeTextEditor.insert(startIndex, text); + if (this._textDocumentChanged) { + const change = new interfaces.TextDocumentChange(this._activeTextEditor, new qub.Span(startIndex, 0), text); + this._textDocumentChanged(change); + } + } + } + + public setActiveEditorChangedCallback(activeEditorChanged: (editor: interfaces.TextEditor) => void): interfaces.Disposable { + this._activeEditorChanged = activeEditorChanged; + return new Disposable(); + } + + public setConfigurationChangedCallback(callback: () => void): interfaces.Disposable { + this._configurationChanged = callback; + return new Disposable(); + } + + public setTextDocumentOpenedCallback(callback: (openedTextDocument: interfaces.TextDocument) => void): interfaces.Disposable { + this._textDocumentOpened = callback; + return new Disposable(); + } + + public setTextDocumentSavedCallback(callback: (savedTextDocument: interfaces.TextDocument) => void): interfaces.Disposable { + this._textDocumentSaved = callback; + return new Disposable(); + } + + public setTextDocumentChangedCallback(callback: (textDocumentChange: interfaces.TextDocumentChange) => void): interfaces.Disposable { + this._textDocumentChanged = callback; + return new Disposable(); + } + + public setTextDocumentClosedCallback(callback: (closedTextDocument: interfaces.TextDocument) => void): interfaces.Disposable { + this._textDocumentClosed = callback; + return new Disposable(); + } + + public setProvideHoverCallback(languageId: string, callback: (textDocument: interfaces.TextDocument, index: number) => interfaces.Hover): interfaces.Disposable { + this._provideHover = callback; + return new Disposable(); + } + + public setProvideCompletionsCallback(languageId: string, completionTriggerCharacters: string[], callback: (textDocument: interfaces.TextDocument, index: number) => qub.Iterable): interfaces.Disposable { + this._provideCompletions = callback; + return new Disposable(); + } + + public setProvideFormattedDocumentTextCallback(languageId: string, callback: (textDocument: interfaces.TextDocument) => string): interfaces.Disposable { + this._provideFormattedDocument = callback; + return new Disposable(); + } + + public setTextDocumentIssues(extensionName: string, textDocument: interfaces.TextDocument, issues: qub.Iterable): void { + this._textDocumentIssues.add(textDocument ? textDocument.getURI() : undefined, issues); + } + + public getTextDocumentIssues(textDocumentUri: string): qub.Iterable { + let result: qub.Iterable = this._textDocumentIssues.get(textDocumentUri); + if (!result) { + result = new qub.SingleLinkList(); + } + return result; + } + + public getConfiguration(): interfaces.Configuration { + return this._configuration; + } + + public setConfiguration(configuration: interfaces.Configuration): void { + if (this._configuration !== configuration) { + this._configuration = configuration; + if (this._configurationChanged) { + this._configurationChanged(configuration); + } + } + } + + public isExtensionInstalled(publisher: string, extensionName: string): boolean { + const publisherExtensions: qub.Iterable = this._installedExtensions.get(publisher); + return publisherExtensions && publisherExtensions.contains(extensionName) ? true : false; + } + + public getLocale(): string { + return "MOCK_LOCALE"; + } + + public getMachineId(): string { + return "MOCK_MACHINE_ID"; + } + + public getSessionId(): string { + return "MOCK_SESSION_ID"; + } + + public getOperatingSystem(): string { + return "MOCK_OPERATING_SYSTEM"; + } +} + +export class PlaintextDocument { + constructor(private _text: string) { + } + + public getText(): string { + return this._text; + } +} + +export class PlaintextLanguageExtension extends interfaces.LanguageExtension { + constructor(platform: interfaces.Platform) { + super("Plaintext Tools", "1.0.0", "txt", platform); + } + + public isParsable(textDocument: interfaces.TextDocument): boolean { + return textDocument && textDocument.getLanguageId() === "txt" ? true : false; + } + + public parseDocument(documentText: string): PlaintextDocument { + return new PlaintextDocument(documentText); + } } \ No newline at end of file diff --git a/tests/Interfaces.tests.ts b/tests/Interfaces.tests.ts index 334c39c..80a7d3e 100644 --- a/tests/Interfaces.tests.ts +++ b/tests/Interfaces.tests.ts @@ -62,4 +62,42 @@ suite("Interfaces", () => { assert.deepStrictEqual(change.text, "HAPPY"); }); }); + + class PlainTextDocument implements interfaces.TextDocument { + constructor(private _uri: string, private _text: string) { + } + + public getLanguageId(): string { + return "txt"; + } + + public getURI(): string { + return this._uri; + } + + public getText(): string { + return this._text; + } + + public getLineIndex(characterIndex: number): number { + return qub.getLineIndex(this._text, characterIndex); + } + + public getColumnIndex(characterIndex: number): number { + return qub.getColumnIndex(this._text, characterIndex); + } + + public getLineIndent(characterIndex: number): string { + return qub.getLineIndent(this._text, characterIndex); + } + } + + suite("ParsedDocumentChange", () => { + test(`constructor()`, () => { + const parsedDocument = new mocks.TextDocument("MOCK_LANGUAGE_ID", "mock://document.uri"); + const editor = new mocks.TextEditor(parsedDocument); + const change = new interfaces.ParsedDocumentChange(parsedDocument, editor, new qub.Span(0, 1), "Hello!"); + assert.deepStrictEqual(change.parsedDocument, parsedDocument); + }); + }); }); \ No newline at end of file diff --git a/tests/Mocks.tests.ts b/tests/Mocks.tests.ts index 467cfd4..dc0e2ea 100644 --- a/tests/Mocks.tests.ts +++ b/tests/Mocks.tests.ts @@ -1,6 +1,7 @@ import * as assert from "assert"; import * as qub from "qub"; +import * as interfaces from "../sources/Interfaces"; import * as mocks from "../sources/Mocks"; suite("Mocks", () => { @@ -241,4 +242,1171 @@ suite("Mocks", () => { setNewLineTest("\r\n"); }); }); + + suite("Platform", () => { + test("constructor()", () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + assert.deepStrictEqual(platform.getCursorIndex(), undefined); + assert.deepStrictEqual(platform.getConfiguration(), undefined); + assert.deepStrictEqual(platform.getLocale(), "MOCK_LOCALE"); + assert.deepStrictEqual(platform.getMachineId(), "MOCK_MACHINE_ID"); + assert.deepStrictEqual(platform.getSessionId(), "MOCK_SESSION_ID"); + assert.deepStrictEqual(platform.getOperatingSystem(), "MOCK_OPERATING_SYSTEM"); + }); + + test("dispose()", () => { + const platform = new mocks.Platform(); + platform.dispose(); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + assert.deepStrictEqual(platform.getCursorIndex(), undefined); + assert.deepStrictEqual(platform.getConfiguration(), undefined); + assert.deepStrictEqual(platform.getLocale(), "MOCK_LOCALE"); + assert.deepStrictEqual(platform.getMachineId(), "MOCK_MACHINE_ID"); + assert.deepStrictEqual(platform.getSessionId(), "MOCK_SESSION_ID"); + assert.deepStrictEqual(platform.getOperatingSystem(), "MOCK_OPERATING_SYSTEM"); + }); + + suite("getHoverAt()", () => { + test(`with no hover provider`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getHoverAt(0), undefined); + }); + + test(`with no activeTextEditor`, () => { + const platform = new mocks.Platform(); + platform.setProvideHoverCallback("txt", + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new interfaces.Hover(["Hello!"], new qub.Span(0, 1)); + }); + assert.deepStrictEqual(platform.getHoverAt(0), undefined); + }); + + test(`with undefined characterIndex`, () => { + const platform = new mocks.Platform(); + platform.setProvideHoverCallback("txt", + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new interfaces.Hover(["Hello!"], new qub.Span(0, 1)); + }); + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("txt", "mock://document.uri", "Hello?"))); + assert.deepStrictEqual(platform.getHoverAt(undefined), undefined); + }); + + test(`with no active document`, () => { + const platform = new mocks.Platform(); + platform.setProvideHoverCallback("txt", + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new interfaces.Hover(["Hello!"], new qub.Span(0, 1)); + }); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + assert.deepStrictEqual(platform.getHoverAt(3), undefined); + }); + + test(`with hover provider, activeTextEditor, and defined characterIndex`, () => { + const platform = new mocks.Platform(); + platform.setProvideHoverCallback("txt", + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new interfaces.Hover(["Hello!"], new qub.Span(0, 1)); + }); + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("txt", "mock://document.uri", "Hello?"))); + assert.deepStrictEqual(platform.getHoverAt(3), new interfaces.Hover(["Hello!"], new qub.Span(0, 1))); + }); + }); + + suite("getCompletionsAt()", () => { + test(`with no completion provider`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getCompletionsAt(0).toArray(), []); + }); + + test(`with no activeTextEditor`, () => { + const platform = new mocks.Platform(); + platform.setProvideCompletionsCallback("txt", [], + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new qub.SingleLinkList([ + new interfaces.Completion("Hello", new qub.Span(0, 1), "A friendly greeting.") + ]); + }); + assert.deepStrictEqual(platform.getCompletionsAt(0).toArray(), []); + }); + + test(`with undefined characterIndex`, () => { + const platform = new mocks.Platform(); + platform.setProvideCompletionsCallback("txt", [], + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new qub.SingleLinkList([ + new interfaces.Completion("Hello", new qub.Span(0, 1), "A friendly greeting.") + ]); + }); + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("txt", "mock://document.uri", "Hello?"))); + assert.deepStrictEqual(platform.getCompletionsAt(undefined).toArray(), []); + }); + + test(`with no active document`, () => { + const platform = new mocks.Platform(); + platform.setProvideCompletionsCallback("txt", [], + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new qub.SingleLinkList([ + new interfaces.Completion("Hello", new qub.Span(0, 1), "A friendly greeting.") + ]); + }); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + assert.deepStrictEqual(platform.getCompletionsAt(3).toArray(), []); + }); + + test(`with hover provider, activeTextEditor, and defined characterIndex`, () => { + const platform = new mocks.Platform(); + platform.setProvideCompletionsCallback("txt", [], + (textDocument: interfaces.TextDocument, characterIndex: number) => { + return new qub.SingleLinkList([ + new interfaces.Completion("Hello", new qub.Span(0, 1), "A friendly greeting.") + ]); + }); + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("txt", "mock://document.uri", "Hello?"))); + assert.deepStrictEqual(platform.getCompletionsAt(3).toArray(), [ + new interfaces.Completion("Hello", new qub.Span(0, 1), "A friendly greeting.") + ]); + }); + }); + + suite("getFormattedDocument()", () => { + test(`with no format provider`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test(`with no activeTextEditor`, () => { + const platform = new mocks.Platform(); + platform.setProvideFormattedDocumentTextCallback("txt", + (textDocument: interfaces.TextDocument) => { + return "abcd"; + }); + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test(`with no active document`, () => { + const platform = new mocks.Platform(); + platform.setProvideFormattedDocumentTextCallback("txt", + (textDocument: interfaces.TextDocument) => { + return "abcd"; + }); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test(`with format provider and activeTextEditor`, () => { + const platform = new mocks.Platform(); + platform.setProvideFormattedDocumentTextCallback("txt", + (textDocument: interfaces.TextDocument) => { + return "abcd"; + }); + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("txt", "mock://document.uri", "Hello?"))); + assert.deepStrictEqual(platform.getFormattedDocument(), "abcd"); + }); + }); + + suite("addInstalledExtension()", () => { + function addInstalledExtensionTest(publisherName: string, extensionName: string): void { + test(`with ${qub.escapeAndQuote(publisherName)} publisher name and ${qub.escapeAndQuote(extensionName)} extension name`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.isExtensionInstalled(publisherName, extensionName), false); + + platform.addInstalledExtension(publisherName, extensionName); + assert.deepStrictEqual(platform.isExtensionInstalled(publisherName, extensionName), true); + + platform.addInstalledExtension(publisherName, extensionName); + assert.deepStrictEqual(platform.isExtensionInstalled(publisherName, extensionName), true); + }); + } + + addInstalledExtensionTest(undefined, undefined); + addInstalledExtensionTest(undefined, null); + addInstalledExtensionTest(undefined, ""); + addInstalledExtensionTest(undefined, "myExtension"); + + addInstalledExtensionTest(null, undefined); + addInstalledExtensionTest(null, null); + addInstalledExtensionTest(null, ""); + addInstalledExtensionTest(null, "myExtension"); + + addInstalledExtensionTest("", undefined); + addInstalledExtensionTest("", null); + addInstalledExtensionTest("", ""); + addInstalledExtensionTest("", "myExtension"); + + addInstalledExtensionTest("myPublisher", undefined); + addInstalledExtensionTest("myPublisher", null); + addInstalledExtensionTest("myPublisher", ""); + addInstalledExtensionTest("myPublisher", "myExtension"); + }); + + suite("setActiveTextEditor()", () => { + test(`when undefined, set to undefined`, () => { + const platform = new mocks.Platform(); + let changed: boolean = false; + platform.setActiveEditorChangedCallback((newActiveTextEditor: interfaces.TextEditor) => { + changed = true; + }); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + + platform.setActiveTextEditor(undefined); + + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + assert.deepStrictEqual(changed, false); + }); + + test(`when undefined, set to not undefined with no callback`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(undefined)); + }); + + test(`when undefined, set to not undefined with a callback`, () => { + const platform = new mocks.Platform(); + let changed: boolean = false; + platform.setActiveEditorChangedCallback((newActiveTextEditor: interfaces.TextEditor) => { + changed = true; + }); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(undefined)); + assert.deepStrictEqual(changed, true); + }); + }); + + suite("getCursorIndex()", () => { + test(`with no active text editor`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getCursorIndex(), undefined); + }); + + test(`with no active text document`, () => { + const platform = new mocks.Platform(); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + assert.deepStrictEqual(platform.getCursorIndex(), 0); + }); + }); + + suite("setCursorIndex()", () => { + test(`with no active text editor`, () => { + const platform = new mocks.Platform(); + platform.setCursorIndex(15); + assert.deepStrictEqual(platform.getCursorIndex(), undefined); + }); + + test(`with no active text document`, () => { + const platform = new mocks.Platform(); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + platform.setCursorIndex(14); + assert.deepStrictEqual(platform.getCursorIndex(), 14); + }); + }); + + suite("openTextDocument()", () => { + test(`with no open text document callback and undefined text document`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + + platform.openTextDocument(undefined); + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(undefined)); + }); + + test(`with open text document callback and text document`, () => { + const platform = new mocks.Platform(); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + + let openedDocument: interfaces.TextDocument; + platform.setTextDocumentOpenedCallback((openedTextDocument: interfaces.TextDocument) => { + openedDocument = openedTextDocument; + }); + + platform.openTextDocument(new mocks.TextDocument("A", "B", "C")); + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(new mocks.TextDocument("A", "B", "C"))); + assert.deepStrictEqual(openedDocument, new mocks.TextDocument("A", "B", "C")); + }); + }); + + suite("saveTextDocument()", () => { + test(`with no save text document callback and undefined text document`, () => { + const platform = new mocks.Platform(); + platform.saveTextDocument(undefined); + }); + + test(`with save text document callback and text document`, () => { + const platform = new mocks.Platform(); + + let savedDocument: interfaces.TextDocument; + platform.setTextDocumentSavedCallback((savedTextDocument: interfaces.TextDocument) => { + savedDocument = savedTextDocument; + }); + + platform.saveTextDocument(new mocks.TextDocument("A", "B", "C")); + assert.deepStrictEqual(savedDocument, new mocks.TextDocument("A", "B", "C")); + }); + }); + + suite("closeTextDocument()", () => { + test(`with no close text document callback and no active text document`, () => { + const platform = new mocks.Platform(); + platform.closeTextDocument(undefined); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + }); + + test(`with close text document callback and no active text document`, () => { + const platform = new mocks.Platform(); + + let closedDocument: interfaces.TextDocument; + platform.setTextDocumentClosedCallback((closedTextDocument: interfaces.TextDocument) => { + closedDocument = closedTextDocument; + }); + + platform.closeTextDocument(new mocks.TextDocument("A", "B", "C")); + assert.deepStrictEqual(closedDocument, new mocks.TextDocument("A", "B", "C")); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + }); + + test(`with close text document callback and different active text document`, () => { + const platform = new mocks.Platform(); + + let closedDocument: interfaces.TextDocument; + platform.setTextDocumentClosedCallback((closedTextDocument: interfaces.TextDocument) => { + closedDocument = closedTextDocument; + }); + + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("A", "B", "C"))); + + platform.closeTextDocument(new mocks.TextDocument("D", "E", "F")); + assert.deepStrictEqual(closedDocument, new mocks.TextDocument("D", "E", "F")); + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(new mocks.TextDocument("A", "B", "C"))); + }); + + test(`with close text document callback and equal active text document`, () => { + const platform = new mocks.Platform(); + + let closedDocument: interfaces.TextDocument; + platform.setTextDocumentClosedCallback((closedTextDocument: interfaces.TextDocument) => { + closedDocument = closedTextDocument; + }); + + const activeTextDocument = new mocks.TextDocument("A", "B", "C"); + platform.setActiveTextEditor(new mocks.TextEditor(activeTextDocument)); + + platform.closeTextDocument(new mocks.TextDocument("A", "B", "C")); + assert.deepStrictEqual(closedDocument, activeTextDocument); + assert.deepStrictEqual(platform.getActiveTextEditor(), new mocks.TextEditor(activeTextDocument)); + }); + + test(`with close text document callback and same active text document`, () => { + const platform = new mocks.Platform(); + + let closedDocument: interfaces.TextDocument; + platform.setTextDocumentClosedCallback((closedTextDocument: interfaces.TextDocument) => { + closedDocument = closedTextDocument; + }); + + const activeTextDocument = new mocks.TextDocument("A", "B", "C"); + platform.setActiveTextEditor(new mocks.TextEditor(activeTextDocument)); + + platform.closeTextDocument(activeTextDocument); + assert.deepStrictEqual(closedDocument, activeTextDocument); + assert.deepStrictEqual(platform.getActiveTextEditor(), undefined); + }); + }); + + suite("insertText()", () => { + test(`with no active text editor`, () => { + const platform = new mocks.Platform(); + + let documentChange: interfaces.TextDocumentChange; + platform.setTextDocumentChangedCallback((textDocumentChange: interfaces.TextDocumentChange) => { + documentChange = textDocumentChange; + }); + + platform.insertText(0, "test"); + + assert.deepStrictEqual(documentChange, undefined); + }); + + test(`with no active text document`, () => { + const platform = new mocks.Platform(); + + let documentChange: interfaces.TextDocumentChange; + platform.setTextDocumentChangedCallback((textDocumentChange: interfaces.TextDocumentChange) => { + documentChange = textDocumentChange; + }); + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + + platform.insertText(0, "test"); + + assert.deepStrictEqual(documentChange, undefined); + }); + + test(`with active text document but no text document change callback`, () => { + const platform = new mocks.Platform(); + + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("A", "B", "C"))); + + platform.insertText(0, "test"); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument().getText(), "testC"); + assert.deepStrictEqual(platform.getCursorIndex(), 4); + }); + + test(`with active text document and text document change callback`, () => { + const platform = new mocks.Platform(); + + platform.setActiveTextEditor(new mocks.TextEditor(new mocks.TextDocument("A", "B", "C"))); + + let change: interfaces.TextDocumentChange; + platform.setTextDocumentChangedCallback((textDocumentChange: interfaces.TextDocumentChange) => { + change = textDocumentChange; + }); + + platform.insertText(0, "test"); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument().getText(), "testC"); + assert.deepStrictEqual(platform.getCursorIndex(), 4); + + const changeEditor = new mocks.TextEditor(new mocks.TextDocument("A", "B", "testC")); + changeEditor.setCursorIndex(4); + assert.deepStrictEqual(change, new interfaces.TextDocumentChange(changeEditor, new qub.Span(0, 0), "test")); + }); + }); + + suite("setConfigurationChangedCallback()", () => { + test("with undefined", () => { + const platform = new mocks.Platform(); + platform.setConfigurationChangedCallback(undefined); + }); + + test("with null", () => { + const platform = new mocks.Platform(); + platform.setConfigurationChangedCallback(null); + }); + }); + + suite("setTextDocumentIssues()", () => { + test("with undefined arguments", () => { + const platform = new mocks.Platform(); + platform.setTextDocumentIssues(undefined, undefined, undefined); + assert.deepStrictEqual(platform.getTextDocumentIssues(undefined).toArray(), []); + }); + + test(`with a single issue`, () => { + const platform = new mocks.Platform(); + platform.setTextDocumentIssues(undefined, new mocks.TextDocument("A", "B", "C"), new qub.ArrayList([qub.Error("D", new qub.Span(0, 1))])); + assert.deepStrictEqual(platform.getTextDocumentIssues("B").toArray(), [qub.Error("D", new qub.Span(0, 1))]); + }); + }); + + suite("setConfiguration()", () => { + test("with undefined", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(undefined); + assert.deepStrictEqual(platform.getConfiguration(), undefined); + }); + + test("with null", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(null); + assert.deepStrictEqual(platform.getConfiguration(), null); + }); + + test("with empty configuration", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(new mocks.Configuration()); + assert.deepStrictEqual(platform.getConfiguration(), new mocks.Configuration()); + }); + + test("with configuration values but no callback", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(new mocks.Configuration({ "apples": 50 })); + assert.deepStrictEqual(platform.getConfiguration(), new mocks.Configuration({ "apples": 50 })); + }); + + test("with configuration values and callback", () => { + const platform = new mocks.Platform(); + + let configurationChanged: boolean = false; + platform.setConfigurationChangedCallback(() => { + configurationChanged = true; + }); + + platform.setConfiguration(new mocks.Configuration({ "apples": 51 })); + + assert.deepStrictEqual(platform.getConfiguration(), new mocks.Configuration({ "apples": 51 })); + assert.deepStrictEqual(configurationChanged, true); + }); + }); + }); + + suite("PlaintextDocument", () => { + test("constructor()", () => { + const document = new mocks.PlaintextDocument("I'm some plaintext."); + assert.deepStrictEqual(document.getText(), "I'm some plaintext."); + }); + }); + + suite("PlaintextLanguageExtension", () => { + suite("constructor()", () => { + test("with no platform", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.name, "Plaintext Tools"); + assert.deepStrictEqual(extension.version, "1.0.0"); + }); + + test("with a platform", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.name, "Plaintext Tools"); + assert.deepStrictEqual(extension.version, "1.0.0"); + + platform.setActiveTextEditor(new mocks.TextEditor(undefined)); + platform.setActiveTextEditor(undefined); + platform.setConfiguration(new mocks.Configuration({})); + platform.openTextDocument(new mocks.TextDocument("X", "Y", "Z")); + platform.saveTextDocument(new mocks.TextDocument("L", "M", "N")); + }); + }); + + suite("isParsable()", () => { + test("with undefined", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.isParsable(undefined), false); + }); + + test("with null", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.isParsable(null), false); + }); + + test("with document with no language id", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + const document = new mocks.TextDocument(undefined, "mock://document.uri"); + assert.deepStrictEqual(extension.isParsable(document), false); + }); + + test("with XML document", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + const document = new mocks.TextDocument("xml", "mock://document.uri"); + assert.deepStrictEqual(extension.isParsable(document), false); + }); + + test("with TXT document", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + const document = new mocks.TextDocument("TXT", "mock://document.uri"); + assert.deepStrictEqual(extension.isParsable(document), false); + }); + + test("with txt document", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + const document = new mocks.TextDocument("txt", "mock://document.uri"); + assert.deepStrictEqual(extension.isParsable(document), true); + }); + }); + + suite("parseDocument()", () => { + test(`with undefined`, () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.parseDocument(undefined), new mocks.PlaintextDocument(undefined)); + }); + + test(`with null`, () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.parseDocument(null), new mocks.PlaintextDocument(null)); + }); + + test(`with ""`, () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.parseDocument(""), new mocks.PlaintextDocument("")); + }); + + test(`with "Shopping List:"`, () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.parseDocument("Shopping List:"), new mocks.PlaintextDocument("Shopping List:")); + }); + }); + + suite("dispose()", () => { + test("when not disposed", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + extension.dispose(); + }); + + test("when disposed", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + extension.dispose(); + + extension.dispose(); + }); + + test("with a platform", () => { + const extension = new mocks.PlaintextLanguageExtension(new mocks.Platform()); + extension.dispose(); + }); + }); + + suite("getConfigurationValue()", () => { + test("with no platform set", () => { + const extension = new mocks.PlaintextLanguageExtension(undefined); + assert.deepStrictEqual(extension.getConfigurationValue(undefined), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(null), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(""), undefined); + assert.deepStrictEqual(extension.getConfigurationValue("apples"), undefined); + }); + + test("with no configuration set", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.getConfigurationValue(undefined), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(null), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(""), undefined); + assert.deepStrictEqual(extension.getConfigurationValue("apples"), undefined); + }); + + test("with empty configuration set", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(new mocks.Configuration({})); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.getConfigurationValue(undefined), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(null), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(""), undefined); + assert.deepStrictEqual(extension.getConfigurationValue("apples"), undefined); + }); + + test("with no extension configuration set", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(new mocks.Configuration({ "apples": 1 })); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.getConfigurationValue(undefined), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(null), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(""), undefined); + assert.deepStrictEqual(extension.getConfigurationValue("apples"), undefined); + }); + + test("with extension configuration set", () => { + const platform = new mocks.Platform(); + platform.setConfiguration(new mocks.Configuration({ "Plaintext Tools": { "apples": 1 } })); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.getConfigurationValue(undefined), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(null), undefined); + assert.deepStrictEqual(extension.getConfigurationValue(""), undefined); + assert.deepStrictEqual(extension.getConfigurationValue("apples"), 1); + }); + }); + + suite("setOnProvideIssues()", () => { + test("with undefined provider", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + extension.setOnProvideIssues(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I have a red dog.")); + + assert.deepStrictEqual(platform.getTextDocumentIssues("mock://document.uri").toArray(), []); + + extension.dispose(); + }); + + test("with provider", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + extension.setOnProvideIssues((parsedDocument: mocks.PlaintextDocument) => { + return new qub.SingleLinkList([qub.Error("No red allowed", new qub.Span(9, 3))]); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I have a red dog.")); + + assert.deepStrictEqual(platform.getTextDocumentIssues("mock://document.uri").toArray(), [qub.Error("No red allowed", new qub.Span(9, 3))]); + }); + }); + + suite("setOnProvideHover()", () => { + test("with undefined provider", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + extension.setOnProvideHover(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(platform.getHoverAt(9), undefined); + }); + + test("with provider", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + extension.setOnProvideHover((parsedDocument: mocks.PlaintextDocument, characterIndex: number) => { + return new interfaces.Hover(["Blue"], new qub.Span(7, 4)); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I have a red dog.")); + + const hover: interfaces.Hover = platform.getHoverAt(9); + assert.deepStrictEqual(hover, new interfaces.Hover(["Blue"], new qub.Span(7, 4))); + }); + + test("with provider but non-plaintext document", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + extension.setOnProvideHover((parsedDocument: mocks.PlaintextDocument, characterIndex: number) => { + return new interfaces.Hover(["Blue"], new qub.Span(7, 4)); + }); + + // Must open a plaintext document first to register all of our language specific + // event handlers. + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I have a red dog.")); + + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document2.uri", "I have a red dog.")); + + assert.deepStrictEqual(platform.getHoverAt(9), undefined); + }); + }); + + suite("setOnParsedDocumentOpened()", () => { + test("with undefined provider", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentOpened(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + }); + + test("with provider but non-plaintext document", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentOpened((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, undefined); + }); + + test("with provider and plaintext document", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentOpened((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + }); + }); + + suite("setOnParsedDocumentSaved()", () => { + test("with undefined provider before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentSaved(undefined); + + platform.saveTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + }); + + test("with undefined provider after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentSaved(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.saveTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + }); + + test("with provider but non-plaintext document", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentSaved((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.saveTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, undefined); + }); + + test("with provider and plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentSaved((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.saveTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + }); + + test("with provider and plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentSaved((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.saveTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + }); + }); + + suite("setOnParsedDocumentClosed()", () => { + test("with undefined callback before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentClosed(undefined); + + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + }); + + test("with undefined callback after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentClosed(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + }); + + test("with callback but non-plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.closeTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, undefined); + }); + + test("with callback but non-plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + platform.closeTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, undefined); + }); + + test("with callback and plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, undefined); + }); + + test("with callback and plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + }); + + test("with plaintext document issues", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + extension.setOnProvideIssues((parsedDocument: mocks.PlaintextDocument) => { + return new qub.SingleLinkList([qub.Error("No red allowed", new qub.Span(9, 3))]); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.setTextDocumentIssues(extension.name, new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese."), new qub.SingleLinkList([qub.Error("Error!", new qub.Span(0, 5))])); + assert.deepStrictEqual(platform.getTextDocumentIssues("mock://document.uri").toArray(), [qub.Error("Error!", new qub.Span(0, 5))]); + + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + assert.deepStrictEqual(platform.getTextDocumentIssues("mock://document.uri").toArray(), []); + }); + + test("with multiple plaintext documents open", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentClosed((parsedDocument: mocks.PlaintextDocument) => { + documentText = parsedDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri2", "I like blue cheese too!")); + + platform.closeTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(documentText, "I like blue cheese."); + }); + }); + + suite("setOnParsedDocumentChanged()", () => { + test("with undefined callback after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnParsedDocumentChanged(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.insertText(2, "don't "); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument(), new mocks.TextDocument("txt", "mock://document.uri", "I don't like blue cheese.")); + }); + + test("with provider but non-plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentChanged((change: interfaces.TextDocumentChange) => { + documentText = change.textDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + platform.insertText(2, "don't "); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument(), new mocks.TextDocument("xml", "mock://document.uri", "I don't like blue cheese.")); + assert.deepStrictEqual(documentText, undefined); + }); + + test("with provider but non-plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentChanged((change: interfaces.TextDocumentChange) => { + documentText = change.textDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.xml", "I like blue cheese too.")); + platform.insertText(2, "don't "); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument(), new mocks.TextDocument("xml", "mock://document.xml", "I don't like blue cheese too.")); + assert.deepStrictEqual(documentText, undefined); + }); + + test("with provider and plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + let documentText: string; + extension.setOnParsedDocumentChanged((change: interfaces.TextDocumentChange) => { + documentText = change.textDocument.getText(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.insertText(2, "don't "); + + assert.deepStrictEqual(platform.getActiveTextEditor().getDocument(), new mocks.TextDocument("txt", "mock://document.uri", "I don't like blue cheese.")); + assert.deepStrictEqual(documentText, "I don't like blue cheese."); + }); + }); + + suite("isExtensionInstalled()", () => { + test("with undefined publisher name and extension name", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.isExtensionInstalled(undefined, undefined), false); + }); + + test(`with "A" publisher name and "B" extension name when not installed`, () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.isExtensionInstalled("A", "B"), false); + }); + + test(`with "A" publisher name and "B" extension name when installed`, () => { + const platform = new mocks.Platform(); + platform.addInstalledExtension("A", "B"); + + const extension = new mocks.PlaintextLanguageExtension(platform); + assert.deepStrictEqual(extension.isExtensionInstalled("A", "B"), true); + }); + }); + + suite("setOnProvideCompletions()", () => { + test("with undefined callback before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideCompletions([], undefined); + + assert.deepStrictEqual(platform.getCompletionsAt(5).toArray(), []); + }); + + test("with undefined callback after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideCompletions([], undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(platform.getCompletionsAt(5).toArray(), []); + }); + + test("with callback but non-plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideCompletions([], (parsedDocument: mocks.PlaintextDocument, characterIndex: number) => { + return new qub.SingleLinkList([new interfaces.Completion("A", new qub.Span(1, 2), "B")]); + }); + + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(platform.getCompletionsAt(5).toArray(), []); + }); + + test("with callback but non-plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideCompletions([], (parsedDocument: mocks.PlaintextDocument, characterIndex: number) => { + return new qub.SingleLinkList([new interfaces.Completion("A", new qub.Span(1, 2), "B")]); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.xml", "I like blue cheese too.")); + + assert.deepStrictEqual(platform.getCompletionsAt(5).toArray(), []); + }); + + test("with callback and plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideCompletions([], (parsedDocument: mocks.PlaintextDocument, characterIndex: number) => { + return new qub.SingleLinkList([new interfaces.Completion("A", new qub.Span(1, 2), "B")]); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(platform.getCompletionsAt(5).toArray(), [new interfaces.Completion("A", new qub.Span(1, 2), "B")]); + }); + }); + + suite("setOnProvideFormattedDocument()", () => { + test("with undefined provider before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideFormattedDocument(undefined); + + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test("with undefined provider after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideFormattedDocument(undefined); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", "I like blue cheese.")); + + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test("with provider but non-plaintext document before plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideFormattedDocument((parsedDocument: mocks.PlaintextDocument) => { + return parsedDocument.getText().trim(); + }); + + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.uri", " I like blue cheese. ")); + + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test("with provider but non-plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideFormattedDocument((parsedDocument: mocks.PlaintextDocument) => { + return parsedDocument.getText().trim(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", " I like blue cheese. ")); + platform.openTextDocument(new mocks.TextDocument("xml", "mock://document.xml", " I like blue cheese too. ")); + + assert.deepStrictEqual(platform.getFormattedDocument(), undefined); + }); + + test("with provider and plaintext document after plaintext document opened", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + extension.setOnProvideFormattedDocument((parsedDocument: mocks.PlaintextDocument) => { + return parsedDocument.getText().trim(); + }); + + platform.openTextDocument(new mocks.TextDocument("txt", "mock://document.uri", " I like blue cheese. ")); + + assert.deepStrictEqual(platform.getFormattedDocument(), "I like blue cheese."); + }); + }); + + test("on configuration changed with active text editor", () => { + const platform = new mocks.Platform(); + const extension = new mocks.PlaintextLanguageExtension(platform); + + platform.openTextDocument(new mocks.TextDocument("txt", "B", "C")); + + platform.setConfiguration(new mocks.Configuration()); + }); + }); }); \ No newline at end of file