aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorArmand Halbert2026-03-28 06:22:26 -0500
committerGitHub2026-03-28 20:22:26 +0900
commit3d3b75cdc523b27f8eb5b6c10251c4e242f129b9 (patch)
tree36717f4ddb54f3e19671ad78ec5b7ef98c06b948
parent0369442b06755e01aaf3bc88c9f5dba875844c71 (diff)
downloadale-3d3b75cdc523b27f8eb5b6c10251c4e242f129b9.tar.gz

Added support for harper in markdown files (#5104)

-rw-r--r--ale_linters/markdown/harper.vim32
-rw-r--r--autoload/ale/lsp.vim21
-rw-r--r--autoload/ale/lsp_linter.vim4
-rw-r--r--doc/ale-markdown.txt29
-rw-r--r--doc/ale-supported-languages-and-tools.txt1
-rw-r--r--doc/ale.txt1
-rw-r--r--supported-tools.md1
-rw-r--r--test/linter/test_markdown_harper.vader14
-rw-r--r--test/lsp/test_engine_lsp_response_handling.vader28
-rw-r--r--test/lsp/test_update_config.vader36
10 files changed, 166 insertions, 1 deletions
diff --git a/ale_linters/markdown/harper.vim b/ale_linters/markdown/harper.vim
new file mode 100644
index 000000000..98e902b31
--- /dev/null
+++ b/ale_linters/markdown/harper.vim
@@ -0,0 +1,32 @@
+" Author: Armand Halbert <armand.halbert@gmail.com>
+" Description: Harper for Markdown files
+
+call ale#Set('markdown_harper_config', {
+\ 'harper-ls': {
+\ 'diagnosticSeverity': 'hint',
+\ 'dialect': 'American',
+\ 'linters': {
+\ 'SpellCheck': v:true,
+\ 'SentenceCapitalization': v:true,
+\ 'RepeatedWords': v:true,
+\ 'LongSentences': v:true,
+\ 'AnA': v:true,
+\ 'Spaces': v:true,
+\ 'SpelledNumbers': v:false,
+\ 'WrongQuotes': v:false,
+\ },
+\ },
+\})
+
+call ale#linter#Define('markdown', {
+\ 'name': 'harper',
+\ 'lsp': 'stdio',
+\ 'executable': 'harper-ls',
+\ 'command': '%e --stdio',
+\ 'project_root': function('ale_linters#markdown#harper#GetProjectRoot'),
+\ 'lsp_config': {b -> ale#Var(b, 'markdown_harper_config')},
+\})
+
+function! ale_linters#markdown#harper#GetProjectRoot(buffer) abort
+ return fnamemodify(bufname(a:buffer), ':p:h')
+endfunction
diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim
index b0b54c8b2..f49f3ae00 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -329,6 +329,27 @@ function! ale#lsp#UpdateConfig(conn_id, buffer, config) abort
return 1
endfunction
+function! ale#lsp#GetConnectionConfig(conn_id) abort
+ let l:conn = get(s:connections, a:conn_id, {})
+
+ return get(l:conn, 'config', {})
+endfunction
+
+" Send a JSON-RPC response to a server-initiated request (e.g. workspace/configuration).
+" Unlike ale#lsp#Send, which builds outgoing requests/notifications with a 'method' field,
+" this sends a response with 'id' + 'result' fields to reply to a request the server sent us.
+function! ale#lsp#SendResponse(conn_id, id, result) abort
+ let l:conn = get(s:connections, a:conn_id, {})
+
+ if empty(l:conn)
+ return
+ endif
+
+ let l:body = json_encode({'jsonrpc': '2.0', 'id': a:id, 'result': a:result})
+ let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body
+ call s:SendMessageData(l:conn, l:data)
+endfunction
+
function! ale#lsp#CallInitCallbacks(conn_id) abort
let l:conn = get(s:connections, a:conn_id, {})
diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim
index 3b3c403c6..fae9f7098 100644
--- a/autoload/ale/lsp_linter.vim
+++ b/autoload/ale/lsp_linter.vim
@@ -241,6 +241,10 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
\ : a:response.result.items
call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
+ elseif l:method is# 'workspace/configuration'
+ let l:items = get(get(a:response, 'params', {}), 'items', [])
+ let l:config = ale#lsp#GetConnectionConfig(a:conn_id)
+ call ale#lsp#SendResponse(a:conn_id, a:response.id, map(copy(l:items), 'l:config'))
elseif l:method is# 'window/showMessage'
call ale#lsp_window#HandleShowMessage(
\ s:lsp_linter_map[a:conn_id].name,
diff --git a/doc/ale-markdown.txt b/doc/ale-markdown.txt
index a309f1387..7ee577e16 100644
--- a/doc/ale-markdown.txt
+++ b/doc/ale-markdown.txt
@@ -15,6 +15,35 @@ See |ale-dprint-options| and https://dprint.dev/plugins/markdown
===============================================================================
+harper *ale-markdown-harper*
+
+ *ale-options.markdown_harper_config*
+ *g:ale_markdown_harper_config*
+ *b:ale_markdown_harper_config*
+markdown_harper_config
+g:ale_markdown_harper_config
+ Type: |Dictionary|
+ Default: `{'harper-ls': {'diagnosticSeverity': 'hint', 'dialect': 'American', ...}}`
+
+ Dictionary passed to harper-ls as LSP workspace configuration. The default
+ enables spell check, sentence capitalization, repeated words, long
+ sentences, a/an errors, and spacing rules, and disables spelled-out numbers
+ and wrong-quote checks.
+
+ Example: >
+ let g:ale_markdown_harper_config = {
+ \ 'harper-ls': {
+ \ 'diagnosticSeverity': 'warning',
+ \ 'linters': {
+ \ 'SpellCheck': v:true,
+ \ 'LongSentences': v:false,
+ \ },
+ \ },
+ \}
+<
+
+
+===============================================================================
markdownlint *ale-markdown-markdownlint*
*ale-options.markdown_markdownlint_executable*
diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt
index 8cb95b569..fdd8f24dc 100644
--- a/doc/ale-supported-languages-and-tools.txt
+++ b/doc/ale-supported-languages-and-tools.txt
@@ -404,6 +404,7 @@ Notes:
* Markdown
* `alex`
* `cspell`
+ * `harper`
* `languagetool`!!
* `markdownlint`!!
* `marksman`
diff --git a/doc/ale.txt b/doc/ale.txt
index 380914bc5..e0192177a 100644
--- a/doc/ale.txt
+++ b/doc/ale.txt
@@ -3738,6 +3738,7 @@ documented in additional help files.
markdown................................|ale-markdown-options|
cspell................................|ale-markdown-cspell|
dprint................................|ale-markdown-dprint|
+ harper................................|ale-markdown-harper|
markdownlint..........................|ale-markdown-markdownlint|
marksman..............................|ale-markdown-marksman|
mdl...................................|ale-markdown-mdl|
diff --git a/supported-tools.md b/supported-tools.md
index a038acbde..8988fa72b 100644
--- a/supported-tools.md
+++ b/supported-tools.md
@@ -414,6 +414,7 @@ formatting.
* Markdown
* [alex](https://github.com/get-alex/alex)
* [cspell](https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell)
+ * [harper](https://github.com/elijah-potter/harper) :speech_balloon:
* [languagetool](https://languagetool.org/) :floppy_disk: :speech_balloon:
* [markdownlint](https://github.com/DavidAnson/markdownlint) :floppy_disk:
* [marksman](https://github.com/artempyanykh/marksman) :speech_balloon:
diff --git a/test/linter/test_markdown_harper.vader b/test/linter/test_markdown_harper.vader
new file mode 100644
index 000000000..92c1c1b36
--- /dev/null
+++ b/test/linter/test_markdown_harper.vader
@@ -0,0 +1,14 @@
+Before:
+ call ale#assert#SetUpLinterTest('markdown', 'harper')
+
+After:
+ call ale#assert#TearDownLinterTest()
+
+Execute(The default command should be correct):
+ AssertLinter 'harper-ls', ale#Escape('harper-ls') . ' --stdio'
+
+Execute(Should accept configuration settings):
+ AssertLSPConfig g:ale_markdown_harper_config
+
+ let b:ale_markdown_harper_config = {'harper-ls': {'diagnosticSeverity': 'warning'}}
+ AssertLSPConfig {'harper-ls': {'diagnosticSeverity': 'warning'}}
diff --git a/test/lsp/test_engine_lsp_response_handling.vader b/test/lsp/test_engine_lsp_response_handling.vader
index 7b18befcc..153be2b0e 100644
--- a/test/lsp/test_engine_lsp_response_handling.vader
+++ b/test/lsp/test_engine_lsp_response_handling.vader
@@ -534,6 +534,34 @@ Execute(LSP pull model diagnostic responses that are 'unchanged' should be handl
\ g:ale_buffer_info[bufnr('')].loclist
AssertEqual [], g:ale_buffer_info[bufnr('')].active_linter_list
+Execute(workspace/configuration requests should be answered with the connection config):
+ let g:sent_responses = []
+
+ function! ale#lsp#GetConnectionConfig(conn_id) abort
+ return {'foo': 'bar'}
+ endfunction
+
+ function! ale#lsp#SendResponse(conn_id, id, result) abort
+ call add(g:sent_responses, [a:conn_id, a:id, a:result])
+ endfunction
+
+ call ale#lsp_linter#SetLSPLinterMap({'1': {'name': 'pylsp', 'aliases': [], 'lsp': 'stdio'}})
+ call ale#lsp_linter#HandleLSPResponse(1, {
+ \ 'jsonrpc': '2.0',
+ \ 'id': 7,
+ \ 'method': 'workspace/configuration',
+ \ 'params': {
+ \ 'items': [{'section': 'foo'}, {'section': 'bar'}],
+ \ },
+ \})
+
+ AssertEqual
+ \ [[1, 7, [{'foo': 'bar'}, {'foo': 'bar'}]]],
+ \ g:sent_responses
+
+ unlet! g:sent_responses
+ runtime autoload/ale/lsp.vim
+
Execute(LSP errors should be logged in the history):
call ale#lsp_linter#SetLSPLinterMap({'347': {'name': 'foobar', 'aliases': [], 'lsp': 'stdio'}})
call ale#lsp_linter#HandleLSPResponse(347, {
diff --git a/test/lsp/test_update_config.vader b/test/lsp/test_update_config.vader
index 2a2c85e6e..75a9a37a9 100644
--- a/test/lsp/test_update_config.vader
+++ b/test/lsp/test_update_config.vader
@@ -1,16 +1,26 @@
Before:
runtime autoload/ale/lsp.vim
+ runtime autoload/ale/job.vim
let g:conn_id = ale#lsp#Register('executable', '/foo/bar', '', {})
+ let g:sent_data = []
- " Stub out this function, so we test updating configs.
+ " Stub out these functions to capture calls without side effects.
function! ale#lsp#Send(conn_id, message) abort
endfunction
+ function! ale#job#SendRaw(job_id, data) abort
+ call add(g:sent_data, a:data)
+ endfunction
+
After:
Restore
unlet! g:conn_id
+ unlet! g:conn
+ unlet! g:sent_data
+ unlet! g:remainder
+ unlet! g:messages
runtime autoload/ale/lsp.vim
@@ -19,3 +29,27 @@ Execute(Only send updates when the configuration dictionary changes):
AssertEqual 1, ale#lsp#UpdateConfig(g:conn_id, bufnr(''), {'a': 1})
AssertEqual 0, ale#lsp#UpdateConfig(g:conn_id, bufnr(''), {'a': 1})
AssertEqual 1, ale#lsp#UpdateConfig(g:conn_id, bufnr(''), {})
+
+Execute(ale#lsp#GetConnectionConfig() should return empty dict for unknown connections):
+ AssertEqual {}, ale#lsp#GetConnectionConfig('unknown:conn')
+
+Execute(ale#lsp#GetConnectionConfig() should return the current connection config):
+ call ale#lsp#UpdateConfig(g:conn_id, bufnr(''), {'foo': 'bar'})
+ AssertEqual {'foo': 'bar'}, ale#lsp#GetConnectionConfig(g:conn_id)
+
+Execute(ale#lsp#SendResponse() should do nothing for unknown connections):
+ " Should not throw
+ call ale#lsp#SendResponse('unknown:conn', 1, [])
+ AssertEqual [], g:sent_data
+
+Execute(ale#lsp#SendResponse() should send a JSON-RPC response message):
+ " Give the connection a job_id so s:SendMessageData routes to ale#job#SendRaw
+ let g:conn = ale#lsp#GetConnections()[g:conn_id]
+ let g:conn.job_id = 1
+
+ call ale#lsp#SendResponse(g:conn_id, 42, ['result_value'])
+
+ AssertEqual 1, len(g:sent_data)
+ let [g:remainder, g:messages] = ale#lsp#ReadMessageData(g:sent_data[0])
+ AssertEqual '', g:remainder
+ AssertEqual [{'jsonrpc': '2.0', 'id': 42, 'result': ['result_value']}], g:messages