检测edm语法的vscode插件开发

前言

我们要写一个edm的语法插件,就需要用到vscode编程语言扩展。
先看一下vscode的编程语言扩展有哪些:

image.png

vscode是支持错误检查的,我们要写一个edm语法插件就需要用到代码扫描的诊断信息,这个诊断信息是以vscode.Diagnostic为载体呈现的。

诊断信息

下图是vscode.Diagnostic类的成员和与相关类的关系:

image.png

以小到大,这些类为:

  • Position: 定位到一行上的一个字符的坐标
  • Range: 由起点和终点两个Position决定
  • Location: 一个Range配上一个URI
  • DiagnosticRelatedInformation: 一个Location配一个message
  • Diagnostic: 主体是一个message字符串,一个Range和一个DiagnosticRelatedInformation.

URL是Uniform Resource Locator的缩写,译为"统一资源定位符"。URL是一种URI,它标识一个互联网资源,并指定对其进行操作或获取该资源的方法。
最大的缺点是当信息资源的存放地点发生变化时,必须对URL作相应的改变。因此人们正在研究新的信息资源表示方法,例如:URI(Universal Resource Identifier)即"通用资源标识" 、URN(Uniform Resource Name)即"统一资源名"和URC(Uniform Resource Citation)即"统一资源引用符"等。
URI还在进一步的研究当中。研究的方向就是弥补URL的缺点。

构造一个诊断信息

以下图的html代码为例,保存为test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DOCUMENT</title>
    <style></style> <!--edm不支持style标签-->
</head>
<body>
    <tr> 
        <td >

        </td>
    </tr> 
</body>
</html>

在这个例子中使用了style标签,在edm中不支持。
出现问题的是第8行的第5字符到第19字符,所以我们构造(7,4)到(7,18)这样两个Position为首尾的Range。
有了Range,加上问题描述字符串,和问题严重程序三项,就可以构造出一个Diagnostic。

let diagnostic1:vscode.Diagnostic = new vscode.Diagnostic(
    new vscode.Range(
        new vscode.Position(7,4),
        new vscode.Position(7,18)
    ),
    'edm不支持style标签',
    vscode.DiagnosticSeverity.Warning
)

updateDiags完整的代码

export function updateDiags(
    document: vscode.TextDocument,
    collection: vscode.DiagnosticCollection
): void {
    let diagnostics: vscode.Diagnostic = new vscode.Diagnostic(
        new vscode.Range(new vscode.Position(7, 4), new vscode.Position(7, 18)),
        'edm不支持style标签',
        vscode.DiagnosticSeverity.Warning
    );
    diagnostics.source = 'edm Helper';
    diagnostics.relatedInformation = [
        new vscode.DiagnosticRelatedInformation(
            new vscode.Location(
                document.uri,
                new vscode.Range(new vscode.Position(7, 0), new vscode.Position(7, 18))
            ),
            'edm grammar check'
        ),
    ];
    diagnostics.code = 102;

    if (document && path.basename(document.uri.fsPath) === 'test.html') {
        collection.set(document.uri, [diagnostics]);
    } else {
        collection.clear();
    }
}

然后在active函数里调用刚刚写的方法

export function activate(context: ExtensionContext) {
    const diag_coll = vscode.languages.createDiagnosticCollection('basic-lint-1');

    if (vscode.window.activeTextEditor) {
        updateDiags(vscode.window.activeTextEditor.document, diag_coll);
    }

    context.subscriptions.push(
        vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => {
            if (e !== undefined) {
                updateDiags(e.document, diag_coll);
            }
        })
    );
    context.subscriptions.push(
        workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent | undefined) => {
            if (e !== undefined) {
                updateDiags(e.document, diag_coll);
            }
        })
    );
}

按F5运行一下,就可以看到检测结果啦


image.png

诊断结果vscode.DiagnosticSeverity的值以下四个,分别是下面的效果:

image.png

image.png

image.png
image.png

语言服务器协议LSP

除了Diagnostic,我们还需要用到语言服务器协议LSP(language sever protocol)
首先 language server是一种跨编辑器的语言支持实现规范。它由微软提出,目前 vscode 、vim、atom 都已经支持了这个规范。
LSP(language sever protocol)是用来处理语言解析等等东西在各式ide里应用的玩意。ide主要干的活还是要提供各类语言的解析跳转高亮等等的东西,所以lsp就显得很重要。放两张图就能大概理解LSP是具体干什么的,为什么需要LSP。

image.png

LSP主要解决了几个问题:
1、语言插件的复用。举个例子:Eclipse里C++相关的支持是用java写的,原因很简单:eclipse本身是java写的。但是这样如果要在vscode里面写C++那就又得拿js写一遍,相当于重复造了轮子。
2、进程独立。语言解析这件事本身是很重的,有时候会需要花非常长的时间来完成,要是这时候整个vscode都卡住那就别玩了。所以干脆把这块东西单独抽出来放在服务器上。
LSP现在支持的功能大概如下:
image.png

所以实际上在涉及各种语言解析的时候,插件需要起一个server来处理,文件夹大体上就会长这样

├── client // 语言客户端
│   ├── package.json
│   ├── src
│   │   ├── test // 单元测试文件
│   │   └── extension.js // 语言客户端的入口文件
├── package.json // 插件的描述文件
└── server // 语言服务端
    └── package.json
    └── src
        └── server.js // 语言服务端的入口文件

LSP生命周期

服务器的生命周期从客服端向服务端发送一个initialize请求开始,参数是一个InitializeParameter对象

interface InitializeParams {
    /**
     * The process Id of the parent process that started
     * the server. Is null if the process has not been started by another process.
     * If the parent process is not alive then the server should exit (see exit notification) its process.
     */
    processId: number | null;

    /**
     * The rootPath of the workspace. Is null
     * if no folder is open.
     *
     * @deprecated in favour of rootUri.
     */
    rootPath?: string | null;

    /**
     * The rootUri of the workspace. Is null if no
     * folder is open. If both `rootPath` and `rootUri` are set
     * `rootUri` wins.
     */
    rootUri: DocumentUri | null;

    /**
     * User provided initialization options.
     */
    initializationOptions?: any;

    /**
     * The capabilities provided by the client (editor or tool)
     */
    capabilities: ClientCapabilities;

    /**
     * The initial trace setting. If omitted trace is disabled ('off').
     */
    trace?: 'off' | 'messages' | 'verbose';

    /**
     * The workspace folders configured in the client when the server starts.
     * This property is only available if the client supports workspace folders.
     * It can be `null` if the client supports workspace folders but none are
     * configured.
     *
     * Since 3.6.0
     */
    workspaceFolders?: WorkspaceFolder[] | null;
}

而服务器返回的是服务器的能力

/**
 * The result returned from an initialize request.
 */
export interface InitializeResult<T = any> {
    /**
     * The capabilities the language server provides.
     */
    capabilities: ServerCapabilities<T>;
    /**
     * Information about the server.
     *
     * @since 3.15.0
     */
    serverInfo?: {
        /**
         * The name of the server as defined by the server.
         */
        name: string;
        /**
         * The server's version as defined by the server.
         */
        version?: string;
    };
    /**
     * Custom initialization results.
     */
    [custom: string]: any;
}

我们这里要用到的就是textDocumentSync

export interface _ServerCapabilities<T = any> {
    /**
     * Defines how text documents are synced. Is either a detailed structure defining each notification or
     * for backwards compatibility the TextDocumentSyncKind number.
     */
    textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind;
    ...
}

textDocumentSync的取值可以直接写具体的能力,也可以设置类型

export interface TextDocumentSyncOptions {
    /**
     * Open and close notifications are sent to the server. If omitted open close notification should not
     * be sent.
     */
    openClose?: boolean;
    /**
     * Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full
     * and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None.
     */
    change?: TextDocumentSyncKind;
    /**
     * If present will save notifications are sent to the server. If omitted the notification should not be
     * sent.
     */
    willSave?: boolean;
    /**
     * If present will save wait until requests are sent to the server. If omitted the request should not be
     * sent.
     */
    willSaveWaitUntil?: boolean;

    ......
}
/**
 * Defines how the host (editor) should sync
 * document changes to the language server.
 */
export declare namespace TextDocumentSyncKind {
    /**
     * Documents should not be synced at all.
     */
    const None = 0;
    /**
     * Documents are synced by always sending the full content
     * of the document.
     */
    const Full = 1;
    /**
     * Documents are synced by sending the full content on open.
     * After that only incremental updates to the document are
     * send.
     */
    const Incremental = 2;
}

客户端收到initialize result之后,按照三次握手的原则,将返回一个initialized消息做确认。至此,一个服务端与客户端通信的生命周期就算是成功建立。

实现一个LSP服务

createConnection

服务端首先要获取一个Connection对象,通过vscode-languageserver提供的createConnection函数来创建Connection.

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);  

Connection中对于LSP的消息进行了封装

onInitialize

监听客户端发送的initialize,返回服务端的能力,这里我们设置类型为增量监听,每次只传变化的部分。

connection.onInitialize((params: InitializeParams) => {
    let capabilities = params.capabilities;

    return {
        capabilities: {
            textDocumentSync: TextDocumentSyncKind.Incremental
        }
    };
});

根据三次握手的原则,客户端还会返回initialized notification进行通知,服务器在这向客服端发送信息了。

onInitialized

connection.onInitialized(() => {
     connection.window.showInformationMessage('Hello World! form server side');

Listen on the connection

//在最后设置监听
// Listen on the connection
connection.listen();

连接成功时效果如图


image.png

客户端连接LSP服务

import * as path from "path";
import { workspace, ExtensionContext } from "vscode";

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind,
} from "vscode-languageclient/node";

let client: LanguageClient;
export function activate(context: ExtensionContext) {
    // 服务端配置
    let serverModule = context.asAbsolutePath(
        path.join('server', 'out', 'server.js')
    );

    let serverOptions: ServerOptions = {
        module: serverModule, transport: TransportKind.ipc
    };

    // 客户端配置
    let clientOptions: LanguageClientOptions = {
        // js代码触发事情
        documentSelector: [{ scheme: 'file', language: 'html' }],
    };

    client = new LanguageClient(
        'DemoLanguageServer',
        'Demo Language Server',
        serverOptions,
        clientOptions
    );

    // 启动客户端,同时启动语言服务器
    client.start();
}

export function deactivate(): Thenable<void> | undefined {
    if (!client) {
        return undefined;
    }
    return client.stop();
}

写一个完整的LSP工程

先写server端
package.json

server端主要是用到vscode-languageservervscode-languageserver-textdocument

{
    "name": "edm-helper-server",
    "description": "Example implementation of a language server in node.",
    "version": "1.0.0",
    "author": "Microsoft Corporation",
    "license": "MIT",
    "engines": {
        "node": "*"
    },
    "repository": {
    
    },
    "dependencies": {
        "vscode-languageserver": "^7.0.0",
        "vscode-languageserver-textdocument": "^1.0.1"
    },
    "scripts": {}
}
tsconfig.json
{
    "compilerOptions": {
        "target": "es2019",
        "lib": ["ES2019"],
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "strict": true,
        "outDir": "out",
        "rootDir": "src"
    },
    "include": ["src"],
    "exclude": ["node_modules", ".vscode-test"]
}
server.ts

先引入依赖

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  TextDocumentSyncKind,
  InitializeResult,
} from "vscode-languageserver/node";

import { TextDocument } from "vscode-languageserver-textdocument";

这样我们就可以调用createConnection来创建连接了:

let connection = createConnection(ProposedFeatures.all);

还需要生成一个文本管理器用来监听文本的变化

let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

初始化事件

connection.onInitialize((params: InitializeParams) => {
  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
    },
  };
  return result;
});

三次握手之后,我们可以在vscode上显示一条消息:

connection.onInitialized(() => {
  connection.window.showInformationMessage("Hello World! form server side");
});

连接成功后监听文件变化,诊断html文件内容是否符合edm语法规范

documents.onDidChangeContent((change) => {
  connection.window.showInformationMessage("validateTextDocument");
  validateTextDocument(change.document); //检查是否符合语法
});

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

检查的语法规则如下:

let errorList: any[] = [
  {
    content: "position:absolute",
    message: "position is not support by edm",
  },
  {
    content: "position:relative",
    message: "position is not support by edm",
  },
  {
    content: "position:fixed",
    message: "position is not support by edm",
  },
  {
    content: "position",
    message: "position is not support by edm",
  },
  {
    content: "class",
    message: "class is not support by edm, use Inline CSS Style",
  },
  {
    content: "<link>",
    message: "<link> is not support by edm, use Inline CSS Style",
  },
  {
    content: "</link>",
    message: "<link> is not support by edm, use Inline CSS Style",
  },
  {
    content: "<style>",
    message: "<style> is not support by edm, use Inline CSS Style",
  },
  {
    content: "</style>",
    message: "<style> is not support by edm, use Inline CSS Style",
  },
  {
    content: "margin",
    message: "margin will fail in some mailboxes, use padding",
  },
  {
    content: "rgb",
    message:
      "rgb will fail in some mailboxes, use hexadecimal color,like #ffffff",
  },
  {
    content: "!important",
    message: "!important is not support by edm",
  },
  {
    content: "background: url",
    message: "background image is not support by edm",
  },
  {
    content: "background-image",
    message: "background image is not support by edm",
  },
  {
    content: "border-radius",
    message: "border-radius is not support by edm, use Image",
  },
  {
    content: "<div>",
    message: "Do not use div, Use TABLE not DIV",
  },
  {
    content: "</div>",
    message: "Do not use div, Use TABLE not DIV",
  },
  {
    content: "#[a-zA-Z_0-9]{3,3};",
    message: "Do not abbreviate colors, such as #fff, write them as #ffffff",
  },
];
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let regList = errorList.map((item) => {
    return item.content;
  });
  let patternStr = regList.join("|");
  let reg = new RegExp(patternStr, "g");
  let m: RegExpExecArray | null;
  let diagnostics: Diagnostic[] = [];

  while ((m = reg.exec(text))) {
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length),
      },
      message: errorList[regList.indexOf(m[0])].message,
      source: "edmHelper",
    };
    diagnostic.relatedInformation = [
      {
        location: {
          uri: textDocument.uri,
          range: Object.assign({}, diagnostic.range),
        },
        message: "edm grammar",
      },
    ];
    diagnostics.push(diagnostic);
  }
  // Send the computed diagnostics to VSCode.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
client客户端

服务端开发完成后,我们继续写客户端

package.json

客户端要用到的库是vscode-languageclient

{
    "name": "edm-helper-client",
    "description": "VSCode part of a language server",
    "author": "Microsoft Corporation",
    "license": "MIT",
    "version": "0.0.1",
    "publisher": "vscode",
    "repository": {
    },
    "engines": {
        "vscode": "^1.52.0"
    },
    "dependencies": {
        "vscode-languageclient": "^7.0.0"
    },
    "devDependencies": {
        "@types/vscode": "^1.52.0",
        "vscode-test": "^1.3.0"
    }
}
tsconfig.json

与服务端的基本一致

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es2019",
        "lib": ["ES2019"],
        "outDir": "out",
        "rootDir": "src",
        "sourceMap": true
    },
    "include": ["src"],
    "exclude": ["node_modules", ".vscode-test"]
}
extension.ts

与之前给的例子一模一样

import * as path from "path";
import { ExtensionContext } from "vscode";

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind,
} from "vscode-languageclient/node";

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  console.log("activate");
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(
    path.join("server", "out", "server.js")
  );
  let serverOptions: ServerOptions = {
    module: serverModule,
    transport: TransportKind.ipc,
  };

  let clientOptions: LanguageClientOptions = {
    // js代码触发事情
    documentSelector: [{ scheme: "file", language: "html" }],
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    "languageServerExample",
    "Language Server Example",
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

组装运行

插件根目录底下的package.json

package.json

重点在入口文件和activationEvents的配置,配置在打开html文件的时候激活插件

"activationEvents": [
        "onLanguage:html"
  ],
 "main": "./client/out/extension",
{
    "name": "edm-helper",
    "description": "A language server example",
    "author": "Microsoft Corporation",
    "license": "MIT",
    "version": "1.0.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/Microsoft/vscode-extension-samples"
    },
    "publisher": "vscode-samples",
    "categories": [],
    "keywords": [
        "multi-root ready"
    ],
    "engines": {
        "vscode": "^1.43.0"
    },
    "activationEvents": [
        "onLanguage:html"
    ],
    "main": "./client/out/extension",
    "contributes": {
        "configuration": {
            "type": "object",
            "title": "Example configuration",
            "properties": {
                "languageServerExample.maxNumberOfProblems": {
                    "scope": "resource",
                    "type": "number",
                    "default": 100,
                    "description": "Controls the maximum number of problems produced by the server."
                },
                "languageServerExample.trace.server": {
                    "scope": "window",
                    "type": "string",
                    "enum": [
                        "off",
                        "messages",
                        "verbose"
                    ],
                    "default": "off",
                    "description": "Traces the communication between VS Code and the language server."
                }
            }
        }
    },
    "scripts": {
        "vscode:prepublish": "npm run compile",
        "compile": "tsc -b",
        "watch": "tsc -b -w",
        "postinstall": "cd client && npm install && cd ../server && npm install && cd ..",
        "test": "sh ./scripts/e2e.sh"
    },
    "devDependencies": {
        "@types/mocha": "^8.0.3",
        "@types/node": "^12.12.0",
        "@typescript-eslint/parser": "^2.3.0",
        "eslint": "^6.4.0",
        "mocha": "^8.1.1",
        "typescript": "^4.2.2"
    }
}
tsconfig.json

我们还需要一个总的tsconfig.json,引用client和server两个目录:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es2019",
        "lib": ["ES2019"],
        "outDir": "out",
        "rootDir": "src",
        "sourceMap": true
    },
    "include": [
        "src"
    ],
    "exclude": [
        "node_modules",
        ".vscode-test"
    ],
    "references": [
        { "path": "./client" },
        { "path": "./server" }
    ]
}
配置vscode

下面我们在.vscode目录中写两个配置文件,使我们可以更方便地调试和运行。

.vscode/launch.json

有了这个文件之后,我们就有了运行的配置,可以通过F5来启动。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "extensionHost",
            "request": "launch",
            "name": "Launch Client",
            "runtimeExecutable": "${execPath}",
            "args": ["--extensionDevelopmentPath=${workspaceRoot}"],
            "outFiles": ["${workspaceRoot}/client/out/**/*.js"],
            "preLaunchTask": {
                "type": "npm",
                "script": "watch"
            }
        },
        {
            "type": "node",
            "request": "attach",
            "name": "Attach to Server",
            "port": 6009,
            "restart": true,
            "outFiles": ["${workspaceRoot}/server/out/**/*.js"]
        },
    ],
    "compounds": [
        {
            "name": "Client + Server",
            "configurations": ["Launch Client", "Attach to Server"]
        }
    ]
}
.vscode/tasks.json

配置npm compile和npm watch两个脚本。
这样⇧⌘B就可以对生成server和client的out目录下的js和map。

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "npm",
            "script": "compile",
            "group": "build",
            "presentation": {
                "panel": "dedicated",
                "reveal": "never"
            },
            "problemMatcher": [
                "$tsc"
            ]
        },
        {
            "type": "npm",
            "script": "watch",
            "isBackground": true,
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "panel": "dedicated",
                "reveal": "never"
            },
            "problemMatcher": [
                "$tsc-watch"
            ]
        }
    ]
}

这时候就全部完成啦,按⇧⌘B生成js和js.map后。再摁F5调试就能看到效果啦。


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容