Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
temp
*.vsix
etc/server_response.txt
23 changes: 17 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 23 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"activationEvents": [
"onCommand:dandy.run"
],
"categories": ["Linters", "Other"],
"categories": [
"Linters",
"Other"
],
"contributes": {
"commands": [
{
Expand All @@ -15,7 +18,20 @@
"command": "dandy.fixAll",
"title": "모두 고치기"
}
]
],
"configuration": {
"title": "Dandy",
"properties": {
"dandy.exceptWords": {
"description": "맞춤법 검사 결과에서 제외할 어휘",
"scope": "resource",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"dependencies": {
"request": "^2.88.0"
Expand All @@ -28,7 +44,11 @@
"vscode": "^1.33.1"
},
"icon": "img/icon.png",
"keywords": ["hangul", "korean", "spelling"],
"keywords": [
"hangul",
"korean",
"spelling"
],
"main": "src/extension.js",
"name": "vscode-dandy",
"publisher": "fallroot",
Expand Down
26 changes: 7 additions & 19 deletions src/code-action-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,21 @@ function getProvider (document, range, context, token) {

context.diagnostics.forEach(diagnostic => {
diagnostic.answers.forEach(message => {
codeActions.push(generateForFix({ document, message, range: diagnostic.range }))
codeActions.push(makeQuickFixCommand('"'+message+'"', [{document, message, range: diagnostic.range}], "dandy.fix"))
})
codeActions.push(generateForSkip({ document, diagnostic }))
codeActions.push(makeQuickFixCommand("건너뛰기",[diagnostic],"dandy.skip" ))
codeActions.push(makeQuickFixCommand("예외추가",[document.getText(range)],"dandy.addToException"))
})

return codeActions
}

function generateForFix ({ document, message, range }) {
const codeAction = new vscode.CodeAction(message, vscode.CodeActionKind.QuickFix)

function makeQuickFixCommand(menuTitle, commandArgs, commandName) {
const codeAction = new vscode.CodeAction(menuTitle, vscode.CodeActionKind.QuickFix)
codeAction.command = {
arguments: [{ document, message, range }],
command: 'dandy.fix'
arguments: commandArgs,
command: commandName
}

return codeAction
}

function generateForSkip ({ document, diagnostic }) {
const codeAction = new vscode.CodeAction('건너뛰기', vscode.CodeActionKind.QuickFix)

codeAction.command = {
arguments: [diagnostic],
command: 'dandy.skip'
}

return codeAction
}

Expand Down
195 changes: 138 additions & 57 deletions src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,100 +2,148 @@ const vscode = require('vscode')
const codeActionProvider = require('./code-action-provider')
const spellChecker = require('./spell-checker')

// collection is a per-extension Map<document.uri, diagnostics[]>
const collection = vscode.languages.createDiagnosticCollection('dandy')
const resultMap = new WeakMap()
// const resultMap = new WeakMap()

function activate (context) {
const subs = context.subscriptions

subs.push(vscode.commands.registerTextEditorCommand('dandy.run', run))
subs.push(vscode.commands.registerCommand('dandy.fix', fix))
subs.push(vscode.commands.registerCommand('dandy.fixAll', fixAll))
subs.push(vscode.commands.registerCommand('dandy.skip', skip))
subs.push(vscode.commands.registerCommand('dandy.addToException', addToException))
subs.push(vscode.languages.registerCodeActionsProvider(['markdown', 'plaintext'], codeActionProvider))
subs.push(vscode.workspace.onDidChangeTextDocument(onDidChangeTextDocument))
subs.push(vscode.workspace.onDidCloseTextDocument(onDidCloseTextDocument))
subs.push(vscode.workspace.onDidSaveTextDocument(onDidSaveTextDocument))
subs.push(collection)
}

function run () {
function run() {

const editor = getEditor()

if (!editor) return

const document = editor.document
const selection = editor.selection
const empty = selection.isEmpty
const text = document.getText(empty ? undefined : selection)

vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: '맞춤법 검사를 진행하고 있습니다.'
}, () => {
return spellChecker.execute(text).then(result => {
resultMap.set(document, result.errors)
setCollections(document)
const document = editor.document;
const selection = editor.selection;
const empty = selection.isEmpty;
// 맞춤법 서버로 전송한 텍스트가 Windows 포맷, 즉 CR LF로 줄바꿈되어 있더라도
// 결과의 오프셋 값은 CR 만의 줄바꿈 기준으로 되어 있다. 이 때문에 오차가 생기는
// 것을 방지하기 위해 LF를 공백으로 대체하여 보냄
const text = document.getText(empty ? undefined : selection).replace(/\n/g, ' ');
const startOffset = empty ? 0 : document.offsetAt(selection.start);

vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: '맞춤법 검사를 진행하고 있습니다.'
},
async (progress) => {
return new Promise(async (resolve, reject) => {
try {
const texts = splitter(text, 8000);
const errors = [];
var splitStart = startOffset;
for (const t of texts) {

const result = await spellChecker.execute(t);

for (error of result.errors) {
error.start += splitStart;
error.end += splitStart;
errors.push(error);
}
splitStart += t.length;
}
setCollections(document, errors);
resolve();
} catch (error) {
vscode.window.showInformationMessage(error);
reject(error);
}
})
})
}
);

}

// limit 길이 한도 내에서 문장 단위로 split
function splitter(str, limit) {
const sentences = str.match( /[^\.!\?]+[\.!\?]+/g );
const splits = [];
var partial = "";
sentences.forEach(s=> {
if (partial.length+s.length>limit) {
splits.push(partial);
partial=s;
} else {
partial+=s;
}
});
if (partial.length>1)
splits.push(partial);
return splits;
}

function fix ({ document, message, range }) {
let edit = new vscode.WorkspaceEdit()
edit.replace(document.uri, range, message)
vscode.workspace.applyEdit(edit)
}

function fixAll () {
const document = getDocument()
const uri = document.uri
const diagnostics = collection.get(uri)
const edit = new vscode.WorkspaceEdit()

diagnostics.forEach(diagnostic => edit.replace(uri, diagnostic.range, diagnostic.answers[0]))
vscode.workspace.applyEdit(edit).then(() => collection.clear()).catch(console.error)
// 해당 diagnostic은 onDidChangeTextDocument 에서 삭제됨
}

function skip (diagnostic) {
const document = getDocument()
const uri = document.uri
let diagnostics = collection.get(uri).slice()
const uri = getDocument().uri;
const diagnostics = collection.get(uri).slice()
const index = diagnostics.indexOf(diagnostic)

if (index < 0) return

const errors = resultMap.get(document)

resultMap.set(document, errors.splice(errors.indexOf(diagnostic.error), 1))
diagnostics.splice(index, 1)
collection.set(uri, diagnostics)
}

function setCollections (document, errors) {
const text = document.getText()
const diagnostics = []

if (errors === undefined) {
errors = resultMap.get(document)
async function addToException(word) {
// 예외처리할 word를 workspace별 dictionary에 저장
const config = vscode.workspace.getConfiguration("dandy")
let words = config.get('exceptWords');
if (!words) words = []
if (!words.includes(word)) {
words.push(word);
words.sort();
}
// Diagnostic에서 같은 단어 삭제
const doc = getDocument();
const diags = collection.get(doc.uri);
const newDiags = diags.filter(diag => doc.getText(diag.range)!=word);
collection.set(doc.uri, newDiags);
// workspace .vscode/settings.json에 저장
await config.update('exceptWords', words, vscode.ConfigurationTarget.workspace);
}

errors.forEach(error => {
const keyword = error.before
let index = text.indexOf(keyword)

while (index >= 0) {
const start = document.positionAt(index)
const end = document.positionAt(index + keyword.length)
const range = new vscode.Range(start, end)
function setCollections(document, errors) {
const text = document.getText()
const diagnostics = []
const config = vscode.workspace.getConfiguration("dandy")
let exceptWords = config.get('exceptWords');
if (!exceptWords) exceptWords = []

for (error of errors) {
if (!exceptWords.includes(error.before)) { // 예외처리 단어 제외
const keyword = error.before
const range = new vscode.Range(
document.positionAt(error.start),
document.positionAt(error.end));
const diagnostic = new vscode.Diagnostic(range, error.help, vscode.DiagnosticSeverity.Error)

diagnostic.answers = error.after
diagnostic.document = document
diagnostic.error = error
// 문서가 편집된 후에 offset 값을 알아내기 어려우므로 추가 field에 저장해둔다.
diagnostic.startOffset = error.start;
diagnostic.endOffset = error.end;
diagnostics.push(diagnostic)

index = text.indexOf(keyword, index + 1)
}
})

}
collection.set(document.uri, diagnostics)
}

Expand All @@ -112,11 +160,44 @@ function getEditor () {
}

function onDidChangeTextDocument (event) {
const document = event.document
const errors = resultMap.get(document)
const changes = event.contentChanges;
if (!changes || changes.length==0)
return;
for(const changed of changes) {
const offsetInc = changed.text.length - changed.rangeLength;

const diags = collection.get(event.document.uri);
const newDiags = []
const document = event.document;
for(d of diags) {
if (d.range.end.isBeforeOrEqual(changed.range.start))
newDiags.push(d);
else if (d.range.start.isAfterOrEqual(changed.range.end)) {
// d.range는 편집 전의 document를 기준으로 좌표를 가지고 있기 때문에
// 지금 시점에서 document.offsetAt으로 offset을 계산할 수 없음. 때문에 별도로 저장해놓은 offset값을 이용
const start=document.positionAt(offsetInc+d.startOffset);
const end=document.positionAt(offsetInc+d.endOffset);
d.range = new vscode.Range(start, end);
d.startOffset += offsetInc;
d.endOffset += offsetInc;
newDiags.push(d);
} else {
// diag에 접하는 영역을 편집시에는 diag를 삭제해버리자.
}
}
collection.set(event.document.uri, newDiags);
}
}

function onDidCloseTextDocument(document) {
if (document && document.uri) {
collection.delete(document.uri);
}
}

if (errors && errors.length > 0) {
setCollections(document, errors)
function onDidSaveTextDocument(document) {
if (document && document.uri) {
collection.set(document.uri, []);
}
}

Expand Down
Loading