Skip to content

Commit

Permalink
support for LSP executeCommand
Browse files Browse the repository at this point in the history
  • Loading branch information
dfgordon committed Oct 6, 2024
1 parent e810458 commit 1002bea
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 18 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2024-10-06

### New Features

* Add LSP command processor
- Renumber Applesoft or Integer
- Tokenize Applesoft or Integer
- Minify Applesoft
39 changes: 25 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,42 @@

## Overview

This plugin provides language support for the following languages that were historically used with the Apple II line of computers:
This plugin provides language support for the following languages:

* Integer BASIC
* Applesoft BASIC
* Merlin Assembly

These were commonly used with the Apple II line of computers.

<img src="nvim-a2-pack-demo.gif" alt="session capture"/>

## Features

* semantic highlights
* language diagnostics
* go to definition with `Ctrl-]`
* display hovers with `K`
* auto completions, see below
* minify Applesoft with `:A2 minify [level]`
* tokenize either BASIC with `:A2 tokenize [address]`
- for Integer, address is *end* of tokens, and only affects display
* renumber either BASIC with `:A2 renumber <start> <step>`
- renumbers selection, or whole document if none
- will change row order if necessary

## Installation

1. Install `a2kit` version 3.3 or higher
- Install the rust toolchain if necessary
1. Install Neovim version 0.10.1 or higher
2. Install `a2kit` version 3.3.2 or higher
- Install/update the rust toolchain as necessary
- Run `cargo install a2kit` in the terminal
- Check that `~/.cargo/bin` was added to your path
- Make sure `~/.cargo/bin` is in the path (usually automatic)
2. Install the plugin. The procedure varies depending on plugin manager. See examples.
3. Test it by moving the cursor over some keyword in an Apple II source file, and pressing `K` (case matters) in normal mode. You should get a hover. If the color scheme is not rendered properly, try installing a better terminal program, or a Neovim GUI.

The plugin does not verify client or server versions. You have to check yourself with `a2kit -V` and `nvim -v` (case matters).

### rocks.nvim example

For [rocks.nvim](https://github.com/nvim-neorocks/rocks.nvim), enter Neovim and issue commands:
Expand Down Expand Up @@ -69,14 +88,6 @@ Merlin analysis requires a workspace scan. The way the plugin finds the workspa
* Merlin assembly is triggered by `*.s` or `*.asm`
- Only `*.s` files are detected by the workspace scanner

## Features

* semantic highlights
* language diagnostics
* go to definition, type `Ctrl-]` with cursor on any kind of reference
* hovers, type `K` with the cursor on a wide variety of language elements
* completions, but see below

## Settings

Changing settings means changing a Lua map (this is the way of Neovim). Some of the available map keys can be found [here](https://github.com/dfgordon/a2kit/wiki/Languages#configuration-options). Translate the key paths to Lua maps in the obvious way.
Expand Down Expand Up @@ -113,7 +124,7 @@ Modify the spec file to include the options. Example:
```lua
--- LAZY.NVIM SPEC FILE
return {
--- ...omitting other plugins...
-- ...omitting other plugins...
{
"dfgordon/nvim-a2-pack",
opts = {
Expand All @@ -134,7 +145,7 @@ The language servers provide completions and snippets. To gain these capabiliti
```lua
--- LAZY.NVIM SPEC FILE
return {
--- ...omitting other plugins...
-- ...omitting other plugins...
{
"hrsh7th/nvim-cmp",
-- load cmp on InsertEnter
Expand Down
218 changes: 218 additions & 0 deletions lua/commands.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
--- :A2 minify {level?} - reduce size of Applesoft code and open in new window
---
--- :A2 tokenize {load_addr?} - Display hex dump of tokenized code in new window.
--- For Integer BASIC load_addr is the *end* of the tokens, and has no effect
--- on the tokenized datastream itself.
---
--- :A2 renumber {start} {step} - Renumber a BASIC program.
--- This will move lines if necessary. Operates on selection, or whole program
--- if there is none. References are updated globally.

local display = require "display"

local commands = {}

commands.load_addr = 0

---@class A2Cmd
---@field impl fun(args:string[], opts: vim.api.keyset.user_command) The command implementation
---@field complete? fun(subcmd_arg_lead: string): string[] Command completions callback, taking the lead of the subcommand's arguments

---@type { [string]: A2Cmd }
local a2_command_tbl = {
minify = {
impl = function(args, opts)
if #args > 1 then
vim.print("expected 0 or 1 args, got " .. #args)
return
end
local ft = vim.api.nvim_get_option_value('filetype', { buf = 0 })
if ft ~= "applesoft" then
vim.print("can't minify " .. ft)
return
end
local level = tonumber(1)
if #args > 0 then
level = tonumber(args[1])
end
vim.lsp.buf.execute_command {
command = 'applesoft.minify',
arguments = {
vim.fn.join(vim.fn.getbufline(vim.fn.bufname("%"), 0, "$"), "\n"),
level
}
}
end,
complete = function (subcmd_arg_lead)
return {"1","2","3"}
end
},
tokenize = {
impl = function(args, opts)
if #args > 1 then
vim.print("expected 0 or 1 args, got " .. #args)
return
end
local ft = vim.api.nvim_get_option_value('filetype', { buf = 0 })
local arguments = {}
if ft == "applesoft" then
commands.load_addr = tonumber(2049)
arguments = {
vim.fn.join(vim.fn.getbufline(vim.fn.bufname("%"), 0, "$"), "\n"),
commands.load_addr
}
elseif ft == "integerbasic" then
commands.load_addr = tonumber(38400)
arguments = {
vim.fn.join(vim.fn.getbufline(vim.fn.bufname("%"), 0, "$"), "\n")
}
else
vim.print("can't tokenize " .. ft)
return
end
if #args > 0 then
commands.load_addr = tonumber(args[1])
end
vim.lsp.buf.execute_command {
command = ft..'.tokenize',
arguments = arguments
}
end,
complete = function (subcmd_arg_lead)
local ft = vim.api.nvim_get_option_value('filetype', { buf = 0 })
if ft == "applesoft" then
return {"2049","16385"}
else
return {"38400"}
end
end
},
renumber = {
impl = function(args, opts)
local ft = vim.api.nvim_get_option_value('filetype', { buf = 0 })
if ft ~= "applesoft" and ft ~= "integerbasic" then
vim.print("can't renumber " .. ft)
return
end
if #args ~= 2 then
vim.print("expected 2 args, got " .. #args)
return
end
local textDocumentItem = {
uri = vim.lsp.util.make_text_document_params().uri,
languageId = ft,
version = 0, -- where to get the right version?
text = vim.fn.join(vim.fn.getbufline(vim.fn.bufname("%"), 0, "$"), "\n")
}
local rng = vim.lsp.util.make_given_range_params().range
if rng.start.line == rng['end'].line then
rng = nil
end
vim.lsp.buf.execute_command {
command = ft .. '.move',
arguments = {
textDocumentItem,
rng,
args[1],
args[2],
true
}
}
end,
complete = function (subcmd_arg_lead)
return {"1","10"}
end
}
}

local function next_untitled_doc(ext)
local base = 'untitled'
local suf = 0
while vim.fn.bufexists(base .. '.' .. ext) ~= 0 do
suf = suf + 1
base = base .. suf
if suf == 1000 then
return nil
end
end
return base .. '.' .. ext
end

local function dispatcher(opts)
local fargs = opts.fargs
local cmd = fargs[1]
local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {}
local command = a2_command_tbl[cmd]
if not command then
vim.notify("A2: Unknown command: " .. cmd, vim.log.levels.ERROR)
return
end
command.impl(args, opts)
end

function commands.create_commands()
vim.api.nvim_create_user_command("A2", dispatcher, {
nargs = "+",
desc = "Apple II language services",
complete = function(arg_lead, cmdline, _)
-- first see if we are completing an argument
local subcmd, subcmd_arg_lead = cmdline:match("^A2[!]*%s(%S+)%s(.*)$")
if subcmd and subcmd_arg_lead and a2_command_tbl[subcmd] and a2_command_tbl[subcmd].complete then
return a2_command_tbl[subcmd].complete(subcmd_arg_lead)
end
-- if not complete the subcommand
if cmdline:match("^A2[!]*%s+%w*$") then
return vim.iter(vim.tbl_keys(a2_command_tbl)):totable()
end
end,
bang = true,
})
end

---Process server's response to executeCommand request
function commands.finish_command(err, result, ctx, config)
if err ~= nil then
vim.print(ctx.params.command .. ' failed: ' .. err.message)
return
end
if string.find(ctx.params.command, ".move") ~= nil then
if string.find(ctx.params.command, "integerbasic") ~= nil then
vim.print("if code branches on expressions they may need manual adjustment")
end
-- no need for other action, lsp client handles the returned edits
return
end
if result ~= nil then
if ctx.params.command == "applesoft.minify" then
local doc_name = next_untitled_doc('bas')
if doc_name == nil then
vim.print('too many untitled docs')
return
end
local bufnr = vim.fn.bufnr(doc_name,true)
vim.fn.bufload(bufnr)
vim.fn.setbufline(bufnr, 1, vim.fn.split(result, "\n"))
vim.api.nvim_open_win(bufnr, false, { split = 'right', win = 0 })
end
if string.find(ctx.params.command, ".tokenize") ~= nil then
local neg = false
if ctx.params.command == "integerbasic.tokenize" then
commands.load_addr = commands.load_addr - #result
neg = true
end
local doc_name = next_untitled_doc('txt')
if doc_name == nil then
vim.print('too many untitled docs')
return
end
local bufnr = vim.fn.bufnr(doc_name,true)
vim.fn.bufload(bufnr)
vim.fn.setbufline(bufnr, 1, vim.fn.split(display.hexdump(result, commands.load_addr, neg), "\n"))
vim.api.nvim_open_win(bufnr, false, { split = 'right', win = 0 })
end
else
vim.print("nil result from executeCommand")
end
end

return commands
36 changes: 36 additions & 0 deletions lua/display.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
local display = {}

function display.hexdump(ary,load_addr,neg)
local dump = ""
local hex = ""
local asc = ""

for i = 1,#ary do
if 1 == i % 8 then
dump = dump .. hex .. asc .. "\n"
hex = string.format("%04x: ", load_addr + i - 1)
asc = ""
end

hex = hex .. string.format("%02x ", ary[i])
if neg then
if ary[i] >= 128 + 32 and ary[i] <= 128 + 126 then
asc = asc .. string.char(ary[i] - 128)
else
asc = asc .. "."
end
else
if ary[i] >= 32 and ary[i] <= 126 then
asc = asc .. string.char(ary[i])
else
asc = asc .. "."
end
end
end


return dump .. hex
.. string.rep(" ", 8 - #ary % 8) .. asc
end

return display
Loading

0 comments on commit 1002bea

Please sign in to comment.