Skip to content

Commit

Permalink
Ignored Word Lists (#55)
Browse files Browse the repository at this point in the history
* version -> 0.3.0

* prepped for auto deployment

* only deploy if os = linux

* use condition instead of if, and specify master branch

* added skip_cleanup

* started dictionary class

* added dictionary

* actually made it a class

* consolidated loadConfiguration and reloadConfiguration

* expanded LTDictionary class

* updated dependencies

* dependency updates

* attempt at lt-service class

* cleaned up duplicated commands

* bump engine to 1.39.0

* removed service for later

* moved dictionary to linter folder, use state stores

* added extension context

* added dictionary

* undo changes

* fixed command titles / categories

* linter is codeactionprovider

* refactor

* launch in test workspace

* removed unused code

* manage ignored words in configuration manager

* added ignored words lists and scoped managed settings

* added ignored words lists

* Use sets to manage word lists. Added word list save methods.

* removed scopes on managed settings. will apply later.

* added spelling test document

* lint plain text as annotated text

* register ignore word commands

* added ignored lists features.

* fixed setting constant

* split suggest into resuable functions

* Cast set to arrays.

* added more to spell check use case

* registered remove ignored word commands.

* added new commands to test

* added boolean to show hints for ignored words

* fixed copy / paste errors

* updated test workstation settings

* added methos for getting ignored words

* added ability to hide hints for ignored words

* updated changelog for ignored words

* added to test case.

* ignored words case insensitive

* fixed saving ignored word lists

* labeled changelog for release
  • Loading branch information
davidlday authored Nov 30, 2019
1 parent 463a988 commit 7917725
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 29 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to the "languagetool-linter" extension will be documented in

## [Unreleased]

## [0.9.0] - 2019-11-29

### Added

* Ignored Words lists at the User and Workspace levels.
* The Workspace level list appears in the User settings, but is ignored. This is due to how settings work as overrides instead of cumulative.
* Ignored words have an optional hint shown to remove the word from the ignored list.

## [0.8.1] - 2019-11-17

### Fixed
Expand Down
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,23 @@
"type": "string",
"default": "",
"description": "IDs of categories to be disabled, comma-separated."
},
"languageToolLinter.languageTool.ignoredWordsGlobal": {
"type": "array",
"scope": "application",
"default": [],
"description": "Global list of words to ignore in spellcheck."
},
"languageToolLinter.languageTool.ignoredWordsInWorkspace": {
"type": "array",
"scope": "window",
"default": [],
"description": "Workspace list of words to ignore in spellcheck. This setting is ignored at the User (Global) level."
},
"languageToolLinter.languageTool.ignoredWordHint": {
"type": "boolean",
"default": true,
"description": "Shows a hint for ignored words, providing a Quick Fix to remove the word from the ignored list."
}
}
}
Expand Down
102 changes: 101 additions & 1 deletion src/common/configuration-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TextDocument, WorkspaceConfiguration, workspace, ConfigurationChangeEvent, Disposable, window } from 'vscode';
import { TextDocument, WorkspaceConfiguration, workspace, ConfigurationChangeEvent, Disposable, window, ExtensionContext, ConfigurationTarget } from 'vscode';
import { LT_DOCUMENT_LANGUAGE_IDS, LT_CONFIGURATION_ROOT, LT_SERVICE_PARAMETERS, LT_SERVICE_EXTERNAL, LT_CHECK_PATH, LT_SERVICE_MANAGED, LT_SERVICE_PUBLIC, LT_PUBLIC_URL, LT_OUTPUT_CHANNEL } from './constants';
import * as portfinder from "portfinder";
import * as execa from "execa";
Expand All @@ -10,11 +10,18 @@ export class ConfigurationManager implements Disposable {
private serviceUrl: string | undefined;
private managedPort: number | undefined;
private process: execa.ExecaChildProcess | undefined;
private globallyIgnoredWords: Set<string>;
private workspaceIgnoredWords: Set<string>;
private static SETTING_IGNORE_GLOBAL: string = "languageTool.ignoredWordsGlobal";
private static SETTING_IGNORE_WORKSPACE: string = "languageTool.ignoredWordsInWorkspace";
private static SETTING_HINT_IGNORED: string = "languageTool.ignoredWordHint";

constructor() {
this.config = workspace.getConfiguration(LT_CONFIGURATION_ROOT);
this.serviceUrl = this.findServiceUrl(this.getServiceType());
this.startManagedService();
this.globallyIgnoredWords = this.getGloballyIgnoredWords();
this.workspaceIgnoredWords = this.getWorkspaceIgnoredWords();
}

dispose(): void {
Expand All @@ -39,6 +46,18 @@ export class ConfigurationManager implements Disposable {
|| event.affectsConfiguration("languageToolLinter.managed.jarFile"))) {
this.startManagedService();
}
// Error about conflicting settings
if (event.affectsConfiguration("languageToolLinter.languageTool.preferredVariants")
|| event.affectsConfiguration("languageToolLinter.languageTool.language")) {
if (this.config.get("languageToolLinter.languageTool.language") !== "auto"
&& this.config.get("languageToolLinter.languageTool.preferredVariants") !== undefined) {
window.showErrorMessage('', ...["Set Language to 'auto'", "Clear Preferred Variants"]).then((selection) => {
console.log(selection);
});
}
}
this.globallyIgnoredWords = this.getGloballyIgnoredWords();
this.workspaceIgnoredWords = this.getWorkspaceIgnoredWords();
}

isAutoFormatEnabled(): boolean {
Expand Down Expand Up @@ -181,5 +200,86 @@ export class ConfigurationManager implements Disposable {
}
}

// Manage Ignored Words Lists
isIgnoredWord(word: string): boolean {
return this.isGloballyIgnoredWord(word) || this.isWorkspaceIgnoredWord(word);
}

// Is word ignored at the User Level?
isGloballyIgnoredWord(word: string): boolean {
return this.globallyIgnoredWords.has(word.toLowerCase());
}

// Is word ignored at the Workspace Level?
isWorkspaceIgnoredWord(word: string): boolean {
return this.workspaceIgnoredWords.has(word.toLowerCase());
}

// Save words to settings
private saveIgnoredWords(words: Set<string>, section: string, configurationTarget: ConfigurationTarget): void {
let wordArray: Array<string> = Array.from(words).sort();
this.config.update(section, wordArray, configurationTarget);
}

// Get Globally ingored words from settings.
private getGloballyIgnoredWords(): Set<string> {
return new Set<string>(this.config.get<Array<string>>(ConfigurationManager.SETTING_IGNORE_GLOBAL));
}

// Save word to User Level ignored word list.
private saveGloballyIgnoredWords(): void {
this.saveIgnoredWords(this.globallyIgnoredWords, ConfigurationManager.SETTING_IGNORE_GLOBAL, ConfigurationTarget.Global);
}

// Get Workspace ignored words from settings.
private getWorkspaceIgnoredWords(): Set<string> {
return new Set<string>(this.config.get<Array<string>>(ConfigurationManager.SETTING_IGNORE_WORKSPACE));
}

// Save word to Workspace Level ignored word list.
private saveWorkspaceIgnoredWords(): void {
this.saveIgnoredWords(this.workspaceIgnoredWords, ConfigurationManager.SETTING_IGNORE_WORKSPACE, ConfigurationTarget.Workspace);
}

// Add word to User Level ignored word list.
ignoreWordGlobally(word: string): void {
let lowerCaseWord: string = word.toLowerCase();
if (!this.isGloballyIgnoredWord(lowerCaseWord)) {
this.globallyIgnoredWords.add(lowerCaseWord);
this.saveGloballyIgnoredWords();
}
}

// Add word to Workspace Level ignored word list.
ignoreWordInWorkspace(word: string): void {
let lowerCaseWord: string = word.toLowerCase();
if (!this.isWorkspaceIgnoredWord(lowerCaseWord)) {
this.workspaceIgnoredWords.add(lowerCaseWord);
this.saveWorkspaceIgnoredWords();
}
}

// Remove word from User Level ignored word list.
removeGloballyIgnoredWord(word: string): void {
let lowerCaseWord: string = word.toLowerCase();
if (this.isGloballyIgnoredWord(lowerCaseWord)) {
this.globallyIgnoredWords.delete(lowerCaseWord);
this.saveGloballyIgnoredWords();
}
}

// Remove word from Workspace Level ignored word list.
removeWorkspaceIgnoredWord(word: string): void {
let lowerCaseWord: string = word.toLowerCase();
if (this.isWorkspaceIgnoredWord(lowerCaseWord)) {
this.workspaceIgnoredWords.delete(lowerCaseWord);
this.saveWorkspaceIgnoredWords();
}
}

// Show hints for ignored words?
showIgnoredWordHints(): boolean {
return this.config.get(ConfigurationManager.SETTING_HINT_IGNORED) as boolean;
}

}
28 changes: 28 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,34 @@ export function activate(context: vscode.ExtensionContext) {
linter.resetDiagnostics();
}));

// Register "Ignore Word Globally" TextEditorCommand
let ignoreWordGlobally = vscode.commands.registerTextEditorCommand("languagetoolLinter.ignoreWordGlobally", (editor, edit, ...args) => {
configMan.ignoreWordGlobally(args[0]);
linter.requestLint(editor.document, 0);
});
context.subscriptions.push(ignoreWordGlobally);

// Register "Ignore Word in Workspace" TextEditorCommand
let ignoreWordInWorkspace = vscode.commands.registerTextEditorCommand("languagetoolLinter.ignoreWordInWorkspace", (editor, edit, ...args) => {
configMan.ignoreWordInWorkspace(args[0]);
linter.requestLint(editor.document, 0);
});
context.subscriptions.push(ignoreWordInWorkspace);

// Register "Remove Globally Ignored Word" TextEditorCommand
let removeGloballyIgnoredWord = vscode.commands.registerTextEditorCommand("languagetoolLinter.removeGloballyIgnoredWord", (editor, edit, ...args) => {
configMan.removeGloballyIgnoredWord(args[0]);
linter.requestLint(editor.document, 0);
});
context.subscriptions.push(removeGloballyIgnoredWord);

// Register "Remove Workspace Ignored Word" TextEditorCommand
let removeWorkspaceIgnoredWord = vscode.commands.registerTextEditorCommand("languagetoolLinter.removeWorkspaceIgnoredWord", (editor, edit, ...args) => {
configMan.removeWorkspaceIgnoredWord(args[0]);
linter.requestLint(editor.document, 0);
});
context.subscriptions.push(removeWorkspaceIgnoredWord);

// Register "Lint Current Document" TextEditorCommand
let lintCommand = vscode.commands.registerTextEditorCommand("languagetoolLinter.lintCurrentDocument", (editor, edit) => {
linter.requestLint(editor.document, 0);
Expand Down
128 changes: 103 additions & 25 deletions src/linter/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
* limitations under the License.
*/

import { TextDocument, WorkspaceEdit, CodeAction, Location, Diagnostic, Position,
Range, CodeActionKind, DiagnosticSeverity, DiagnosticCollection, languages, Uri, CodeActionProvider, CodeActionContext, CancellationToken } from 'vscode';
import {
TextDocument, WorkspaceEdit, CodeAction, Location, Diagnostic, Position,
Range, CodeActionKind, DiagnosticSeverity, DiagnosticCollection, languages, Uri, CodeActionProvider, CodeActionContext, CancellationToken, workspace
} from 'vscode';
import { ConfigurationManager } from '../common/configuration-manager';
import { LT_TIMEOUT_MS, LT_SERVICE_PARAMETERS, LT_OUTPUT_CHANNEL, LT_DIAGNOSTIC_SOURCE, LT_DISPLAY_NAME } from '../common/constants';
import { LT_TIMEOUT_MS, LT_OUTPUT_CHANNEL, LT_DIAGNOSTIC_SOURCE, LT_DISPLAY_NAME } from '../common/constants';
import * as rp from "request-promise-native";
import * as rehypeBuilder from "annotatedtext-rehype";
import * as remarkBuilder from "annotatedtext-remark";
Expand Down Expand Up @@ -128,6 +130,11 @@ export class Linter implements CodeActionProvider {
return JSON.stringify(rehypeBuilder.build(text, this.rehypeBuilderOptions));
}

// Build annotatedtext from PLAINTEXT
buildAnnotatedPlaintext(text: string): string {
return JSON.stringify({ "text": text });
}

// Perform Lint on Document
lintDocument(document: TextDocument): void {
if (this.configManager.isSupportedDocument(document)) {
Expand All @@ -138,7 +145,8 @@ export class Linter implements CodeActionProvider {
let annotatedHTML: string = this.buildAnnotatedHTML(document.getText());
this.lintAnnotatedText(document, annotatedHTML);
} else {
this.lintPlaintext(document);
let annotatedPlaintext: string = this.buildAnnotatedPlaintext(document.getText());
this.lintAnnotatedText(document, annotatedPlaintext);
}
}
}
Expand Down Expand Up @@ -176,15 +184,6 @@ export class Linter implements CodeActionProvider {
}
}

// Lint Plain Text Document
lintPlaintext(document: TextDocument): void {
if (this.configManager.isSupportedDocument(document)) {
let ltPostDataDict: any = this.getPostDataTemplate();
ltPostDataDict["text"] = document.getText();
this.callLanguageTool(document, ltPostDataDict);
}
}

// Lint Annotated Text
lintAnnotatedText(document: TextDocument, annotatedText: string): void {
if (this.configManager.isSupportedDocument(document)) {
Expand All @@ -199,29 +198,101 @@ export class Linter implements CodeActionProvider {
let matches = response.matches;
let diagnostics: Diagnostic[] = [];
let actions: CodeAction[] = [];
matches.forEach(function (match: ILanguageToolMatch) {
matches.forEach((match: ILanguageToolMatch) => {
let start: Position = document.positionAt(match.offset);
let end: Position = document.positionAt(match.offset + match.length);
let diagnosticRange: Range = new Range(start, end);
let diagnosticMessage: string = match.rule.id + ": " + match.message;
let diagnostic: Diagnostic = new Diagnostic(diagnosticRange, diagnosticMessage, DiagnosticSeverity.Warning);
match.replacements.forEach(function (replacement: ILanguageToolReplacement) {
let actionTitle: string = "'" + replacement.value + "'";
diagnostic.source = LT_DIAGNOSTIC_SOURCE;
// Spelling Rules
if (Linter.isSpellingRule(match.rule.id)) {
let spellingActions: CodeAction[] = this.getSpellingRuleActions(document, diagnostic, match);
if (spellingActions.length > 0) {
diagnostics.push(diagnostic);
spellingActions.forEach( (action) => {
actions.push(action);
});
}
} else {
diagnostics.push(diagnostic);
this.getRuleActions(document, diagnostic, match).forEach((action) => {
actions.push(action);
});
}
});
this.codeActionMap.set(document.uri.toString(), actions);
this.diagnosticMap.set(document.uri.toString(), diagnostics);
this.resetDiagnostics();
}

private getSpellingRuleActions(document: TextDocument, diagnostic: Diagnostic, match: ILanguageToolMatch): CodeAction[] {
let actions: CodeAction[] = [];
let word: string = document.getText(diagnostic.range);
if (this.configManager.isIgnoredWord(word)) {
if (this.configManager.showIgnoredWordHints()) {
// Change severity for ignored words.
diagnostic.severity = DiagnosticSeverity.Hint;
if (this.configManager.isGloballyIgnoredWord(word)) {
let actionTitle: string = "Remove '" + word + "' from always ignored words.";
let action: CodeAction = new CodeAction(actionTitle, CodeActionKind.QuickFix);
action.command = { title: actionTitle, command: "languagetoolLinter.removeGloballyIgnoredWord", arguments: [word] };
action.diagnostics = [];
action.diagnostics.push(diagnostic);
actions.push(action);
}
if (this.configManager.isWorkspaceIgnoredWord(word)) {
let actionTitle: string = "Remove '" + word + "' from Workspace ignored words.";
let action: CodeAction = new CodeAction(actionTitle, CodeActionKind.QuickFix);
action.command = { title: actionTitle, command: "languagetoolLinter.removeWorkspaceIgnoredWord", arguments: [word] };
action.diagnostics = [];
action.diagnostics.push(diagnostic);
actions.push(action);
}
}
} else {
let actionTitle: string = "Always ignore '" + word + "'";
let action: CodeAction = new CodeAction(actionTitle, CodeActionKind.QuickFix);
action.command = { title: actionTitle, command: "languagetoolLinter.ignoreWordGlobally", arguments: [word] };
action.diagnostics = [];
action.diagnostics.push(diagnostic);
actions.push(action);
if (workspace !== undefined) {
let actionTitle: string = "Ignore '" + word + "' in Workspace";
let action: CodeAction = new CodeAction(actionTitle, CodeActionKind.QuickFix);
let location: Location = new Location(document.uri, diagnosticRange);
let edit: WorkspaceEdit = new WorkspaceEdit();
edit.replace(document.uri, location.range, replacement.value);
action.edit = edit;
action.command = { title: actionTitle, command: "languagetoolLinter.ignoreWordInWorkspace", arguments: [word] };
action.diagnostics = [];
action.diagnostics.push(diagnostic);
actions.push(action);
}
this.getReplacementActions(document, diagnostic, match.replacements).forEach((action: CodeAction) => {
actions.push(action);
});
diagnostic.source = LT_DIAGNOSTIC_SOURCE;
diagnostics.push(diagnostic);
}
return actions;
}

private getRuleActions(document: TextDocument, diagnostic: Diagnostic, match: ILanguageToolMatch): CodeAction[] {
let actions: CodeAction[] = [];
this.getReplacementActions(document, diagnostic, match.replacements).forEach((action: CodeAction) => {
actions.push(action);
});
this.codeActionMap.set(document.uri.toString(), actions);
this.diagnosticMap.set(document.uri.toString(), diagnostics);
this.resetDiagnostics();
return actions;
}

private getReplacementActions(document: TextDocument, diagnostic: Diagnostic, replacements: ILanguageToolReplacement[]): CodeAction[] {
let actions: CodeAction[] = [];
replacements.forEach((replacement: ILanguageToolReplacement) => {
let actionTitle: string = "'" + replacement.value + "'";
let action: CodeAction = new CodeAction(actionTitle, CodeActionKind.QuickFix);
let edit: WorkspaceEdit = new WorkspaceEdit();
edit.replace(document.uri, diagnostic.range, replacement.value);
action.edit = edit;
action.diagnostics = [];
action.diagnostics.push(diagnostic);
actions.push(action);
});
return actions;
}

// Reset the Diagnostic Collection
Expand All @@ -232,5 +303,12 @@ export class Linter implements CodeActionProvider {
});
}

// Is the rule a Spelling rule?
// See: https://forum.languagetool.org/t/identify-spelling-rules/4775/3
static isSpellingRule(ruleId: string): boolean {
return ruleId.indexOf("MORFOLOGIK_RULE") !== -1 || ruleId.indexOf("SPELLER_RULE") !== -1
|| ruleId.indexOf("HUNSPELL_NO_SUGGEST_RULE") !== -1 || ruleId.indexOf("HUNSPELL_RULE") !== -1
|| ruleId.indexOf("FR_SPELLING_RULE") !== -1;
}

}
6 changes: 6 additions & 0 deletions src/test-fixtures/workspace/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"languageToolLinter.languageTool.ignoredWordsInWorkspace": [
"misspeeled"
],
"languageToolLinter.languageTool.disabledRules": "DASH_RULE"
}
5 changes: 5 additions & 0 deletions src/test-fixtures/workspace/markdown/spelling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Spell Check

This is a misspeeled word. This is a second misspeeled worrd. This is a third sentence staring with This. and this sentence needs a period

This one doesn’t. Annother misspelled word. And annother.
Loading

0 comments on commit 7917725

Please sign in to comment.